diff --git a/Oqtane.Client/Modules/Admin/Files/Details.razor b/Oqtane.Client/Modules/Admin/Files/Details.razor index f1caba10..56eca8c7 100644 --- a/Oqtane.Client/Modules/Admin/Files/Details.razor +++ b/Oqtane.Client/Modules/Admin/Files/Details.razor @@ -27,6 +27,12 @@ +
+ +
+ +
+
@@ -49,6 +55,7 @@ private string _name; private List _folders; private int _folderId = -1; + private string _description = string.Empty; private int _size; private string _createdBy; private DateTime _createdOn; @@ -70,6 +77,7 @@ { _name = file.Name; _folderId = file.FolderId; + _description = file.Description; _size = file.Size; _createdBy = file.CreatedBy; _createdOn = file.CreatedOn; @@ -97,6 +105,7 @@ File file = await FileService.GetFileAsync(_fileId); file.Name = _name; file.FolderId = _folderId; + file.Description = _description; file = await FileService.UpdateFileAsync(file); await logger.LogInformation("File Saved {File}", file); NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 56692be3..4f53443f 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -28,7 +28,7 @@
- +
@@ -47,6 +47,18 @@ }
+
+ +
+ +
+
+
+ +
+ +
+
@@ -84,6 +96,8 @@ private int _parentId = -1; private string _name; private string _type = FolderTypes.Private; + private string _imagesizes = string.Empty; + private string _capacity = "0"; private bool _isSystem; private string _permissions = string.Empty; private string _createdBy; @@ -114,6 +128,8 @@ _parentId = folder.ParentId ?? -1; _name = folder.Name; _type = folder.Type; + _imagesizes = folder.ImageSizes; + _capacity = folder.Capacity.ToString(); _isSystem = folder.IsSystem; _permissions = folder.Permissions; _createdBy = folder.CreatedBy; @@ -125,7 +141,6 @@ else { _parentId = _folders[0].FolderId; - _permissions = string.Empty; } } catch (Exception ex) @@ -178,6 +193,8 @@ folder.Name = _name; folder.Type = _type; + folder.ImageSizes = _imagesizes; + folder.Capacity = int.Parse(_capacity); folder.IsSystem = _isSystem; folder.Permissions = _permissionGrid.GetPermissions(); diff --git a/Oqtane.Client/Modules/Admin/Languages/Add.razor b/Oqtane.Client/Modules/Admin/Languages/Add.razor index 505793e8..0cddca87 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Add.razor @@ -83,15 +83,15 @@ else @((MarkupString)(context.TrialPeriod > 0 ? "  |  " + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "" : "")) - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PackageUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl)) { } - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PaymentUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl)) { - @context.Price.ToString("$#,##0.00") + @context.Price.Value.ToString("$#,##0.00") } else { diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor index 07a1d059..3b861320 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor @@ -40,15 +40,15 @@ @((MarkupString)(context.TrialPeriod > 0 ? "  |  " + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "" : "")) - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PackageUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl)) { } - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PaymentUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl)) { - @context.Price.ToString("$#,##0.00") + @context.Price.Value.ToString("$#,##0.00") } else { diff --git a/Oqtane.Client/Modules/Admin/Themes/Add.razor b/Oqtane.Client/Modules/Admin/Themes/Add.razor index 37bf4c14..661d8b62 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Add.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Add.razor @@ -40,15 +40,15 @@ @((MarkupString)(context.TrialPeriod > 0 ? "  |  " + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "" : "")) - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PackageUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl)) { } - @if (context.Price > 0 && !string.IsNullOrEmpty(context.PaymentUrl)) + @if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl)) { - @context.Price.ToString("$#,##0.00") + @context.Price.Value.ToString("$#,##0.00") } else { diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index d6c7f38c..d5771d47 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -6,12 +6,13 @@ @inject ISettingService SettingService @inject INotificationService NotificationService @inject IFileService FileService +@inject IFolderService FolderService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @if (PageState.User != null && photo != null) { - @displayname + @displayname } else { @@ -19,7 +20,7 @@ else } - @if (PageState.User != null) + @if (profiles != null && settings != null) {
@@ -55,7 +56,7 @@ else
- +
@@ -67,8 +68,6 @@ else @if (profiles != null && settings != null) { - -
@foreach (Profile profile in profiles) @@ -132,11 +131,11 @@ else {
-   -   - @Localizer["From"] - @Localizer["Subject"] - @Localizer["Received"] +   +   + @Localizer["From"] + @Localizer["Subject"] + @Localizer["Received"]
@@ -165,11 +164,11 @@ else {
-   -   - @Localizer["To"] - @Localizer["Subject"] - @Localizer["Sent"] +   +   + @Localizer["To"] + @Localizer["Subject"] + @Localizer["Sent"]
@@ -210,6 +209,7 @@ else private string email = string.Empty; private string displayname = string.Empty; private FileManager filemanager; + private int folderid = -1; private int photofileid = -1; private File photo = null; private List profiles; @@ -230,6 +230,13 @@ else email = PageState.User.Email; displayname = PageState.User.DisplayName; + // get user folder + var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); + if (folder != null) + { + folderid = folder.FolderId; + } + if (PageState.User.PhotoFileId != null) { photofileid = PageState.User.PhotoFileId.Value; diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 5aa5c428..d454a0de 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -59,7 +59,7 @@
}
- @if (_image != string.Empty) + @if (_image != string.Empty && ShowImage) {
@((MarkupString) _image) @@ -110,6 +110,9 @@ [Parameter] public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true + [Parameter] + public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true + [Parameter] public int FileId { get; set; } = -1; // optional - for setting a specific file by default diff --git a/Oqtane.Client/Modules/Controls/Pager.razor b/Oqtane.Client/Modules/Controls/Pager.razor index 3f128785..464431dd 100644 --- a/Oqtane.Client/Modules/Controls/Pager.razor +++ b/Oqtane.Client/Modules/Controls/Pager.razor @@ -2,68 +2,57 @@ @inherits ModuleControlBase @typeparam TableItem -

- @if (Toolbar == "Top") +@if (ItemList != null) +{ + @if (Toolbar == "Top" && _pages > 0 && Items.Count() > _maxItems) {

} - @if (Format == "Table") + @if (Format == "Table" && Row != null) { @@ -81,113 +70,125 @@
} - @if (Format == "Grid") + @if (Format == "Grid" && Row != null) { + int count = 0; + if (ItemList != null) + { + count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns; + }
-
@Header
- @foreach (var item in ItemList) + @if (Header != null) { -
@Row(item)
- @if (Detail != null) - { -
@Detail(item)
- } +
@Header
+ } + @for (int row = 0; row < (count / _columns); row++) + { +
+ @for (int col = 0; col < _columns; col++) + { + int index = (row * _columns) + col; + if (index < ItemList.Count()) + { +
@Row(ItemList.ElementAt(index))
+ } + else + { +
 
+ } + } +
}
} - @if (Toolbar == "Bottom") + @if (Toolbar == "Bottom" && _pages > 0 && Items.Count() > _maxItems) { } -

+} @code { private int _pages = 0; private int _page = 1; private int _maxItems = 10; - private int _maxPages = 5; + private int _displayPages = 5; private int _startPage = 0; private int _endPage = 0; + private int _columns = 1; [Parameter] - public string Format { get; set; } + public string Format { get; set; } // Table or Grid [Parameter] - public string Toolbar { get; set; } + public string Toolbar { get; set; } // Top or Bottom [Parameter] - public RenderFragment Header { get; set; } + public RenderFragment Header { get; set; } = null; [Parameter] - public RenderFragment Row { get; set; } + public RenderFragment Row { get; set; } = null; [Parameter] - public RenderFragment Detail { get; set; } + public RenderFragment Detail { get; set; } = null; // only applicable to Table layouts [Parameter] - public IEnumerable Items { get; set; } + public IEnumerable Items { get; set; } // the IEnumerable data source [Parameter] - public string PageSize { get; set; } + public string PageSize { get; set; } // number of items to display on a page [Parameter] - public string DisplayPages { get; set; } + public string Columns { get; set; } // only applicable to Grid layouts + + [Parameter] + public string CurrentPage { get; set; } // optional property to set the initial page to display + + [Parameter] + public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection [Parameter] public string Class { get; set; } @@ -223,86 +224,89 @@ _maxItems = int.Parse(PageSize); } - if (!string.IsNullOrEmpty(DisplayPages)) + if (!string.IsNullOrEmpty(Columns)) { - _maxPages = int.Parse(DisplayPages); + _columns = int.Parse(Columns); + } + + if (!string.IsNullOrEmpty(DisplayPages)) + { + _displayPages = int.Parse(DisplayPages); + } + + if (!string.IsNullOrEmpty(CurrentPage)) + { + _page = int.Parse(CurrentPage); + } + else + { + _page = 1; } - _page = 1; _startPage = 0; _endPage = 0; if (Items != null) { - ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems); _pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems); + if (_page > _pages) + { + _page = _pages; + } + ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems); + SetPagerSize(); } - - SetPagerSize("forward"); } - public void UpdateList(int currentPage) + public void SetPagerSize() { - ItemList = Items.Skip((currentPage - 1) * _maxItems).Take(_maxItems); - _page = currentPage; - + _startPage = ((_page - 1) / _displayPages) * _displayPages + 1; + _endPage = _startPage + _displayPages - 1; + if (_endPage > _pages) + { + _endPage = _pages; + } StateHasChanged(); } - public void SetPagerSize(string direction) + public void UpdateList(int page) { - if (direction == "forward") - { - if (_endPage + 1 < _pages) - { - _startPage = _endPage + 1; - } - else - { - _startPage = 1; - } + ItemList = Items.Skip((page - 1) * _maxItems).Take(_maxItems); + _page = page; + SetPagerSize(); + } - if (_endPage + _maxPages < _pages) - { - _endPage = _startPage + _maxPages - 1; - } - else - { - _endPage = _pages; - } - - StateHasChanged(); - } - else if (direction == "back") + public void SkipPages(string direction) + { + switch (direction) { - _endPage = _startPage - 1; - _startPage = _startPage - _maxPages; + case "forward": + _page = _endPage + 1; + break; + case "back": + _page = _startPage - 1; + break; } + + SetPagerSize(); } public void NavigateToPage(string direction) { - if (direction == "next") + switch (direction) { - if (_page < _pages) - { - if (_page == _endPage) + case "next": + if (_page < _pages) { - SetPagerSize("forward"); + _page += 1; } - _page += 1; - } - } - else if (direction == "previous") - { - if (_page > 1) - { - if (_page == _startPage) + break; + case "previous": + if (_page > 1) { - SetPagerSize("back"); + _page -= 1; } - _page -= 1; - } + break; } UpdateList(_page); diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 7a6aad03..8db02254 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -134,6 +134,11 @@ namespace Oqtane.Modules return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment); } + public string ImageUrl(int fileid, string size, string mode) + { + return Utilities.ImageUrl(PageState.Alias, fileid, size, mode); + } + public virtual Dictionary GetUrlParameters(string parametersTemplate = "") { var urlParameters = new Dictionary(); diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Details.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Details.resx index 95bac50f..84ddc423 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Details.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Details.resx @@ -144,4 +144,10 @@ Size: + + A description of the file. This can be used as a caption for image files. + + + Description: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index b2740256..81d6a5a6 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -171,4 +171,16 @@ Type: + + Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited. + + + Capacity: + + + Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,x200,200x) + + + Image Sizes: + \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 8ca0abf7..d6e76bd8 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -400,17 +400,17 @@ await PageModuleService.AddPageModuleAsync(pageModule); await PageModuleService.UpdatePageModuleOrderAsync(pageModule.PageId, pageModule.Pane); - Message = $"
{Localizer["Success.Page.ModuleAdd"]}
"; + Message = $"
{Localizer["Success.Page.ModuleAdd"]}
"; NavigationManager.NavigateTo(NavigateUrl()); } else { - Message = $"
{Localizer["Message.Require.ModuleSelect"]}
"; + Message = $"
{Localizer["Message.Require.ModuleSelect"]}
"; } } else { - Message = $"
{Localizer["Error.Authorize.No"]}
"; + Message = $"
{Localizer["Error.Authorize.No"]}
"; } } diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 158321f9..2ab10f7b 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -103,5 +103,10 @@ namespace Oqtane.Themes { return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment); } + + public string ImageUrl(int fileid, string size, string mode) + { + return Utilities.ImageUrl(PageState.Alias, fileid, size, mode); + } } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 0c4217b7..fa91c338 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -138,6 +138,7 @@ if (authState.User.Identity.IsAuthenticated) { user = await UserService.GetUserAsync(authState.User.Identity.Name, site.SiteId); + user.IsAuthenticated = authState.User.Identity.IsAuthenticated; } } else diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 18d38597..efccfa95 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -17,6 +17,9 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Extensions; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -71,7 +74,7 @@ namespace Oqtane.Controllers { foreach (string file in Directory.GetFiles(folder)) { - files.Add(new Models.File {Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "")}); + files.Add(new Models.File { Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "") }); } } } @@ -169,7 +172,11 @@ namespace Oqtane.Controllers string filepath = _files.GetFilePath(file); if (System.IO.File.Exists(filepath)) { - System.IO.File.Delete(filepath); + // remove file and thumbnails + foreach(var f in Directory.GetFiles(Path.GetDirectoryName(filepath), Path.GetFileNameWithoutExtension(filepath) + ".*")) + { + System.IO.File.Delete(f); + } } _logger.Log(LogLevel.Information, this, LogFunction.Delete, "File Deleted {File}", file); @@ -194,7 +201,7 @@ namespace Oqtane.Controllers folder = _folders.GetFolder(FolderId); } - if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.Permissions)) + if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.Permissions)) { string folderPath = _folders.GetFolderPath(folder); CreateDirectory(folderPath); @@ -226,7 +233,11 @@ namespace Oqtane.Controllers } client.DownloadFile(url, targetPath); - file = _files.AddFile(CreateFile(filename, folder.FolderId, targetPath)); + file = CreateFile(filename, folder.FolderId, targetPath); + if (file != null) + { + file = _files.AddFile(file); + } } catch { @@ -235,7 +246,7 @@ namespace Oqtane.Controllers } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {FolderId} {Url}", folderid, url); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Download Attempt {FolderId} {Url}", folderid, url); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } @@ -244,16 +255,16 @@ namespace Oqtane.Controllers // POST api//upload [HttpPost("upload")] - public async Task UploadFile(string folder, IFormFile file) + public async Task UploadFile(string folder, IFormFile formfile) { - if (file.Length <= 0) + if (formfile.Length <= 0) { return; } - if (!file.FileName.IsPathOrFileValid()) + if (!formfile.FileName.IsPathOrFileValid()) { - HttpContext.Response.StatusCode = (int) HttpStatusCode.Conflict; + HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict; return; } @@ -280,20 +291,24 @@ namespace Oqtane.Controllers if (!string.IsNullOrEmpty(folderPath)) { CreateDirectory(folderPath); - using (var stream = new FileStream(Path.Combine(folderPath, file.FileName), FileMode.Create)) + using (var stream = new FileStream(Path.Combine(folderPath, formfile.FileName), FileMode.Create)) { - await file.CopyToAsync(stream); + await formfile.CopyToAsync(stream); } - string upload = await MergeFile(folderPath, file.FileName); + string upload = await MergeFile(folderPath, formfile.FileName); if (upload != "" && FolderId != -1) { - _files.AddFile(CreateFile(upload, FolderId, Path.Combine(folderPath, upload))); + var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload)); + if (file != null) + { + _files.AddFile(file); + } } } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, file); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } @@ -479,6 +494,99 @@ namespace Oqtane.Controllers return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; } + [HttpGet("image/{id}/{size}/{mode?}")] + public IActionResult GetImage(int id, string size, string mode) + { + var file = _files.GetFile(id); + if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions)) + { + if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) + { + var filepath = _files.GetFilePath(file); + if (System.IO.File.Exists(filepath)) + { + size = size.ToLower(); + mode = (string.IsNullOrEmpty(mode)) ? "crop" : mode; + if ((_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.Permissions) || + size.Contains("x") && !string.IsNullOrEmpty(file.Folder.ImageSizes) && file.Folder.ImageSizes.ToLower().Split(",").Contains(size)) + && Enum.TryParse(mode, true, out ResizeMode resizemode)) + { + var imagepath = CreateImage(filepath, size, resizemode.ToString()); + if (!string.IsNullOrEmpty(imagepath)) + { + return PhysicalFile(imagepath, file.GetMimeType()); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Creating Image For File {File} {Size}", file, size); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder Or Invalid Mode Specification {Folder} {Size} {Mode}", file.Folder, size, mode); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FileId} {FilePath}", id, filepath); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Is Not An Image {File}", file); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + + string errorPath = Path.Combine(GetFolderPath("images"), "error.png"); + return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; + } + + private string CreateImage(string filepath, string size, string mode) + { + string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + size + "." + mode.ToLower() + ".png"); + + if (!System.IO.File.Exists(imagepath)) + { + try + { + FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); + using (Image image = Image.Load(stream)) + { + var parts = size.Split('x'); + int width = (!string.IsNullOrEmpty(parts[0])) ? int.Parse(parts[0]) : 0; + int height = (!string.IsNullOrEmpty(parts[1])) ? int.Parse(parts[1]) : 0; + Enum.TryParse(mode, true, out ResizeMode resizemode); + + image.Mutate(x => + x.Resize(new ResizeOptions + { + Size = new Size(width, height), + Mode = resizemode + }) + .BackgroundColor(new Rgba32(255, 255, 255, 0))); + + image.Save(imagepath, new PngEncoder()); + } + stream.Close(); + } + catch // error creating image + { + imagepath = ""; + } + } + + return imagepath; + } + private string GetFolderPath(string folder) { return Utilities.PathCombine(_environment.ContentRootPath, folder); @@ -489,7 +597,7 @@ namespace Oqtane.Controllers if (!Directory.Exists(folderpath)) { string path = ""; - var separators = new char[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar}; + var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries); foreach (string folder in folders) { @@ -504,25 +612,52 @@ namespace Oqtane.Controllers private Models.File CreateFile(string filename, int folderid, string filepath) { - Models.File file = new Models.File(); - file.Name = filename; - file.FolderId = folderid; + Models.File file = null; + + int size = 0; + var folder = _folders.GetFolder(folderid); + if (folder.Capacity != 0) + { + foreach (var f in _files.GetFiles(folderid)) + { + size += f.Size; + } + } FileInfo fileinfo = new FileInfo(filepath); - file.Extension = fileinfo.Extension.ToLower().Replace(".", ""); - file.Size = (int) fileinfo.Length; - file.ImageHeight = 0; - file.ImageWidth = 0; - - if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) + if (folder.Capacity == 0 || ((size + fileinfo.Length) / 1000000) < folder.Capacity) { - FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); - using (Image image = Image.Load(stream)) + file = new Models.File(); + file.Name = filename; + file.FolderId = folderid; + + file.Extension = fileinfo.Extension.ToLower().Replace(".", ""); + file.Size = (int)fileinfo.Length; + file.ImageHeight = 0; + file.ImageWidth = 0; + + if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) { - file.ImageHeight = image.Height; - file.ImageWidth = image.Width; + try + { + FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); + using (Image image = Image.Load(stream)) + { + file.ImageHeight = image.Height; + file.ImageWidth = image.Width; + } + stream.Close(); + } + catch + { + // error opening image file + } } - stream.Close(); + } + else + { + System.IO.File.Delete(filepath); + _logger.Log(LogLevel.Warning, this, LogFunction.Create, "File Exceeds Folder Capacity {Folder} {File}", folder, filepath); } return file; diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 78bb4e53..6e92bc15 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -187,8 +187,10 @@ namespace Oqtane.Controllers ParentId = folder.FolderId, Name = "My Folder", Type = FolderTypes.Private, - Path = Utilities.PathCombine(folder.Path, newUser.UserId.ToString(),Path.DirectorySeparatorChar.ToString()), + Path = Utilities.PathCombine(folder.Path, newUser.UserId.ToString(), Path.DirectorySeparatorChar.ToString()), Order = 1, + ImageSizes = "", + Capacity = Constants.UserFolderCapacity, IsSystem = true, Permissions = new List { @@ -196,7 +198,7 @@ namespace Oqtane.Controllers new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.Edit, newUser.UserId, true) }.EncodePermissions() - }); + }) ; } } } diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 1b8ec468..b553573a 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -631,13 +631,15 @@ namespace Oqtane.Infrastructure Type = FolderTypes.Private, Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()), Order = 1, + ImageSizes = "", + Capacity = Constants.UserFolderCapacity, IsSystem = true, Permissions = new List - { - new Permission(PermissionNames.Browse, user.UserId, true), - new Permission(PermissionNames.View, RoleNames.Everyone, true), - new Permission(PermissionNames.Edit, user.UserId, true), - }.EncodePermissions(), + { + new Permission(PermissionNames.Browse, user.UserId, true), + new Permission(PermissionNames.View, RoleNames.Everyone, true), + new Permission(PermissionNames.Edit, user.UserId, true), + }.EncodePermissions(), }); } } diff --git a/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs b/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs new file mode 100644 index 00000000..3658598c --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/02030001_AddFolderCapacity.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.02.03.00.01")] + public class AddFolderCapacity : MultiDatabaseMigration + { + public AddFolderCapacity(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); + folderEntityBuilder.AddIntegerColumn("Capacity", true); + folderEntityBuilder.UpdateColumn("Capacity", "0"); + folderEntityBuilder.UpdateColumn("Capacity", Constants.UserFolderCapacity.ToString(), $"{ActiveDatabase.RewriteName("Name")} = 'My Folder'"); + folderEntityBuilder.AddStringColumn("ImageSizes", 512, true, true); + + var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); + fileEntityBuilder.AddStringColumn("Description", 512, true, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); + folderEntityBuilder.DropColumn("ImageSizes"); + folderEntityBuilder.DropColumn("Capacity"); + + var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); + fileEntityBuilder.DropColumn("Description"); + } + } +} diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index c47f3977..6200bc63 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -124,7 +124,7 @@ namespace Oqtane.Repository Folder folder = _folderRepository.AddFolder(new Folder { - SiteId = site.SiteId, ParentId = null, Name = "Root", Type = FolderTypes.Private, Path = "", Order = 1, IsSystem = true, + SiteId = site.SiteId, ParentId = null, Name = "Root", Type = FolderTypes.Private, Path = "", Order = 1, ImageSizes = "", Capacity = 0, IsSystem = true, Permissions = new List { new Permission(PermissionNames.Browse, RoleNames.Admin, true), @@ -132,7 +132,7 @@ namespace Oqtane.Repository new Permission(PermissionNames.Edit, RoleNames.Admin, true) }.EncodePermissions() }); - _folderRepository.AddFolder(new Folder { SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Public", Type = FolderTypes.Public, Path = Utilities.PathCombine("Public", Path.DirectorySeparatorChar.ToString()), Order = 1, IsSystem = false, + _folderRepository.AddFolder(new Folder { SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Public", Type = FolderTypes.Public, Path = Utilities.PathCombine("Public", Path.DirectorySeparatorChar.ToString()), Order = 1, ImageSizes = "", Capacity = 0, IsSystem = false, Permissions = new List { new Permission(PermissionNames.Browse, RoleNames.Admin, true), @@ -142,7 +142,7 @@ namespace Oqtane.Repository }); _folderRepository.AddFolder(new Folder { - SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Users", Type = FolderTypes.Private, Path = Utilities.PathCombine("Users",Path.DirectorySeparatorChar.ToString()), Order = 3, IsSystem = true, + SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Users", Type = FolderTypes.Private, Path = Utilities.PathCombine("Users",Path.DirectorySeparatorChar.ToString()), Order = 3, ImageSizes = "", Capacity = 0, IsSystem = true, Permissions = new List { new Permission(PermissionNames.Browse, RoleNames.Admin, true), diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 6d1b549d..5042005c 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -333,7 +333,7 @@ Oqtane.Interop = { var data = new FormData(); data.append('folder', folder); - data.append('file', Chunk, FileName); + data.append('formfile', Chunk, FileName); var request = new XMLHttpRequest(); request.open('POST', posturl, true); request.upload.onloadstart = function (e) { diff --git a/Oqtane.Shared/Models/File.cs b/Oqtane.Shared/Models/File.cs index efba620a..4830e60e 100644 --- a/Oqtane.Shared/Models/File.cs +++ b/Oqtane.Shared/Models/File.cs @@ -50,6 +50,11 @@ namespace Oqtane.Models /// public int ImageWidth { get; set; } + /// + /// Description of a file + /// + public string Description { get; set; } + #region IAuditable Properties /// diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs index ebe80584..d5e26b68 100644 --- a/Oqtane.Shared/Models/Folder.cs +++ b/Oqtane.Shared/Models/Folder.cs @@ -45,7 +45,17 @@ namespace Oqtane.Models public int Order { get; set; } /// - /// TODO: unclear what this is for + /// List of image sizes which can be generated dynamically from uploaded images (ie. 200x200,x200,200x) + /// + public string ImageSizes { get; set; } + + /// + /// Maximum folder capacity (in bytes) + /// + public int Capacity { get; set; } + + /// + /// Folder is a dependency of the framework and cannot be modified or removed /// public bool IsSystem { get; set; } diff --git a/Oqtane.Shared/Models/Package.cs b/Oqtane.Shared/Models/Package.cs index 859bd4ab..1a57e7bc 100644 --- a/Oqtane.Shared/Models/Package.cs +++ b/Oqtane.Shared/Models/Package.cs @@ -67,24 +67,28 @@ namespace Oqtane.Models /// public int Vulnerabilities { get; set; } - /// - /// The price of the package ( if commercial ) - /// - public decimal Price { get; set; } + #region Commercial Properties /// - /// The Url for purchasing the package ( if commercial ) + /// The price of the package + /// + public decimal? Price { get; set; } + + /// + /// The Url for purchasing the package /// public string PaymentUrl { get; set; } /// - /// The trial period in days ( if commercial ) + /// The trial period in days /// public int TrialPeriod { get; set; } /// - /// The expiry date of the package ( if commercial ) + /// The expiry date of the package /// public DateTime? ExpiryDate { get; set; } + + #endregion } } diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index 3a389810..8e7a83af 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -88,5 +88,14 @@ namespace Oqtane.Models /// [NotMapped] public bool IsAuthenticated { get; set; } + + /// + /// The path name of the user's personal folder + /// + [NotMapped] + public string FolderPath + { + get => "Users\\" + UserId.ToString() + "\\"; + } } } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 27e975d3..d4300434 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -40,6 +40,8 @@ namespace Oqtane.Shared { public const string DefaultSiteTemplate = "Oqtane.SiteTemplates.DefaultSiteTemplate, Oqtane.Server"; public const string ContentUrl = "/api/file/download/"; + public const string ImageUrl = "/api/file/image/"; + public const int UserFolderCapacity = 20; // megabytes [Obsolete("Use UserNames.Host instead.")] public const string HostUser = UserNames.Host; diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 1c7845fa..aad5849a 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -112,6 +112,12 @@ namespace Oqtane.Shared return $"{aliasUrl}{Constants.ContentUrl}{fileId}{method}"; } + public static string ImageUrl(Alias alias, int fileId, string size, string mode) + { + var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; + return $"{aliasUrl}{Constants.ImageUrl}{fileId}/{size}/{mode}"; + } + public static string TenantUrl(Alias alias, string url) { url = (!url.StartsWith("/")) ? "/" + url : url;