diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 863db4f6..f3802045 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -329,7 +329,7 @@ } } - if(PagePathIsDeleted(page.Path, page.SiteId, _pageList)) + if (PagePathIsDeleted(page.Path, page.SiteId, _pageList)) { AddModuleMessage(string.Format(Localizer["Message.Page.Deleted"], _path), MessageType.Warning); return; @@ -341,6 +341,12 @@ return; } + if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower())) + { + AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning); + return; + } + Page child; switch (_insert) { diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 3a1f4a67..36578432 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -463,6 +463,12 @@ return; } + if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower())) + { + AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning); + return; + } + if (_insert != "=") { Page child; diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx index 71c3f04f..25b893b7 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx @@ -237,4 +237,7 @@ Meta: + + The page name {0} is reserved. Please enter a different name for your page. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx index 5dc82a55..dec99529 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx @@ -270,4 +270,7 @@ Meta: + + The page name {0} is reserved. Please enter a different name for your page. + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 8ae32116..5bdede15 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -18,15 +18,13 @@ namespace Oqtane.Controllers [Route(ControllerRoutes.ApiRoute)] public class FolderController : Controller { - private readonly IWebHostEnvironment _environment; private readonly IFolderRepository _folders; private readonly IUserPermissions _userPermissions; private readonly ILogManager _logger; private readonly Alias _alias; - public FolderController(IWebHostEnvironment environment, IFolderRepository folders, IUserPermissions userPermissions, ILogManager logger, ITenantManager tenantManager) + public FolderController(IFolderRepository folders, IUserPermissions userPermissions, ILogManager logger, ITenantManager tenantManager) { - _environment = environment; _folders = folders; _userPermissions = userPermissions; _logger = logger; @@ -78,10 +76,10 @@ namespace Oqtane.Controllers [HttpGet("{siteId}/{path}")] public Folder GetByPath(int siteId, string path) { - var folderPath = WebUtility.UrlDecode(path); - if (!(folderPath.EndsWith(System.IO.Path.DirectorySeparatorChar) || folderPath.EndsWith(System.IO.Path.AltDirectorySeparatorChar))) + var folderPath = WebUtility.UrlDecode(path).Replace("\\", "/"); + if (!folderPath.EndsWith("/")) { - folderPath = Utilities.PathCombine(folderPath, System.IO.Path.DirectorySeparatorChar.ToString()); + folderPath += "/"; } Folder folder = _folders.GetFolder(siteId, folderPath); if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.Permissions)) @@ -121,9 +119,9 @@ namespace Oqtane.Controllers if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null) { Folder parent = _folders.GetFolder(folder.ParentId.Value); - folder.Path = Utilities.PathCombine(parent.Path, folder.Name); + folder.Path = Utilities.UrlCombine(parent.Path, folder.Name); } - folder.Path = Utilities.PathCombine(folder.Path, Path.DirectorySeparatorChar.ToString()); + folder.Path = folder.Path + "/"; folder = _folders.AddFolder(folder); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder); } @@ -162,14 +160,14 @@ namespace Oqtane.Controllers if (folder.ParentId != null) { Folder parent = _folders.GetFolder(folder.ParentId.Value); - folder.Path = Utilities.PathCombine(parent.Path, folder.Name); + folder.Path = Utilities.UrlCombine(parent.Path, folder.Name); } - folder.Path = Utilities.PathCombine(folder.Path, Path.DirectorySeparatorChar.ToString()); + folder.Path = folder.Path + "/"; - Models.Folder _folder = _folders.GetFolder(id, false); - if (_folder.Path != folder.Path && Directory.Exists(GetFolderPath(_folder))) + Folder _folder = _folders.GetFolder(id, false); + if (_folder.Path != folder.Path && Directory.Exists(_folders.GetFolderPath(_folder))) { - Directory.Move(GetFolderPath(_folder), GetFolderPath(folder)); + Directory.Move(_folders.GetFolderPath(_folder), _folders.GetFolderPath(folder)); } folder = _folders.UpdateFolder(folder); @@ -226,9 +224,9 @@ namespace Oqtane.Controllers var folder = _folders.GetFolder(id, false); if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, EntityNames.Folder, id, PermissionNames.Edit)) { - if (Directory.Exists(GetFolderPath(folder))) + if (Directory.Exists(_folders.GetFolderPath(folder))) { - Directory.Delete(GetFolderPath(folder)); + Directory.Delete(_folders.GetFolderPath(folder)); } _folders.DeleteFolder(id); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Folder Deleted {FolderId}", id); @@ -239,10 +237,5 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } - - private string GetFolderPath(Folder folder) - { - return Utilities.PathCombine(_environment.ContentRootPath, "Content", "Tenants", _alias.TenantId.ToString(), "Sites", folder.SiteId.ToString(), folder.Path); - } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index bd878f4e..5f17f40c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -273,7 +273,7 @@ namespace Oqtane.Controllers } // remove user folder for site - var folder = _folders.GetFolder(SiteId, Utilities.PathCombine("Users", user.UserId.ToString(), Path.DirectorySeparatorChar.ToString())); + var folder = _folders.GetFolder(SiteId, $"Users{user.UserId}/"); if (folder != null) { if (Directory.Exists(_folders.GetFolderPath(folder))) diff --git a/Oqtane.Server/Infrastructure/TenantManager.cs b/Oqtane.Server/Infrastructure/TenantManager.cs index 8ebcc20d..f34bd3ca 100644 --- a/Oqtane.Server/Infrastructure/TenantManager.cs +++ b/Oqtane.Server/Infrastructure/TenantManager.cs @@ -38,7 +38,7 @@ namespace Oqtane.Infrastructure // legacy support for client api requests which would include the alias as a path prefix ( ie. {alias}/api/[controller] ) int aliasId; string[] segments = httpcontext.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (segments.Length > 1 && (segments[1] == "api" || segments[1] == "pages") && int.TryParse(segments[0], out aliasId)) + if (segments.Length > 1 && Shared.Constants.ReservedRoutes.Contains(segments[1]) && int.TryParse(segments[0], out aliasId)) { alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.AliasId == aliasId); } diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index cdefb7c7..0b26df1d 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -54,6 +54,9 @@ namespace Oqtane.Infrastructure case "3.1.4": Upgrade_3_1_4(tenant, scope); break; + case "3.2.0": + Upgrade_3_2_0(tenant, scope); + break; } } } @@ -238,5 +241,29 @@ namespace Oqtane.Infrastructure } } } + + private void Upgrade_3_2_0(Tenant tenant, IServiceScope scope) + { + try + { + // convert folder paths cross platform format + var siteRepository = scope.ServiceProvider.GetRequiredService(); + var folderRepository = scope.ServiceProvider.GetRequiredService(); + foreach (Site site in siteRepository.GetSites().ToList()) + { + var folders = folderRepository.GetFolders(site.SiteId); + foreach (Folder folder in folders) + { + folder.Path = folder.Path.Replace("\\", "/"); + folderRepository.UpdateFolder(folder); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Oqtane Error: Error In 3.2.0 Upgrade Logic - {ex}"); + } + } + } } diff --git a/Oqtane.Server/Pages/Files.cshtml b/Oqtane.Server/Pages/Files.cshtml new file mode 100644 index 00000000..ea77fc5f --- /dev/null +++ b/Oqtane.Server/Pages/Files.cshtml @@ -0,0 +1,3 @@ +@page "/files/{**path}" +@namespace Oqtane.Pages +@model Oqtane.Pages.FilesModel diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs new file mode 100644 index 00000000..b446c4d9 --- /dev/null +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; +using Oqtane.Extensions; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Security; +using Oqtane.Shared; + +namespace Oqtane.Pages +{ + [AllowAnonymous] + public class FilesModel : PageModel + { + private readonly IWebHostEnvironment _environment; + private readonly IFileRepository _files; + private readonly IUserPermissions _userPermissions; + private readonly IUrlMappingRepository _urlMappings; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ILogManager logger, ITenantManager tenantManager) + { + _environment = environment; + _files = files; + _userPermissions = userPermissions; + _urlMappings = urlMappings; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + public IActionResult OnGet(string path) + { + path = path.Replace("\\", "/"); + var folderpath = ""; + var filename = ""; + + var segments = path.Split('/'); + if (segments.Length > 0) + { + filename = segments[segments.Length - 1].ToLower(); + if (segments.Length > 1) + { + folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/"; + } + } + + var file = _files.GetFile(_alias.SiteId, folderpath, filename); + if (file != null) + { + if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions)) + { + var filepath = _files.GetFilePath(file); + if (System.IO.File.Exists(filepath)) + { + return PhysicalFile(filepath, file.GetMimeType()); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {SiteId} {Path}", _alias.SiteId, path); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + else + { + // look for url mapping + var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename); + if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) + { + var url = urlMapping.MappedUrl; + if (!url.StartsWith("http")) + { + var uri = new Uri(HttpContext.Request.GetEncodedUrl()); + url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url; + } + return RedirectPermanent(url); + } + } + + // broken link + string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot\\images"), "error.png"); + return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)); + } + } +} diff --git a/Oqtane.Server/Repository/AliasRepository.cs b/Oqtane.Server/Repository/AliasRepository.cs index c2d8d690..cbe3bd9c 100644 --- a/Oqtane.Server/Repository/AliasRepository.cs +++ b/Oqtane.Server/Repository/AliasRepository.cs @@ -73,7 +73,7 @@ namespace Oqtane.Repository int start = segments.Length; for (int i = 0; i < segments.Length; i++) { - if (segments[i] == "api" || segments[i] == "pages" || segments[i] == Constants.ModuleDelimiter) + if (Constants.ReservedRoutes.Contains(segments[i]) || segments[i] == Constants.ModuleDelimiter) { start = i; break; diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs index dba0d491..6dbb92fd 100644 --- a/Oqtane.Server/Repository/FileRepository.cs +++ b/Oqtane.Server/Repository/FileRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -82,6 +83,24 @@ namespace Oqtane.Repository return file; } + public File GetFile(int siteId, string folderPath, string fileName) + { + var file = _db.File.AsNoTracking() + .Include(item => item.Folder) + .FirstOrDefault(item => item.Folder.SiteId == siteId && + item.Folder.Path.ToLower() == folderPath && + item.Name.ToLower() == fileName); + + if (file != null) + { + IEnumerable permissions = _permissions.GetPermissions(EntityNames.Folder, file.FolderId).ToList(); + file.Folder.Permissions = permissions.EncodePermissions(); + file.Url = GetFileUrl(file, _tenants.GetAlias()); + } + + return file; + } + public void DeleteFile(int fileId) { File file = _db.File.Find(fileId); @@ -105,17 +124,7 @@ namespace Oqtane.Repository private string GetFileUrl(File file, Alias alias) { - string url = ""; - switch (file.Folder.Type) - { - case FolderTypes.Private: - url = Utilities.ContentUrl(alias, file.FileId); - break; - case FolderTypes.Public: - url = alias.BaseUrl + Utilities.UrlCombine("Content", "Tenants", alias.TenantId.ToString(), "Sites", file.Folder.SiteId.ToString(), file.Folder.Path) + file.Name; - break; - } - return url; + return Utilities.FileUrl(alias, file.Folder.Path, file.Name); } } } diff --git a/Oqtane.Server/Repository/Interfaces/IFileRepository.cs b/Oqtane.Server/Repository/Interfaces/IFileRepository.cs index adfe8f89..0da50f09 100644 --- a/Oqtane.Server/Repository/Interfaces/IFileRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IFileRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Oqtane.Models; namespace Oqtane.Repository @@ -10,6 +10,7 @@ namespace Oqtane.Repository File UpdateFile(File file); File GetFile(int fileId); File GetFile(int fileId, bool tracking); + File GetFile(int siteId, string folderPath, string fileName); void DeleteFile(int fileId); string GetFilePath(int fileId); string GetFilePath(File file); diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index ded32453..0db7c251 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -134,7 +134,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, ImageSizes = "", Capacity = 0, IsSystem = false, + _folderRepository.AddFolder(new Folder { SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Public", Type = FolderTypes.Public, Path = "Public/", Order = 1, ImageSizes = "", Capacity = 0, IsSystem = false, Permissions = new List { new Permission(PermissionNames.Browse, RoleNames.Admin, true), @@ -144,7 +144,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, ImageSizes = "", Capacity = 0, IsSystem = true, + SiteId = site.SiteId, ParentId = folder.FolderId, Name = "Users", Type = FolderTypes.Private, Path = "Users/", Order = 3, ImageSizes = "", Capacity = 0, IsSystem = true, Permissions = new List { new Permission(PermissionNames.Browse, RoleNames.Admin, true), diff --git a/Oqtane.Server/Repository/UserRepository.cs b/Oqtane.Server/Repository/UserRepository.cs index 7f72585c..6c9e31d7 100644 --- a/Oqtane.Server/Repository/UserRepository.cs +++ b/Oqtane.Server/Repository/UserRepository.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using Microsoft.EntityFrameworkCore; using Oqtane.Extensions; @@ -41,7 +40,7 @@ namespace Oqtane.Repository } // add folder for user - Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString())); + Folder folder = _folders.GetFolder(user.SiteId, "Users/"); if (folder != null) { _folders.AddFolder(new Folder @@ -50,7 +49,7 @@ namespace Oqtane.Repository ParentId = folder.FolderId, Name = "My Folder", Type = FolderTypes.Private, - Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()), + Path = $"Users/{user.UserId}/", Order = 1, ImageSizes = "", Capacity = Constants.UserFolderCapacity, diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 926b7705..aabe2ef6 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -26,6 +26,8 @@ namespace Oqtane.Shared [Obsolete("Use PaneNames.Admin")] public const string AdminPane = PaneNames.Admin; + + public static readonly string[] ReservedRoutes = { "api", "pages", "files" }; public const string ModuleDelimiter = "*"; public const string UrlParametersDelimiter = "!"; @@ -91,6 +93,5 @@ namespace Oqtane.Shared public static readonly string HttpContextSiteSettingsKey = "SiteSettings"; public static readonly string MauiUserAgent = "MAUI"; - } } diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index db3b439c..8717c6b2 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -111,6 +111,12 @@ namespace Oqtane.Shared return $"{alias.BaseUrl}{aliasUrl}{Constants.ContentUrl}{fileId}{method}"; } + public static string FileUrl(Alias alias, string folderpath, string filename) + { + var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; + return $"{alias.BaseUrl}{aliasUrl}/files/{folderpath.Replace("\\", "/")}{filename}"; + } + public static string ImageUrl(Alias alias, int fileId, int width, int height, string mode) { return ImageUrl(alias, fileId, width, height, mode, "", "", 0, false); @@ -361,7 +367,8 @@ namespace Oqtane.Shared } public static string UrlCombine(params string[] segments) - { +{ + segments = segments.Where(item => !string.IsNullOrEmpty(item) && item != "/" && item != "\\").ToArray(); for (int i = 1; i < segments.Length; i++) { segments[i] = segments[i].Replace("\\", "/");