diff --git a/.gitignore b/.gitignore index 258c0c62..2d6b767b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ msbuild.binlog *.zip *.idea +_ReSharper.Caches +.DS_Store Oqtane.Server/appsettings.json Oqtane.Server/Data diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index a994ff33..bc1d4780 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 5b81383d..18146904 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -186,8 +186,8 @@ if (firstRender) { var interop = new Interop(JSRuntime); - await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", ""); - await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head"); + await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/css/bootstrap.min.css", "text/css", "sha512-XWTTruHZEYJsxV3W/lSXG1n3Q39YIWOstqvmFsdNEEQfHoZ6vm6E9GK2OrF6DSJSpIbRbi+Nn0WDPID9O7xB2Q==", "anonymous", ""); + await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", "anonymous", "", "head"); } } diff --git a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor index 6c665ba2..97127ba3 100644 --- a/Oqtane.Client/Modules/Admin/Dashboard/Index.razor +++ b/Oqtane.Client/Modules/Admin/Dashboard/Index.razor @@ -10,8 +10,8 @@ if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) { string url = NavigateUrl(p.Path); -
- +
+

@SharedLocalizer[p.Name]
diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index 120ede22..34780aa0 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -132,22 +132,10 @@ _isEnabled = job.IsEnabled.ToString(); _interval = job.Interval.ToString(); _frequency = job.Frequency; - _startDate = job.StartDate; - if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0) - { - _startTime = job.StartDate.Value.ToString("HH:mm"); - } - _endDate = job.EndDate; - if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0) - { - _endTime = job.EndDate.Value.ToString("HH:mm"); - } + (_startDate, _startTime) = Utilities.UtcAsLocalDateAndTime(job.StartDate); + (_endDate, _endTime) = Utilities.UtcAsLocalDateAndTime(job.EndDate); _retentionHistory = job.RetentionHistory.ToString(); - _nextDate = job.NextExecution; - if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0) - { - _nextTime = job.NextExecution.Value.ToString("HH:mm"); - } + (_nextDate, _nextTime) = Utilities.UtcAsLocalDateAndTime(job.NextExecution); createdby = job.CreatedBy; createdon = job.CreatedOn; modifiedby = job.ModifiedBy; @@ -180,50 +168,27 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = _startDate; - if (job.StartDate != null) - { - job.StartDate = job.StartDate.Value.Date; - if (!string.IsNullOrEmpty(_startTime)) - { - job.StartDate = DateTime.Parse(job.StartDate.Value.ToShortDateString() + " " + _startTime); - } - } - job.EndDate = _endDate; - if (job.EndDate != null) - { - job.EndDate = job.EndDate.Value.Date; - if (!string.IsNullOrEmpty(_endTime)) - { - job.EndDate = DateTime.Parse(job.EndDate.Value.ToShortDateString() + " " + _endTime); - } - } - job.RetentionHistory = int.Parse(_retentionHistory); - job.NextExecution = _nextDate; - if (job.NextExecution != null) - { - job.NextExecution = job.NextExecution.Value.Date; - if (!string.IsNullOrEmpty(_nextTime)) - { - job.NextExecution = DateTime.Parse(job.NextExecution.Value.ToShortDateString() + " " + _nextTime); - } - } + job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime); + job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime); + job.RetentionHistory = int.Parse(_retentionHistory); + job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime); + + try + { + job = await JobService.UpdateJobAsync(job); + await logger.LogInformation("Job Updated {Job}", job); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message); + AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); + } + } - try - { - job = await JobService.UpdateJobAsync(job); - await logger.LogInformation("Job Updated {Job}", job); - NavigationManager.NavigateTo(NavigateUrl()); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message); - AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error); - } - } - else - { - AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); - } - } } diff --git a/Oqtane.Client/Modules/Admin/Jobs/Index.razor b/Oqtane.Client/Modules/Admin/Jobs/Index.razor index 3553d973..2ae6e15c 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Index.razor @@ -33,7 +33,7 @@ else @context.Name @DisplayStatus(context.IsEnabled, context.IsExecuting) @DisplayFrequency(context.Interval, context.Frequency) - @context.NextExecution + @context.NextExecution?.ToLocalTime() @if (context.IsStarted) { diff --git a/Oqtane.Client/Modules/Admin/Languages/Add.razor b/Oqtane.Client/Modules/Admin/Languages/Add.razor index c72f21cd..0f371f29 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Add.razor @@ -110,6 +110,10 @@ else } @SharedLocalizer["Cancel"] + +
+
+ } diff --git a/Oqtane.Client/Modules/Admin/Languages/Index.razor b/Oqtane.Client/Modules/Admin/Languages/Index.razor index 069948a7..cb097355 100644 --- a/Oqtane.Client/Modules/Admin/Languages/Index.razor +++ b/Oqtane.Client/Modules/Admin/Languages/Index.razor @@ -30,7 +30,7 @@ else @context.Version - @if (UpgradeAvailable(context.Code)) + @if (UpgradeAvailable(context.Code, context.Version)) { } @@ -40,21 +40,21 @@ else } @code { - private List _languages; - private List _packages; + private List _languages; + private List _packages; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; - protected override async Task OnParametersSetAsync() - { - _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + protected override async Task OnParametersSetAsync() + { + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.PackageId); - var cultures = await LocalizationService.GetCulturesAsync(); + var cultures = await LocalizationService.GetCulturesAsync(); var culture = cultures.First(c => c.Name.Equals(Constants.DefaultCulture)); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - _packages = await PackageService.GetPackagesAsync("translation"); + _packages = await PackageService.GetPackagesAsync("translation"); } } @@ -75,15 +75,16 @@ else } } - private bool UpgradeAvailable(string code) + private bool UpgradeAvailable(string code, string version) { var upgradeavailable = false; if (_packages != null) { - var package = _packages.Where(item => item.PackageId == ("Oqtane.Client." + code)).FirstOrDefault(); - if (package != null) + var package = _packages.Where(item => item.PackageId == (Constants.PackageId + "." + code)).FirstOrDefault(); + if (package != null) { - upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0); + upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0) && + (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0); } } @@ -96,7 +97,7 @@ else { if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - await PackageService.DownloadPackageAsync(Constants.PackageId + ".Client." + code, Constants.Version, Constants.PackagesFolder); + await PackageService.DownloadPackageAsync(Constants.PackageId + "." + code, Constants.Version, Constants.PackagesFolder); await logger.LogInformation("Translation Downloaded {Code} {Version}", code, Constants.Version); await PackageService.InstallPackagesAsync(); AddModuleMessage(string.Format(Localizer["Success.Language.Install"], NavigateUrl("admin/system")), MessageType.Success); diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 14916d80..d237190b 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -3,7 +3,6 @@ @inject NavigationManager NavigationManager @inject IUserService UserService @inject IServiceProvider ServiceProvider -@inject SiteState SiteState @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -184,11 +183,12 @@ var interop = new Interop(JSRuntime); if (await interop.FormValid(login)) { + var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid); var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress}; - + if (!twofactor) { - user = await UserService.LoginUserAsync(user); + user = await UserService.LoginUserAsync(user, hybrid, _remember); } else { @@ -199,10 +199,21 @@ { await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); - // post back to the Login page so that the cookies are set correctly - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; - string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); - await interop.SubmitForm(url, fields); + if (hybrid) + { + // hybrid apps utilize an interactive login + var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider + .GetService(typeof(IdentityAuthenticationStateProvider)); + authstateprovider.NotifyAuthenticationChanged(); + NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true)); + } + else + { + // post back to the Login page so that the cookies are set correctly + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; + string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); + await interop.SubmitForm(url, fields); + } } else { diff --git a/Oqtane.Client/Modules/Admin/Logs/Detail.razor b/Oqtane.Client/Modules/Admin/Logs/Detail.razor index 1953a72d..29ac0f0d 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Detail.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Detail.razor @@ -130,13 +130,14 @@ private string _properties = string.Empty; private string _server = string.Empty; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; + public override string UrlParametersTemplate => "/{id}"; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; protected override async Task OnInitializedAsync() { try { - _logId = Int32.Parse(PageState.QueryString["id"]); + _logId = Int32.Parse(UrlParameters["id"]); var log = await LogService.GetLogAsync(_logId); if (log != null) { @@ -191,13 +192,6 @@ private string CloseUrl() { - if (!PageState.QueryString.ContainsKey("level")) - { - return NavigateUrl(); - } - else - { - return NavigateUrl(PageState.Page.Path, "level=" + PageState.QueryString["level"] + "&function=" + PageState.QueryString["function"] + "&rows=" + PageState.QueryString["rows"] + "&page=" + PageState.QueryString["page"]); - } + return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl(); } } diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index d2af52a0..afd95fdb 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -63,7 +63,7 @@ else @Localizer["Function"] - + @context.LogDate @context.Level @context.Feature @@ -99,29 +99,32 @@ else private List _logs; private string _retention = ""; + public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}"; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; - protected override async Task OnInitializedAsync() + protected override async Task OnParametersSetAsync() { try { + // external link to log item will display Details component if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int id)) { NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, ModuleState.ModuleId, "Detail", $"id={id}")); } - if (PageState.QueryString.ContainsKey("level")) + + if (UrlParameters.ContainsKey("level")) { - _level = PageState.QueryString["level"]; + _level = UrlParameters["level"]; } - if (PageState.QueryString.ContainsKey("function")) + if (UrlParameters.ContainsKey("function")) { - _function = PageState.QueryString["function"]; + _function = UrlParameters["function"]; } - if (PageState.QueryString.ContainsKey("rows")) + if (UrlParameters.ContainsKey("rows")) { - _rows = PageState.QueryString["rows"]; + _rows = UrlParameters["rows"]; } - if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page)) + if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page)) { _page = page; } diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor index 4428cabf..70723195 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Add.razor @@ -114,6 +114,10 @@ @SharedLocalizer["Cancel"] +
+
+ + @code { private List _packages; private string _price = "free"; diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index e514c2ec..c0f8c951 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -1,7 +1,10 @@ @namespace Oqtane.Modules.Admin.ModuleDefinitions @inherits ModuleBase +@using System.Globalization +@using Microsoft.AspNetCore.Localization @inject IModuleDefinitionService ModuleDefinitionService @inject IPackageService PackageService +@inject ILanguageService LanguageService @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -99,35 +102,32 @@ @SharedLocalizer["Cancel"]
- @if (_packages != null) + @if (_languages != null) { - if (_packages.Count > 0) + @if (_languages.Count > 0) { - + +
+ @SharedLocalizer["Name"] + @Localizer["Code"] + @Localizer["Version"] +   +
+ @context.Name + @context.Code + @context.Version -

@context.Name

  by:  @context.Owner
- @(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)
- @(String.Format("{0:n0}", context.Downloads)) @SharedLocalizer["Search.Downloads"]  |   - @SharedLocalizer["Search.Released"]: @context.ReleaseDate.ToString("MMM dd, yyyy")  |   - @SharedLocalizer["Search.Version"]: @context.Version - @((MarkupString)(!string.IsNullOrEmpty(context.PackageUrl) ? "  |  " + SharedLocalizer["Search.Source"] + ": " + new Uri(context.PackageUrl).Host + "" : "")) - @((MarkupString)(context.TrialPeriod > 0 ? "  |  " + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "" : "")) - - - @if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl)) + @if (context.IsDefault) { - - } - - - @if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl)) - { - @context.Price.Value.ToString("$#,##0.00") + } else { - + if (UpgradeAvailable(_packagename + "." + context.Code, context.Version)) + { + + } }
@@ -140,6 +140,7 @@
@Localizer["Search.NoResults"]
+
} }
@@ -169,7 +170,7 @@

@@ -179,110 +180,120 @@ } @code { - private ElementReference form; - private bool validated = false; - private int _moduleDefinitionId; - private string _name; + private ElementReference form; + private bool validated = false; + private int _moduleDefinitionId; + private string _name; private string _description = ""; private string _categories; - private string _moduledefinitionname = ""; + private string _moduledefinitionname = ""; private string _version; private string _packagename = ""; private string _owner = ""; - private string _url = ""; - private string _contact = ""; - private string _license = ""; - private string _runtimes = ""; - private string _permissions; - private string _createdby; - private DateTime _createdon; - private string _modifiedby; - private DateTime _modifiedon; + private string _url = ""; + private string _contact = ""; + private string _license = ""; + private string _runtimes = ""; + private string _permissions; + private string _createdby; + private DateTime _createdon; + private string _modifiedby; + private DateTime _modifiedon; #pragma warning disable 649 - private PermissionGrid _permissionGrid; + private PermissionGrid _permissionGrid; #pragma warning restore 649 private List _packages; + private List _languages; private string _productname = ""; - private string _packageid = ""; private string _packagelicense = ""; - private string _packageversion = ""; + private string _packageid = ""; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; - protected override async Task OnInitializedAsync() - { - try - { - _moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]); - var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); - if (moduleDefinition != null) - { - _name = moduleDefinition.Name; + protected override async Task OnInitializedAsync() + { + try + { + _moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]); + var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); + if (moduleDefinition != null) + { + _name = moduleDefinition.Name; _description = moduleDefinition.Description; - _categories = moduleDefinition.Categories; - _moduledefinitionname = moduleDefinition.ModuleDefinitionName; + _categories = moduleDefinition.Categories; + _moduledefinitionname = moduleDefinition.ModuleDefinitionName; _version = moduleDefinition.Version; _packagename = moduleDefinition.PackageName; _owner = moduleDefinition.Owner; - _url = moduleDefinition.Url; - _contact = moduleDefinition.Contact; - _license = moduleDefinition.License; - _runtimes = moduleDefinition.Runtimes; - _permissions = moduleDefinition.Permissions; - _createdby = moduleDefinition.CreatedBy; - _createdon = moduleDefinition.CreatedOn; - _modifiedby = moduleDefinition.ModifiedBy; - _modifiedon = moduleDefinition.ModifiedOn; + _url = moduleDefinition.Url; + _contact = moduleDefinition.Contact; + _license = moduleDefinition.License; + _runtimes = moduleDefinition.Runtimes; + _permissions = moduleDefinition.Permissions; + _createdby = moduleDefinition.CreatedBy; + _createdon = moduleDefinition.CreatedOn; + _modifiedby = moduleDefinition.ModifiedBy; + _modifiedon = moduleDefinition.ModifiedOn; - _packages = await PackageService.GetPackagesAsync("translation", "", "", moduleDefinition.PackageName); + _packages = await PackageService.GetPackagesAsync("translation", "", "", _packagename); + _languages = await LanguageService.GetLanguagesAsync(-1, _packagename); + foreach (var package in _packages) + { + var code = package.PackageId.Split('.').Last(); + if (!_languages.Any(item => item.Code == code)) + { + _languages.Add(new Language { Code = code, Name = CultureInfo.GetCultureInfo(code).DisplayName, Version = package.Version, IsDefault = true }); + } + } + _languages = _languages.OrderBy(item => item.Name).ToList(); } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message); - AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error); - } - } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error); + } + } - private async Task SaveModuleDefinition() - { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) - { - try - { - var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); - if (moduledefinition.Name != _name) - { - moduledefinition.Name = _name; - } - if (moduledefinition.Description != _description) - { - moduledefinition.Description = _description; - } - if (moduledefinition.Categories != _categories) - { - moduledefinition.Categories = _categories; - } - moduledefinition.Permissions = _permissionGrid.GetPermissions(); - await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition); - await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition); - NavigationManager.NavigateTo(NavigateUrl()); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message); - AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error); - } - } - else - { - AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); - } - } + private async Task SaveModuleDefinition() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + try + { + var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); + if (moduledefinition.Name != _name) + { + moduledefinition.Name = _name; + } + if (moduledefinition.Description != _description) + { + moduledefinition.Description = _description; + } + if (moduledefinition.Categories != _categories) + { + moduledefinition.Categories = _categories; + } + moduledefinition.Permissions = _permissionGrid.GetPermissions(); + await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition); + await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error); + } + } + else + { + AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } private void HideModal() { @@ -290,37 +301,53 @@ _packagelicense = ""; StateHasChanged(); } - - private async Task GetPackage(string packageid, string version) + + private bool UpgradeAvailable(string packagename, string version) { + var upgradeavailable = false; + if (_packages != null) + { + var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault(); + if (package != null) + { + upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0); + } + + } + return upgradeavailable; + } + + private async Task GetPackage(string packagename) + { + var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version; try { - var package = await PackageService.GetPackageAsync(packageid, version); + var package = await PackageService.GetPackageAsync(packagename, version); if (package != null) { _productname = package.Name; - _packageid = package.PackageId; if (!string.IsNullOrEmpty(package.License)) { _packagelicense = package.License.Replace("\n", "
"); } - _packageversion = package.Version; + _packageid = package.PackageId; } StateHasChanged(); } catch (Exception ex) { - await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version); + await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packagename, version); AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error); } } - private async Task DownloadTranslation() + private async Task DownloadPackage(string packagename) { try { - await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder); - await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version); + var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version; + await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder); + await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", packagename, version); AddModuleMessage(Localizer["Success.Translation.Download"], MessageType.Success); _productname = ""; _packagelicense = ""; @@ -328,7 +355,7 @@ } catch (Exception ex) { - await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version); + await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version); AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index fb5938f4..6afb875b 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -19,7 +19,7 @@ @code { private string _content = string.Empty; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; @@ -27,7 +27,7 @@ { try { - _content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId); + _content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId); AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); } catch (Exception ex) diff --git a/Oqtane.Client/Modules/Admin/Modules/Import.razor b/Oqtane.Client/Modules/Admin/Modules/Import.razor index 7241b294..961157eb 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Import.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Import.razor @@ -25,7 +25,7 @@ private ElementReference form; private bool validated = false; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Import Content"; private async Task ImportModule() @@ -38,7 +38,7 @@ { try { - bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, _content); + bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, _content); if (success) { AddModuleMessage(Localizer["Success.Content.Import"], MessageType.Success); diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index 863db4f6..de004f44 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -24,7 +24,7 @@
- @foreach (Page page in _pageList) + @foreach (Page page in PageState.Pages) { if (page.PageId != _pageId) { @@ -201,7 +201,6 @@ private List _themeList; private List _themes = new List(); private List _containers = new List(); - private List _pageList; private List _pageModules; private int _pageId; private string _name; @@ -238,7 +237,6 @@ { try { - _pageList = PageState.Pages; _children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _themeList = await ThemeService.GetThemesAsync(); _themes = ThemeService.GetThemeControls(_themeList); @@ -435,6 +433,10 @@ } if (_path.Contains("/")) { + if (_path.EndsWith("/") && _path != "/") + { + _path = _path.Substring(0, _path.Length - 1); + } _path = _path.Substring(_path.LastIndexOf("/") + 1); } @@ -457,12 +459,19 @@ } } - if (!PagePathIsUnique(page.Path, page.SiteId, page.PageId, _pageList)) + var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); + if (_pages.Any(item => item.Path == page.Path && item.PageId != page.PageId)) { AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning); 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; @@ -567,9 +576,4 @@ NavigationManager.NavigateTo(NavigateUrl()); } } - - private static bool PagePathIsUnique(string pagePath, int siteId, int pageId, List existingPages) - { - return !existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath && page.PageId != pageId); - } } diff --git a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor index 3e93cf7c..34a46b6c 100644 --- a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor +++ b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor @@ -58,7 +58,7 @@ - @PageState.Pages.Find(item => item.PageId == context.PageId).Name + @_pages.Find(item => item.PageId == context.PageId).Name @context.Title @context.DeletedBy @context.DeletedOn @@ -140,6 +140,7 @@ { try { + ModuleInstance.ShowProgressIndicator(); foreach (Page page in _pages) { await PageService.DeletePageAsync(page.PageId); @@ -148,6 +149,7 @@ await logger.LogInformation("Pages Permanently Deleted"); await Load(); + ModuleInstance.HideProgressIndicator(); StateHasChanged(); NavigationManager.NavigateTo(NavigateUrl()); } @@ -155,6 +157,7 @@ { await logger.LogError(ex, "Error Permanently Deleting Pages {Error}", ex.Message); AddModuleMessage(ex.Message, MessageType.Error); + ModuleInstance.HideProgressIndicator(); } } @@ -204,6 +207,7 @@ { try { + ModuleInstance.ShowProgressIndicator(); foreach (Module module in _modules) { await PageModuleService.DeletePageModuleAsync(module.PageModuleId); @@ -218,12 +222,14 @@ await logger.LogInformation("Modules Permanently Deleted"); await Load(); + ModuleInstance.HideProgressIndicator(); StateHasChanged(); } catch (Exception ex) { await logger.LogError(ex, "Error Permanently Deleting Modules {Error}", ex.Message); AddModuleMessage(Localizer["Error.Modules.Delete"], MessageType.Error); + ModuleInstance.HideProgressIndicator(); } } } diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 15a8f019..1aae6b9a 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -70,6 +70,21 @@ }
+ +
+ +
+ +
@@ -228,6 +243,7 @@
@@ -295,6 +311,7 @@ private string _themetype = "-"; private string _containertype = "-"; private string _admincontainertype = "-"; + private string _homepageid = "-"; private string _smtphost = string.Empty; private string _smtpport = string.Empty; private string _smtpssl = "False"; @@ -353,6 +370,11 @@ _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; + if (site.HomePageId != null) + { + _homepageid = site.HomePageId.Value.ToString(); + } + _pwaisenabled = site.PwaIsEnabled.ToString(); if (site.PwaAppIconFileId != null) { @@ -479,6 +501,7 @@ refresh = true; // needs to be refreshed on client } site.AdminContainerType = _admincontainertype; + site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); if (site.PwaIsEnabled.ToString() != _pwaisenabled) { diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index f1ac13fe..a8b7d495 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -89,7 +89,8 @@ else + +
@@ -336,7 +337,7 @@ else user.Username = _hostusername; user.Password = _hostpassword; user.LastIPAddress = PageState.RemoteIPAddress; - user = await UserService.LoginUserAsync(user); + user = await UserService.LoginUserAsync(user, false, false); if (user.IsAuthenticated) { var database = _databases.SingleOrDefault(d => d.Name == _databaseName); diff --git a/Oqtane.Client/Modules/Admin/Themes/Add.razor b/Oqtane.Client/Modules/Admin/Themes/Add.razor index b8717647..f3ca2c98 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Add.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Add.razor @@ -114,6 +114,10 @@ @SharedLocalizer["Cancel"] +
+
+ + @code { private List _packages; private string _price = "free"; diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 98912db2..28166ecb 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -226,8 +226,8 @@ else @code { private string username = string.Empty; private string _password = string.Empty; - private string _passwordtype = "password"; - private string _togglepassword = string.Empty; + private string _passwordtype = "password"; + private string _togglepassword = string.Empty; private string confirm = string.Empty; private bool allowtwofactor = false; private string twofactor = "False"; @@ -429,6 +429,7 @@ else { try { + ModuleInstance.ShowProgressIndicator(); foreach(var Notification in notifications) { if (!Notification.IsDeleted) @@ -444,12 +445,15 @@ else } await logger.LogInformation("Notifications Permanently Deleted"); await LoadNotificationsAsync(); + ModuleInstance.HideProgressIndicator(); + StateHasChanged(); } catch (Exception ex) { await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message); AddModuleMessage(ex.Message, MessageType.Error); + ModuleInstance.HideProgressIndicator(); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 63147056..a8f38030 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -6,7 +6,6 @@ @inject ISiteService SiteService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -@inject SiteState SiteState @if (users == null) { diff --git a/Oqtane.Client/Modules/Admin/Visitors/Detail.razor b/Oqtane.Client/Modules/Admin/Visitors/Detail.razor index 3078eb1b..e0570431 100644 --- a/Oqtane.Client/Modules/Admin/Visitors/Detail.razor +++ b/Oqtane.Client/Modules/Admin/Visitors/Detail.razor @@ -128,13 +128,6 @@ private string CloseUrl() { - if (!PageState.QueryString.ContainsKey("type")) - { - return NavigateUrl(); - } - else - { - return NavigateUrl(PageState.Page.Path, "type=" + PageState.QueryString["type"] + "&days=" + PageState.QueryString["days"] + "&page=" + PageState.QueryString["page"]); - } + return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl(); } } diff --git a/Oqtane.Client/Modules/Admin/Visitors/Index.razor b/Oqtane.Client/Modules/Admin/Visitors/Index.razor index 11deb70f..59e5739c 100644 --- a/Oqtane.Client/Modules/Admin/Visitors/Index.razor +++ b/Oqtane.Client/Modules/Admin/Visitors/Index.razor @@ -43,7 +43,7 @@ else @Localizer["Created"] - + @context.IPAddress @if (context.UserId != null) diff --git a/Oqtane.Client/Modules/Controls/ActionLink.razor b/Oqtane.Client/Modules/Controls/ActionLink.razor index 0ba0dfbe..6f3a29bf 100644 --- a/Oqtane.Client/Modules/Controls/ActionLink.razor +++ b/Oqtane.Client/Modules/Controls/ActionLink.razor @@ -1,4 +1,5 @@ @namespace Oqtane.Modules.Controls +@using System.Net @inherits LocalizableComponent @inject IUserService UserService @@ -71,6 +72,9 @@ [Parameter] public bool IconOnly { get; set; } // optional - specifies only icon in link + [Parameter] + public string ReturnUrl { get; set; } // optional - used to set a url to redirect to + protected override void OnParametersSet() { base.OnParametersSet(); @@ -116,9 +120,13 @@ } _permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions; - _text = Localize(nameof(Text), _text); - _url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters); - _authorized = IsAuthorized(); + _text = Localize(nameof(Text), _text); + _url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters); + if (!string.IsNullOrEmpty(ReturnUrl)) + { + _url += ((_url.Contains("?")) ? "&" : "?") + $"returnurl={WebUtility.UrlEncode(ReturnUrl)}"; + } + _authorized = IsAuthorized(); } private bool IsAuthorized() diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 3ce51e28..b189fb74 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -376,42 +376,62 @@ _messagetype = MessageType.Warning; } } - else - { - _message = Localizer["Message.File.NotSelected"]; - _messagetype = MessageType.Warning; - } - } + else + { + _message = Localizer["Message.File.NotSelected"]; + _messagetype = MessageType.Warning; + } + } - private async Task DeleteFile() - { - _message = string.Empty; - try - { - await FileService.DeleteFileAsync(FileId); - await logger.LogInformation("File Deleted {File}", FileId); - await OnDelete.InvokeAsync(FileId); + private async Task DeleteFile() + { + _message = string.Empty; + try + { + await FileService.DeleteFileAsync(FileId); + await logger.LogInformation("File Deleted {File}", FileId); + await OnDelete.InvokeAsync(FileId); - _message = Localizer["Success.File.Delete"]; - _messagetype = MessageType.Success; + _message = Localizer["Success.File.Delete"]; + _messagetype = MessageType.Success; - await GetFiles(); - FileId = -1; - await SetImage(); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message); + await GetFiles(); + FileId = -1; + await SetImage(); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message); - _message = Localizer["Error.File.Delete"]; - _messagetype = MessageType.Error; - } - } + _message = Localizer["Error.File.Delete"]; + _messagetype = MessageType.Error; + } + } - public int GetFileId() => FileId; + public int GetFileId() => FileId; - public int GetFolderId() => FolderId; + public int GetFolderId() => FolderId; - public File GetFile() => _file; + public File GetFile() => _file; + + public async Task Refresh() + { + await Refresh(-1); + } + + public async Task Refresh(int fileId) + { + await GetFiles(); + if (fileId != -1) + { + var file = _files.Where(item => item.FileId == fileId).FirstOrDefault(); + if (file != null) + { + FileId = file.FileId; + await SetImage(); + } + } + StateHasChanged(); + } } diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index 1e31db60..a67eb421 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -5,79 +5,100 @@
- - @if (AllowFileManagement) - { - @if (_filemanagervisible) - { - - -
- } -
-    - - @if (_filemanagervisible) - { - @((MarkupString)"  ") - - } -
- } -
-
-
- @if (ToolbarContent != null) - { - @ToolbarContent - } - else - { - - - - - - - - - - - - - - - - - - - } -
-
-
-
-
-
- -
- -
- @if (ReadOnly) - { - - } - else - { - - } -
+ + @if (_richfilemanager) + { + + +
+ } +
+ @if (AllowRawHtml) + { + @((MarkupString)"  ") + } + @if (AllowFileManagement) + { + + } + @if (_richfilemanager) + { + @((MarkupString)"  ") + + } +
+
+
+
+ @if (ToolbarContent != null) + { + @ToolbarContent + } + else + { + + + + + + + + + + + + + + + + + + + } +
+
+
+
+
+
+ @if (AllowRawHtml) + { + + @if (_rawfilemanager) + { + + +
+ } +
+    + @if (AllowFileManagement) + { + + } + @if (_rawfilemanager) + { + @((MarkupString)"  ") + + } +
+ @if (ReadOnly) + { + + } + else + { + + } +
+ }
@@ -85,10 +106,11 @@ @code { private ElementReference _editorElement; private ElementReference _toolBar; - private bool _filemanagervisible = false; + private bool _richfilemanager = false; private FileManager _fileManager; private string _richhtml = string.Empty; private string _originalrichhtml = string.Empty; + private bool _rawfilemanager = false; private string _rawhtml = string.Empty; private string _originalrawhtml = string.Empty; private string _message = string.Empty; @@ -102,6 +124,12 @@ [Parameter] public string Placeholder { get; set; } = "Enter Your Content..."; + [Parameter] + public bool AllowFileManagement { get; set; } = true; + + [Parameter] + public bool AllowRawHtml { get; set; } = true; + // parameters only applicable to rich text editor [Parameter] public RenderFragment ToolbarContent { get; set; } @@ -112,9 +140,6 @@ [Parameter] public string DebugLevel { get; set; } = "info"; - [Parameter] - public bool AllowFileManagement { get; set; } = true; - public override List Resources => new List() { new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" }, @@ -152,9 +177,16 @@ } } - public void CloseFileManager() + public void CloseRichFileManager() { - _filemanagervisible = false; + _richfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } + + public void CloseRawFileManager() + { + _rawfilemanager = false; _message = string.Empty; StateHasChanged(); } @@ -194,29 +226,55 @@ return _originalrawhtml; } } - } + } - public async Task InsertImage() - { - _message = string.Empty; - if (_filemanagervisible) - { - var file = _fileManager.GetFile(); - if (file != null) - { - var interop = new RichTextEditorInterop(JSRuntime); - await interop.InsertImage(_editorElement, file.Url, file.Name); - _filemanagervisible = false; - } - else - { - _message = Localizer["Message.Require.Image"]; - } - } - else - { - _filemanagervisible = true; - } - StateHasChanged(); - } + public async Task InsertRichImage() + { + _message = string.Empty; + if (_richfilemanager) + { + var file = _fileManager.GetFile(); + if (file != null) + { + var interop = new RichTextEditorInterop(JSRuntime); + await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name)); + _richfilemanager = false; + } + else + { + _message = Localizer["Message.Require.Image"]; + } + } + else + { + _richfilemanager = true; + } + StateHasChanged(); + } + + public async Task InsertRawImage() + { + _message = string.Empty; + if (_rawfilemanager) + { + var file = _fileManager.GetFile(); + if (file != null) + { + var interop = new Interop(JSRuntime); + int pos = await interop.GetCaretPosition("rawhtmleditor"); + var image = "\"""; + _rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos); + _rawfilemanager = false; + } + else + { + _message = Localizer["Message.Require.Image"]; + } + } + else + { + _rawfilemanager = true; + } + StateHasChanged(); + } } diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index ca86ea95..bb609f80 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -13,7 +13,7 @@ @if (_content != null) { - +
@SharedLocalizer["Cancel"] @@ -60,6 +60,7 @@ private RichTextEditor RichTextEditorHtml; private bool _allowfilemanagement; + private bool _allowrawhtml; private string _content = null; private string _createdby; private DateTime _createdon; @@ -73,6 +74,7 @@ try { _allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true")); + _allowrawhtml = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true")); await LoadContent(); } catch (Exception ex) diff --git a/Oqtane.Client/Modules/HtmlText/Settings.razor b/Oqtane.Client/Modules/HtmlText/Settings.razor index 237049a6..bc1004a3 100644 --- a/Oqtane.Client/Modules/HtmlText/Settings.razor +++ b/Oqtane.Client/Modules/HtmlText/Settings.razor @@ -15,18 +15,29 @@
+
+ +
+ +
+
@code { private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization private string _allowfilemanagement; + private string _allowrawhtml; protected override void OnInitialized() { try { - _allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"); - } + _allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"); + _allowrawhtml = SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true"); + } catch (Exception ex) { ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error); @@ -39,7 +50,8 @@ { var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement); - await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); + settings = SettingService.SetSetting(settings, "AllowRawHtml", _allowrawhtml); + await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); } catch (Exception ex) { diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index b87c6a2c..be2c9d12 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -15,6 +15,8 @@ namespace Oqtane.Modules public abstract class ModuleBase : ComponentBase, IModuleControl { private Logger _logger; + private string _urlparametersstate; + private Dictionary _urlparameters; protected Logger logger => _logger ?? (_logger = new Logger(this)); @@ -24,6 +26,9 @@ namespace Oqtane.Modules [Inject] protected IJSRuntime JSRuntime { get; set; } + [Inject] + protected SiteState SiteState { get; set; } + [CascadingParameter] protected PageState PageState { get; set; } @@ -44,6 +49,21 @@ namespace Oqtane.Modules public virtual List Resources { get; set; } + // url parameters + public virtual string UrlParametersTemplate { get; set; } + + public Dictionary UrlParameters { + get + { + if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters) + { + _urlparametersstate = PageState.UrlParameters; + _urlparameters = GetUrlParameters(UrlParametersTemplate); + } + return _urlparameters; + } + } + // base lifecycle method for handling JSInterop script registration protected override async Task OnAfterRenderAsync(bool firstRender) @@ -55,7 +75,8 @@ namespace Oqtane.Modules var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); + var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + "/" + resource.Url; + scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); } if (scripts.Any()) { @@ -149,15 +170,26 @@ namespace Oqtane.Modules return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode, position, background, rotate, recreate); } - public virtual Dictionary GetUrlParameters(string parametersTemplate = "") + public string AddUrlParameters(params object[] parameters) + { + var url = ""; + for (var i = 0; i < parameters.Length; i++) + { + url += "/" + parameters[i].ToString(); + } + return url; + } + + // template is in the form of a standard route template ie. "/{id}/{name}" and produces dictionary of key/value pairs + // if url parameters belong to a specific module you should embed a unique key into the route (ie. /!/blog/1) and validate the url parameter key in the module + public virtual Dictionary GetUrlParameters(string template = "") { var urlParameters = new Dictionary(); - string[] templateSegments; - var parameters = PageState.UrlParameters.Split('/', StringSplitOptions.RemoveEmptyEntries); - var parameterId = 0; + var parameters = _urlparametersstate.Split('/', StringSplitOptions.RemoveEmptyEntries); - if (string.IsNullOrEmpty(parametersTemplate)) + if (string.IsNullOrEmpty(template)) { + // no template will populate dictionary with generic "parameter#" keys for (int i = 0; i < parameters.Length; i++) { urlParameters.TryAdd("parameter" + i, parameters[i]); @@ -165,39 +197,37 @@ namespace Oqtane.Modules } else { - templateSegments = parametersTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries); + var segments = template.Split('/', StringSplitOptions.RemoveEmptyEntries); + string key; - if (parameters.Length == templateSegments.Length) + for (int i = 0; i < parameters.Length; i++) { - for (int i = 0; i < parameters.Length; i++) + if (i < segments.Length) { - if (parameters.Length > i) + key = segments[i]; + if (key.StartsWith("{") && key.EndsWith("}")) { - if (templateSegments[i] == parameters[i]) - { - urlParameters.TryAdd("parameter" + parameterId, parameters[i]); - parameterId++; - } - else if (templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}")) - { - var key = templateSegments[i].Replace("{", ""); - key = key.Replace("}", ""); - urlParameters.TryAdd(key, parameters[i]); - } - else - { - i = parameters.Length; - urlParameters.Clear(); - } + // dynamic segment + key = key.Substring(1, key.Length - 2); + } + else + { + // static segments use generic "parameter#" keys + key = "parameter" + i.ToString(); } } + else // unspecified segments use generic "parameter#" keys + { + key = "parameter" + i.ToString(); + } + urlParameters.TryAdd(key, parameters[i]); } } return urlParameters; } - // user feedback methods + // UI methods public void AddModuleMessage(string message, MessageType type) { ModuleInstance.AddModuleMessage(message, type); @@ -218,6 +248,18 @@ namespace Oqtane.Modules ModuleInstance.HideProgressIndicator(); } + public void SetModuleTitle(string title) + { + var obj = new { PageModuleId = ModuleState.PageModuleId, Title = title }; + SiteState.Properties.ModuleTitle = obj; + } + + public void SetModuleVisibility(bool visible) + { + var obj = new { PageModuleId = ModuleState.PageModuleId, Visible = visible }; + SiteState.Properties.ModuleVisibility = obj; + } + // logging methods public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args) { diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index 7f5800c1..2d9e6a7b 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -5,15 +5,15 @@ Exe 3.0 Debug;Release - 3.1.4 + 3.2.0 Oqtane Shaun Walker .NET Foundation - Modular Application Framework for Blazor + Modular Application Framework for Blazor and MAUI .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -35,13 +35,8 @@ - - - - - - + false false diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 0a327c7a..610bf26e 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.Loader; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Localization; @@ -15,7 +16,6 @@ using Microsoft.JSInterop; using Oqtane.Documentation; using Oqtane.Modules; using Oqtane.Services; -using Oqtane.Shared; using Oqtane.UI; namespace Oqtane.Client @@ -33,7 +33,7 @@ namespace Oqtane.Client builder.Services.AddOptions(); - // Register localization services + // register localization services builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); // register auth services @@ -42,7 +42,9 @@ namespace Oqtane.Client // register scoped core services builder.Services.AddOqtaneScopedServices(); - await LoadClientAssemblies(httpClient); + var serviceProvider = builder.Services.BuildServiceProvider(); + + await LoadClientAssemblies(httpClient, serviceProvider); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) @@ -54,37 +56,105 @@ namespace Oqtane.Client RegisterClientStartups(assembly, builder.Services); } - var host = builder.Build(); - - await SetCultureFromLocalizationCookie(host.Services); - - ServiceActivator.Configure(host.Services); - - await host.RunAsync(); + await builder.Build().RunAsync(); } - private static async Task LoadClientAssemblies(HttpClient http) + private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider) { - // get list of loaded assemblies on the client - var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); + var dlls = new Dictionary(); + var pdbs = new Dictionary(); + var filter = new List(); - // get assemblies from server and load into client app domain - var zip = await http.GetByteArrayAsync($"/api/Installation/load"); + var jsRuntime = serviceProvider.GetRequiredService(); + var interop = new Interop(jsRuntime); + var files = await interop.GetIndexedDBKeys(".dll"); - // asemblies and debug symbols are packaged in a zip file - using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + if (files.Count() != 0) { - var dlls = new Dictionary(); - var pdbs = new Dictionary(); + // get list of assemblies from server + var json = await http.GetStringAsync("/api/Installation/list"); + var assemblies = JsonSerializer.Deserialize>(json); - foreach (ZipArchiveEntry entry in archive.Entries) + // determine which assemblies need to be downloaded + foreach (var assembly in assemblies) { - if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName))) + var file = files.FirstOrDefault(item => item.Contains(assembly)); + if (file == null) + { + filter.Add(assembly); + } + else + { + // check if newer version available + if (GetFileDate(assembly) > GetFileDate(file)) + { + filter.Add(assembly); + } + } + } + + // get assemblies already downloaded + foreach (var file in files) + { + if (assemblies.Contains(file) && !filter.Contains(file)) + { + try + { + dlls.Add(file, await interop.GetIndexedDBItem(file)); + var pdb = file.Replace(".dll", ".pdb"); + if (files.Contains(pdb)) + { + pdbs.Add(pdb, await interop.GetIndexedDBItem(pdb)); + } + } + catch + { + // ignore + } + } + else // file is deprecated + { + try + { + await interop.RemoveIndexedDBItem(file); + } + catch + { + // ignore + } + } + } + } + else + { + filter.Add("*"); + } + + if (filter.Count != 0) + { + // get assemblies from server and load into client app domain + var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", filter)); + + // asemblies and debug symbols are packaged in a zip file + using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + { + foreach (ZipArchiveEntry entry in archive.Entries) { using (var memoryStream = new MemoryStream()) { entry.Open().CopyTo(memoryStream); byte[] file = memoryStream.ToArray(); + + // save assembly to indexeddb + try + { + await interop.SetIndexedDBItem(entry.FullName, file); + } + catch + { + // ignore + } + switch (Path.GetExtension(entry.FullName)) { case ".dll": @@ -97,21 +167,28 @@ namespace Oqtane.Client } } } + } - foreach (var item in dlls) + // load assemblies into app domain + foreach (var item in dlls) + { + if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb"))) { - if (pdbs.ContainsKey(item.Key)) - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key])); - } - else - { - AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); - } + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")])); + } + else + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); } } } + private static DateTime GetFileDate(string filepath) + { + var segments = filepath.Split('.'); + return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture); + } + private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) { // dynamically register module scoped services diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx index 71c3f04f..3b2128c2 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx @@ -1,4 +1,4 @@ - + + + Exe + 3.2.0 + Oqtane + Shaun Walker + .NET Foundation + Modular Application Framework for Blazor and MAUI + .NET Foundation + https://www.oqtane.org + https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 + https://github.com/oqtane/oqtane.framework + Git + Oqtane.Maui + true + true + enable + false + + + Oqtane.Maui + + + com.oqtane.maui + 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 + + + 3.2.0 + 1 + + 14.2 + 14.0 + 24.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\Oqtane.Server\bin\Debug\net6.0\Oqtane.Client.dll + + + ..\Oqtane.Server\bin\Debug\net6.0\Oqtane.Shared.dll + + + + + diff --git a/Oqtane.Maui/Platforms/Android/AndroidManifest.xml b/Oqtane.Maui/Platforms/Android/AndroidManifest.xml new file mode 100644 index 00000000..2617300d --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Android/MainActivity.cs b/Oqtane.Maui/Platforms/Android/MainActivity.cs new file mode 100644 index 00000000..bcb3c791 --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/MainActivity.cs @@ -0,0 +1,10 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace Oqtane.Maui; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} diff --git a/Oqtane.Maui/Platforms/Android/MainApplication.cs b/Oqtane.Maui/Platforms/Android/MainApplication.cs new file mode 100644 index 00000000..38c8e6ed --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace Oqtane.Maui; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml b/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 00000000..c04d7492 --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Android/Resources/xml/network_security_config.xml b/Oqtane.Maui/Platforms/Android/Resources/xml/network_security_config.xml new file mode 100644 index 00000000..c704f54b --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/Resources/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 10.0.2.2 + + diff --git a/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs b/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 00000000..ea18c92f --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Oqtane.Maui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/MacCatalyst/Info.plist b/Oqtane.Maui/Platforms/MacCatalyst/Info.plist new file mode 100644 index 00000000..c96dd0a2 --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Oqtane.Maui/Platforms/MacCatalyst/Program.cs b/Oqtane.Maui/Platforms/MacCatalyst/Program.cs new file mode 100644 index 00000000..1f902d0e --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Oqtane.Maui; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Tizen/Main.cs b/Oqtane.Maui/Platforms/Tizen/Main.cs new file mode 100644 index 00000000..e9860c5a --- /dev/null +++ b/Oqtane.Maui/Platforms/Tizen/Main.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace Oqtane.Maui; + +class Program : MauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } +} diff --git a/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml b/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 00000000..dc813535 --- /dev/null +++ b/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + appicon.xhigh.png + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Windows/App.xaml b/Oqtane.Maui/Platforms/Windows/App.xaml new file mode 100644 index 00000000..9ace8d3d --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/Oqtane.Maui/Platforms/Windows/App.xaml.cs b/Oqtane.Maui/Platforms/Windows/App.xaml.cs new file mode 100644 index 00000000..334e0b14 --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/App.xaml.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Oqtane.Maui.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/Oqtane.Maui/Platforms/Windows/Package.appxmanifest b/Oqtane.Maui/Platforms/Windows/Package.appxmanifest new file mode 100644 index 00000000..2bcb11ed --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,43 @@ + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/Platforms/Windows/app.manifest b/Oqtane.Maui/Platforms/Windows/app.manifest new file mode 100644 index 00000000..669a2d9a --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Oqtane.Maui/Platforms/iOS/AppDelegate.cs b/Oqtane.Maui/Platforms/iOS/AppDelegate.cs new file mode 100644 index 00000000..ea18c92f --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Oqtane.Maui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/iOS/Info.plist b/Oqtane.Maui/Platforms/iOS/Info.plist new file mode 100644 index 00000000..0004a4fd --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Oqtane.Maui/Platforms/iOS/Program.cs b/Oqtane.Maui/Platforms/iOS/Program.cs new file mode 100644 index 00000000..64159aa8 --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Oqtane.Maui; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/Oqtane.Maui/Properties/launchSettings.json b/Oqtane.Maui/Properties/launchSettings.json new file mode 100644 index 00000000..edf8aadc --- /dev/null +++ b/Oqtane.Maui/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/Oqtane.Maui/Resources/AppIcon/appicon.svg b/Oqtane.Maui/Resources/AppIcon/appicon.svg new file mode 100644 index 00000000..9d63b651 --- /dev/null +++ b/Oqtane.Maui/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Resources/AppIcon/appiconfg.svg b/Oqtane.Maui/Resources/AppIcon/appiconfg.svg new file mode 100644 index 00000000..21dfb25f --- /dev/null +++ b/Oqtane.Maui/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf b/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..563a5baa Binary files /dev/null and b/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/Oqtane.Maui/Resources/Images/dotnet_bot.svg b/Oqtane.Maui/Resources/Images/dotnet_bot.svg new file mode 100644 index 00000000..abfaff26 --- /dev/null +++ b/Oqtane.Maui/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/Resources/Splash/splash.svg b/Oqtane.Maui/Resources/Splash/splash.svg new file mode 100644 index 00000000..21dfb25f --- /dev/null +++ b/Oqtane.Maui/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/_Imports.razor b/Oqtane.Maui/_Imports.razor new file mode 100644 index 00000000..1445aeb8 --- /dev/null +++ b/Oqtane.Maui/_Imports.razor @@ -0,0 +1,7 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Oqtane.Maui diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css new file mode 100644 index 00000000..5da25ceb --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -0,0 +1,215 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +app { + position: relative; + display: flex; + flex-direction: column; +} + +/* Admin Modal */ +.app-admin-modal .modal { + position: fixed; /* Stay in place */ + z-index: 9999; /* Sit on top */ + left: 0; + top: 0; + display: block; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background: rgba(0,0,0,0.3); /* Dim background */ +} + + .app-admin-modal .modal-dialog { + width: 100%; /* Full width */ + height: 100%; /* Full height */ + max-width: none; /* Override default of 500px */ + } + + .app-admin-modal .modal-content { + margin: 5% auto; /* 5% from the top and centered */ + width: 80%; /* Could be more or less, depending on screen size */ + } + +/* Action Dialog */ +.app-actiondialog .modal { + position: fixed; /* Stay in place */ + z-index: 9999; /* Sit on top */ + left: 0; + top: 0; + display: block; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background: rgba(0,0,0,0.3); /* Dim background */ +} + +.app-actiondialog .modal-dialog { + width: 100%; /* Full width */ + height: 100%; /* Full height */ + max-width: none; /* Override default of 500px */ +} + +.app-actiondialog .modal-content { + margin: 15% auto; /* 15% from the top and centered */ + width: 40%; /* Could be more or less, depending on screen size */ +} + +/* Admin Pane */ +.app-pane-admin-border { + width: 100%; + border-width: 1px; + border-style: dashed; + border-color: gray; +} + +.app-pane-admin-title { + width: 100%; + text-align: center; + color: gray; +} + +.app-moduleactions .dropdown-submenu { + position: relative; +} + + .app-moduleactions .dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: 0px; + margin-left: 0px; + } + +.app-progress-indicator { + background: rgba(0,0,0,0.2) url('../loading.gif') no-repeat 50% 50%; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 9999; /* Sit on top */ +} + +.app-rule { + width: 100%; + color: gray; + height: 1px; + background-color: gray; + margin: 0.5rem; +} + +.app-link-unstyled, .app-link-unstyled:visited, .app-link-unstyled:hover, .app-link-unstyled:active, .app-link-unstyled:focus, .app-link-unstyled:active:hover { + font-style: inherit; + color: inherit; + background-color: transparent; + font-size: inherit; + text-decoration: none; + font-variant: inherit; + font-weight: inherit; + line-height: inherit; + font-family: inherit; + border-radius: inherit; + border: inherit; + outline: inherit; + box-shadow: inherit; + padding: inherit; + vertical-align: inherit; +} + +.app-alert { + padding: 20px; + background-color: #f44336; /* red */ + color: white; + margin-bottom: 15px; +} + +.app-moduletitle a { + scroll-margin-top: 7rem; +} + +/* Tooltips */ +.app-tooltip { + cursor: help; + position: relative; +} + + .app-tooltip::before, + .app-tooltip::after { + left: 25%; + opacity: 0; + position: absolute; + z-index: -100; + } + + .app-tooltip:hover::before, + .app-tooltip:focus::before, + .app-tooltip:hover::after, + .app-tooltip:focus::after { + opacity: 1; + transform: scale(1) translateY(0); + z-index: 100; + } + + .app-tooltip::before { + border-style: solid; + border-width: 1em 0.75em 0 0.75em; + border-color: #3E474F transparent transparent transparent; + bottom: 100%; + content: ""; + margin-left: -0.5em; + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26), opacity .65s .5s; + transform: scale(.6) translateY(-90%); + } + + .app-tooltip:hover::before, + .app-tooltip:focus::before { + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26) .2s; + } + + .app-tooltip::after { + background: #3E474F; + border-radius: .25em; + bottom: 140%; + color: #EDEFF0; + content: attr(data-tip); + margin-left: -8.75em; + padding: 1em; + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26) .2s; + transform: scale(.6) translateY(50%); + width: 17.5em; + } + + .app-tooltip:hover::after, + .app-tooltip:focus::after { + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26); + } + +@media (max-width: 760px) { + .app-tooltip::after { + font-size: .75em; + margin-left: -5em; + width: 10em; + } +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/Oqtane.Maui/wwwroot/css/empty.css b/Oqtane.Maui/wwwroot/css/empty.css new file mode 100644 index 00000000..e69de29b diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE b/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 00000000..a1dc03f3 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE b/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 00000000..2199f4a6 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/README.md b/Oqtane.Maui/wwwroot/css/open-iconic/README.md new file mode 100644 index 00000000..6b810e47 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](http://useiconic.com/open) +=========== + +### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 00000000..4664f2e8 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 00000000..f98177db Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf new file mode 100644 index 00000000..f6bd6846 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg new file mode 100644 index 00000000..32b2c4e9 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 00000000..fab60486 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 00000000..f9309988 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff differ diff --git a/Oqtane.Maui/wwwroot/favicon.ico b/Oqtane.Maui/wwwroot/favicon.ico new file mode 100644 index 00000000..550d600e Binary files /dev/null and b/Oqtane.Maui/wwwroot/favicon.ico differ diff --git a/Oqtane.Maui/wwwroot/images/checked.png b/Oqtane.Maui/wwwroot/images/checked.png new file mode 100644 index 00000000..a4100c70 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/checked.png differ diff --git a/Oqtane.Maui/wwwroot/images/error.png b/Oqtane.Maui/wwwroot/images/error.png new file mode 100644 index 00000000..0095d2f1 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/error.png differ diff --git a/Oqtane.Maui/wwwroot/images/help.png b/Oqtane.Maui/wwwroot/images/help.png new file mode 100644 index 00000000..f380be58 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/help.png differ diff --git a/Oqtane.Maui/wwwroot/images/logo-black.png b/Oqtane.Maui/wwwroot/images/logo-black.png new file mode 100644 index 00000000..9463cbf0 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/logo-black.png differ diff --git a/Oqtane.Maui/wwwroot/images/logo-white.png b/Oqtane.Maui/wwwroot/images/logo-white.png new file mode 100644 index 00000000..94454bf6 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/logo-white.png differ diff --git a/Oqtane.Maui/wwwroot/images/null.png b/Oqtane.Maui/wwwroot/images/null.png new file mode 100644 index 00000000..d0f50939 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/null.png differ diff --git a/Oqtane.Maui/wwwroot/images/unchecked.png b/Oqtane.Maui/wwwroot/images/unchecked.png new file mode 100644 index 00000000..566e60a8 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/unchecked.png differ diff --git a/Oqtane.Maui/wwwroot/index.html b/Oqtane.Maui/wwwroot/index.html new file mode 100644 index 00000000..a1469a6b --- /dev/null +++ b/Oqtane.Maui/wwwroot/index.html @@ -0,0 +1,34 @@ + + + + + + Oqtane Maui + + + + + + + + + + + +
+ +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/js/app.js b/Oqtane.Maui/wwwroot/js/app.js new file mode 100644 index 00000000..2c5d837e --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/app.js @@ -0,0 +1,8 @@ +function subMenu(a) { + event.preventDefault(); + event.stopPropagation(); + + var li = a.parentElement, submenu = li.getElementsByTagName('ul')[0]; + submenu.style.display = submenu.style.display == "block" ? "none" : "block"; + return false; +} \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js new file mode 100644 index 00000000..8a677952 --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -0,0 +1,443 @@ +var Oqtane = Oqtane || {}; + +Oqtane.Interop = { + setCookie: function (name, value, days) { + var d = new Date(); + d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = name + "=" + value + ";" + expires + ";path=/"; + }, + getCookie: function (name) { + name = name + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; + }, + updateTitle: function (title) { + if (document.title !== title) { + document.title = title; + } + }, + includeMeta: function (id, attribute, name, content, key) { + var meta; + if (id !== "" && key === "id") { + meta = document.getElementById(id); + } + else { + meta = document.querySelector("meta[" + attribute + "=\"" + CSS.escape(name) + "\"]"); + } + if (meta === null) { + meta = document.createElement("meta"); + meta.setAttribute(attribute, name); + if (id !== "") { + meta.id = id; + } + meta.content = content; + document.head.appendChild(meta); + } + else { + if (id !== "") { + meta.setAttribute("id", id); + } + if (meta.content !== content) { + meta.setAttribute("content", content); + } + } + }, + includeLink: function (id, rel, href, type, integrity, crossorigin, insertbefore) { + var link = document.querySelector("link[href=\"" + CSS.escape(href) + "\"]"); + if (link === null) { + link = document.createElement("link"); + if (id !== "") { + link.id = id; + } + link.rel = rel; + if (type !== "") { + link.type = type; + } + link.href = href; + if (integrity !== "") { + link.integrity = integrity; + } + if (crossorigin !== "") { + link.crossOrigin = crossorigin; + } + if (insertbefore === "") { + document.head.appendChild(link); + } + else { + var sibling = document.getElementById(insertbefore); + sibling.parentNode.insertBefore(link, sibling); + } + } + else { + if (link.id !== id) { + link.setAttribute('id', id); + } + if (link.rel !== rel) { + link.setAttribute('rel', rel); + } + if (type !== "") { + if (link.type !== type) { + link.setAttribute('type', type); + } + } else { + link.removeAttribute('type'); + } + if (link.href !== this.getAbsoluteUrl(href)) { + link.removeAttribute('integrity'); + link.removeAttribute('crossorigin'); + link.setAttribute('href', href); + } + if (integrity !== "") { + if (link.integrity !== integrity) { + link.setAttribute('integrity', integrity); + } + } else { + link.removeAttribute('integrity'); + } + if (crossorigin !== "") { + if (link.crossOrigin !== crossorigin) { + link.setAttribute('crossorigin', crossorigin); + } + } else { + link.removeAttribute('crossorigin'); + } + } + }, + includeLinks: function (links) { + for (let i = 0; i < links.length; i++) { + this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); + } + }, + includeScript: function (id, src, integrity, crossorigin, content, location) { + var script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); + if (script === null) { + script = document.createElement("script"); + if (id !== "") { + script.id = id; + } + if (src !== "") { + script.src = src; + if (integrity !== "") { + script.integrity = integrity; + } + if (crossorigin !== "") { + script.crossOrigin = crossorigin; + } + } + else { + script.innerHTML = content; + } + script.async = false; + this.addScript(script, location) + .then(() => { + console.log(src + ' loaded'); + }) + .catch(() => { + console.error(src + ' failed'); + }); + } + else { + if (script.id !== id) { + script.setAttribute('id', id); + } + if (src !== "") { + if (script.src !== this.getAbsoluteUrl(src)) { + script.removeAttribute('integrity'); + script.removeAttribute('crossorigin'); + script.src = src; + } + if (integrity !== "") { + if (script.integrity !== integrity) { + script.setAttribute('integrity', integrity); + } + } else { + script.removeAttribute('integrity'); + } + if (crossorigin !== "") { + if (script.crossOrigin !== crossorigin) { + script.setAttribute('crossorigin', crossorigin); + } + } else { + script.removeAttribute('crossorigin'); + } + } + else { + if (script.innerHTML !== content) { + script.innerHTML = content; + } + } + } + }, + addScript: function (script, location) { + if (location === 'head') { + document.head.appendChild(script); + } + if (location === 'body') { + document.body.appendChild(script); + } + + return new Promise((res, rej) => { + script.onload = res(); + script.onerror = rej(); + }); + }, + includeScripts: async function (scripts) { + const bundles = []; + for (let s = 0; s < scripts.length; s++) { + if (scripts[s].bundle === '') { + scripts[s].bundle = scripts[s].href; + } + if (!bundles.includes(scripts[s].bundle)) { + bundles.push(scripts[s].bundle); + } + } + const promises = []; + for (let b = 0; b < bundles.length; b++) { + const urls = []; + for (let s = 0; s < scripts.length; s++) { + if (scripts[s].bundle === bundles[b]) { + urls.push(scripts[s].href); + } + } + promises.push(new Promise((resolve, reject) => { + if (loadjs.isDefined(bundles[b])) { + resolve(true); + } + else { + loadjs(urls, bundles[b], { + async: false, + returnPromise: true, + before: function (path, element) { + for (let s = 0; s < scripts.length; s++) { + if (path === scripts[s].href && scripts[s].integrity !== '') { + element.integrity = scripts[s].integrity; + } + if (path === scripts[s].href && scripts[s].crossorigin !== '') { + element.crossOrigin = scripts[s].crossorigin; + } + if (path === scripts[s].href && scripts[s].es6module === true) { + element.type = "module"; + } + } + } + }) + .then(function () { resolve(true) }) + .catch(function (pathsNotFound) { reject(false) }); + } + })); + } + if (promises.length !== 0) { + await Promise.all(promises); + } + }, + getAbsoluteUrl: function (url) { + var a = document.createElement('a'); + getAbsoluteUrl = function (url) { + a.href = url; + return a.href; + } + return getAbsoluteUrl(url); + }, + removeElementsById: function (prefix, first, last) { + var elements = document.querySelectorAll('[id^=' + prefix + ']'); + for (var i = elements.length - 1; i >= 0; i--) { + var element = elements[i]; + if (element.id.startsWith(prefix) && (first === '' || element.id >= first) && (last === '' || element.id <= last)) { + element.parentNode.removeChild(element); + } + } + }, + getElementByName: function (name) { + var elements = document.getElementsByName(name); + if (elements.length) { + return elements[0].value; + } else { + return ""; + } + }, + submitForm: function (path, fields) { + const form = document.createElement('form'); + form.method = 'post'; + form.action = path; + + for (const key in fields) { + if (fields.hasOwnProperty(key)) { + const hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = key; + hiddenField.value = fields[key]; + form.appendChild(hiddenField); + } + } + + document.body.appendChild(form); + form.submit(); + }, + getFiles: function (id) { + var files = []; + var fileinput = document.getElementById(id); + if (fileinput !== null) { + for (var i = 0; i < fileinput.files.length; i++) { + files.push(fileinput.files[i].name); + } + } + return files; + }, + uploadFiles: function (posturl, folder, id, antiforgerytoken) { + var fileinput = document.getElementById(id + 'FileInput'); + var files = fileinput.files; + var progressinfo = document.getElementById(id + 'ProgressInfo'); + var progressbar = document.getElementById(id + 'ProgressBar'); + + progressinfo.setAttribute("style", "display: inline;"); + progressbar.setAttribute("style", "width: 200px; display: inline;"); + + for (var i = 0; i < files.length; i++) { + var FileChunk = []; + var file = files[i]; + var MaxFileSizeMB = 1; + var BufferChunkSize = MaxFileSizeMB * (1024 * 1024); + var FileStreamPos = 0; + var EndPos = BufferChunkSize; + var Size = file.size; + + while (FileStreamPos < Size) { + FileChunk.push(file.slice(FileStreamPos, EndPos)); + FileStreamPos = EndPos; + EndPos = FileStreamPos + BufferChunkSize; + } + + var TotalParts = FileChunk.length; + var PartCount = 0; + + while (Chunk = FileChunk.shift()) { + PartCount++; + var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0'); + + var data = new FormData(); + data.append('__RequestVerificationToken', antiforgerytoken); + data.append('folder', folder); + data.append('formfile', Chunk, FileName); + var request = new XMLHttpRequest(); + request.open('POST', posturl, true); + request.upload.onloadstart = function (e) { + progressinfo.innerHTML = file.name + ' 0%'; + progressbar.value = 0; + }; + request.upload.onprogress = function (e) { + var percent = Math.ceil((e.loaded / e.total) * 100); + progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%'; + progressbar.value = (percent / 100); + }; + request.upload.onloadend = function (e) { + progressinfo.innerHTML = file.name + ' 100%'; + progressbar.value = 1; + }; + request.upload.onerror = function () { + progressinfo.innerHTML = file.name + ' Error: ' + xhr.status; + progressbar.value = 0; + }; + request.send(data); + } + + if (i === files.length - 1) { + fileinput.value = ''; + } + } + }, + refreshBrowser: function (reload, wait) { + setInterval(function () { + window.location.reload(reload); + }, wait * 1000); + }, + redirectBrowser: function (url, wait) { + setInterval(function () { + window.location.href = url; + }, wait * 1000); + }, + formValid: function (formRef) { + return formRef.checkValidity(); + }, + setElementAttribute: function (id, attribute, value) { + var element = document.getElementById(id); + if (element !== null) { + element.setAttribute(attribute, value); + } + }, + scrollTo: function (top, left, behavior) { + window.scrollTo({ + top: top, + left: left, + behavior: behavior + }); + }, + scrollToId: function (id) { + var element = document.getElementById(id); + if (element instanceof HTMLElement) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); + } + }, + getCaretPosition: function (id) { + var element = document.getElementById(id); + return element.selectionStart; + }, + manageIndexedDBItems: async function (action, key, value) { + var idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + if (action.startsWith("get")) { + let request = new Promise((resolve) => { + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readonly"); + let collection = transaction.objectStore("items"); + let result; + if (action === "get") { + result = collection.get(key); + } + if (action === "getallkeys") { + result = collection.getAllKeys(); + } + + result.onsuccess = function (e) { + resolve(result.result); + } + } + }); + + let result = await request; + + return result; + } + else { + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readwrite"); + let collection = transaction.objectStore("items"); + if (action === "put") { + collection.put(value, key); + } + if (action === "delete") { + collection.delete(key); + } + } + } + } +}; diff --git a/Oqtane.Maui/wwwroot/js/loadjs.min.js b/Oqtane.Maui/wwwroot/js/loadjs.min.js new file mode 100644 index 00000000..b2165fc3 --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/loadjs.min.js @@ -0,0 +1 @@ +loadjs=function(){var h=function(){},c={},u={},f={};function o(e,n){if(e){var r=f[e];if(u[e]=n,r)for(;r.length;)r[0](e,n),r.splice(0,1)}}function l(e,n){e.call&&(e={success:e}),n.length?(e.error||h)(n):(e.success||h)(e)}function d(r,t,s,i){var c,o,e=document,n=s.async,u=(s.numRetries||0)+1,f=s.before||h,l=r.replace(/[\?|#].*$/,""),a=r.replace(/^(css|img)!/,"");i=i||0,/(^css!|\.css$)/.test(l)?((o=e.createElement("link")).rel="stylesheet",o.href=a,(c="hideFocus"in o)&&o.relList&&(c=0,o.rel="preload",o.as="style")):/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(l)?(o=e.createElement("img")).src=a:((o=e.createElement("script")).src=r,o.async=void 0===n||n),!(o.onload=o.onerror=o.onbeforeload=function(e){var n=e.type[0];if(c)try{o.sheet.cssText.length||(n="e")}catch(e){18!=e.code&&(n="e")}if("e"==n){if((i+=1) Oqtane.Client - 3.1.4 + 3.2.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 89ff1d1b..5a916fc7 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 3.1.4 + 3.2.0 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v3.1.4/Oqtane.Framework.3.1.2.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/download/v3.2.0/Oqtane.Framework.3.2.0.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index daa86b79..a0bbbbb6 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 3.1.4 + 3.2.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 871ed53d..449e256f 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 3.1.4 + 3.2.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index d9166999..f42dec37 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 3.1.4 + 3.2.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index c8b7443f..089fd43d 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.4.Install.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.2.0.Install.zip" -Force \ No newline at end of file diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 66aa2ef0..f68a8c34 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.1.4.Upgrade.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net6.0\publish\*" -DestinationPath "Oqtane.Framework.3.2.0.Upgrade.zip" -Force \ No newline at end of file diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 27666b66..9f95cd70 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -503,7 +503,7 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } - string errorPath = Path.Combine(GetFolderPath("images"), "error.png"); + string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png"); return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; } @@ -568,7 +568,7 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } - string errorPath = Path.Combine(GetFolderPath("images"), "error.png"); + string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png"); return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; } diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 8ae32116..cb2c0cf0 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -11,22 +11,20 @@ using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; -using Microsoft.AspNetCore.Hosting; +using System; 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; @@ -48,6 +46,7 @@ namespace Oqtane.Controllers folders.Add(folder); } } + folders = GetFoldersHierarchy(folders); } else { @@ -78,10 +77,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 +120,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 +161,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 +225,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); @@ -240,9 +239,47 @@ namespace Oqtane.Controllers } } - private string GetFolderPath(Folder folder) + private static List GetFoldersHierarchy(List folders) { - return Utilities.PathCombine(_environment.ContentRootPath, "Content", "Tenants", _alias.TenantId.ToString(), "Sites", folder.SiteId.ToString(), folder.Path); + List hierarchy = new List(); + Action, Folder> getPath = null; + var folders1 = folders; + getPath = (folderList, folder) => + { + IEnumerable children; + int level; + if (folder == null) + { + level = -1; + children = folders1.Where(item => item.ParentId == null); + } + else + { + level = folder.Level; + children = folders1.Where(item => item.ParentId == folder.FolderId); + } + + foreach (Folder child in children) + { + child.Level = level + 1; + child.HasChildren = folders1.Any(item => item.ParentId == child.FolderId); + hierarchy.Add(child); + if (getPath != null) getPath(folderList, child); + } + }; + folders = folders.OrderBy(item => item.Order).ToList(); + getPath(folders, null); + + // add any non-hierarchical items to the end of the list + foreach (Folder folder in folders) + { + if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null) + { + hierarchy.Add(folder); + } + } + + return hierarchy; } } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 35e94b6f..ff5e63c8 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -18,6 +18,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using System.Collections.Generic; namespace Oqtane.Controllers { @@ -49,7 +50,7 @@ namespace Oqtane.Controllers [HttpPost] public async Task Post([FromBody] InstallConfig config) { - var installation = new Installation {Success = false, Message = ""}; + var installation = new Installation { Success = false, Message = "" }; if (ModelState.IsValid && (User.IsInRole(RoleNames.Host) || string.IsNullOrEmpty(_configManager.GetSetting("ConnectionStrings:" + SettingKeys.ConnectionStringKey, "")))) { @@ -85,7 +86,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Host)] public Installation Upgrade() { - var installation = new Installation {Success = true, Message = ""}; + var installation = new Installation { Success = true, Message = "" }; _installationManager.UpgradeFramework(); return installation; } @@ -98,107 +99,172 @@ namespace Oqtane.Controllers _installationManager.RestartApplication(); } - // GET api//load - [HttpGet("load")] - public IActionResult Load() + // GET api//list + [HttpGet("list")] + public List List() { - return File(GetAssemblies(), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll"); + return _cache.GetOrCreate("assemblieslist", entry => + { + return GetAssemblyList(); + }); } - private byte[] GetAssemblies() - { - return _cache.GetOrCreate("assemblies", entry => + // GET api//load?list=x,y + [HttpGet("load")] + public IActionResult Load(string list = "*") + { + return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll"); + } + + private List GetAssemblyList() + { + // get list of assemblies which should be downloaded to client + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); + var list = assemblies.Select(a => a.GetName().Name).ToList(); + + // include version numbers + for (int i = 0; i < list.Count; i++) { - // get list of assemblies which should be downloaded to client - var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); - var list = assemblies.Select(a => a.GetName().Name).ToList(); - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + list[i] = Path.GetFileName(AddFileDate(Path.Combine(binFolder, list[i] + ".dll"))); + } - // insert satellite assemblies at beginning of list - foreach (var culture in _localizationManager.GetSupportedCultures()) + // insert satellite assemblies at beginning of list + foreach (var culture in _localizationManager.GetInstalledCultures()) + { + var assembliesFolderPath = Path.Combine(binFolder, culture); + if (culture == Constants.DefaultCulture) { - var assembliesFolderPath = Path.Combine(binFolder, culture); - if (culture == Constants.DefaultCulture) - { - continue; - } - - if (Directory.Exists(assembliesFolderPath)) - { - foreach (var resourceFile in Directory.EnumerateFiles(assembliesFolderPath)) - { - list.Insert(0, Path.Combine(culture, Path.GetFileNameWithoutExtension(resourceFile))); - } - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); - } + continue; } - // insert module and theme dependencies at beginning of list - foreach (var assembly in assemblies) + if (Directory.Exists(assembliesFolderPath)) { - foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule)))) + foreach (var resourceFile in Directory.EnumerateFiles(assembliesFolderPath)) { - var instance = Activator.CreateInstance(type) as IModule; - foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) - { - if (!list.Contains(name)) list.Insert(0, name); - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"Module {instance.ModuleDefinition.ModuleDefinitionName} Dependency {name}.dll Does Not Exist")); - } - } + list.Insert(0, culture + "/" + Path.GetFileName(AddFileDate(resourceFile))); } - foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme)))) + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist")); + } + } + + // insert module and theme dependencies at beginning of list + foreach (var assembly in assemblies) + { + foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule)))) + { + var instance = Activator.CreateInstance(type) as IModule; + foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - var instance = Activator.CreateInstance(type) as ITheme; - foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + var path = Path.Combine(binFolder, name + ".dll"); + if (System.IO.File.Exists(path)) { - if (System.IO.File.Exists(Path.Combine(binFolder, name + ".dll"))) - { - if (!list.Contains(name)) list.Insert(0, name); - } - else - { - _filelogger.LogError(Utilities.LogMessage(this, $"Theme {instance.Theme.ThemeName} Dependency {name}.dll Does Not Exist")); - } + path = Path.GetFileName(AddFileDate(path)); + if (!list.Contains(path)) list.Insert(0, path); + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"Module {instance.ModuleDefinition.ModuleDefinitionName} Dependency {name}.dll Does Not Exist")); } } } - - // create zip file containing assemblies and debug symbols - using (var memoryStream = new MemoryStream()) + foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme)))) { - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + var instance = Activator.CreateInstance(type) as ITheme; + foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - foreach (string file in list) + var path = Path.Combine(binFolder, name + ".dll"); + if (System.IO.File.Exists(path)) { - using (var filestream = new FileStream(Path.Combine(binFolder, file + ".dll"), FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(file + ".dll").Open()) + path = Path.GetFileName(AddFileDate(path)); + if (!list.Contains(path)) list.Insert(0, path); + } + else + { + _filelogger.LogError(Utilities.LogMessage(this, $"Theme {instance.Theme.ThemeName} Dependency {name}.dll Does Not Exist")); + } + } + } + } + + return list; + } + + private byte[] GetAssemblies(string list) + { + if (list == "*") + { + return _cache.GetOrCreate("assemblies", entry => + { + return GetZIP(list); + }); + } + else + { + return GetZIP(list); + } + } + + private byte[] GetZIP(string list) + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // get list of assemblies which should be downloaded to client + List assemblies; + if (list == "*") + { + assemblies = GetAssemblyList(); + } + else + { + assemblies = list.Split(',').ToList(); + } + + // create zip file containing assemblies and debug symbols + using (var memoryStream = new MemoryStream()) + { + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + { + foreach (string file in assemblies) + { + var filename = RemoveFileDate(file); + if (System.IO.File.Exists(Path.Combine(binFolder, filename))) + { + using (var filestream = new FileStream(Path.Combine(binFolder, filename), FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(file).Open()) { filestream.CopyTo(entrystream); } - - // include debug symbols - if (System.IO.File.Exists(Path.Combine(binFolder, file + ".pdb"))) + } + filename = filename.Replace(".dll", ".pdb"); + if (System.IO.File.Exists(Path.Combine(binFolder, filename))) + { + using (var filestream = new FileStream(Path.Combine(binFolder, filename), FileMode.Open, FileAccess.Read)) + using (var entrystream = archive.CreateEntry(file.Replace(".dll", ".pdb")).Open()) { - using (var filestream = new FileStream(Path.Combine(binFolder, file + ".pdb"), FileMode.Open, FileAccess.Read)) - using (var entrystream = archive.CreateEntry(file + ".pdb").Open()) - { - filestream.CopyTo(entrystream); - } + filestream.CopyTo(entrystream); } } } - - return memoryStream.ToArray(); } - }); + + return memoryStream.ToArray(); + } + } + + private string AddFileDate(string filepath) + { + DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath); + return Path.GetFileNameWithoutExtension(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); + } + + private string RemoveFileDate(string filepath) + { + var segments = filepath.Split("."); + return string.Join(".", segments, 0, segments.Length - 2) + Path.GetExtension(filepath); } private async Task RegisterContact(string email) diff --git a/Oqtane.Server/Controllers/LanguageController.cs b/Oqtane.Server/Controllers/LanguageController.cs index ef9b0aa1..dc63eda8 100644 --- a/Oqtane.Server/Controllers/LanguageController.cs +++ b/Oqtane.Server/Controllers/LanguageController.cs @@ -12,7 +12,6 @@ using Oqtane.Shared; using System.Linq; using System.Diagnostics; using System.Globalization; -using System; namespace Oqtane.Controllers { @@ -24,9 +23,9 @@ namespace Oqtane.Controllers private readonly ILogManager _logger; private readonly Alias _alias; - public LanguageController(ILanguageRepository language, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + public LanguageController(ILanguageRepository languages, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) { - _languages = language; + _languages = languages; _syncManager = syncManager; _logger = logger; _alias = tenantManager.GetAlias(); @@ -36,23 +35,37 @@ namespace Oqtane.Controllers public IEnumerable Get(string siteid, string packagename) { int SiteId; - if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) + if (int.TryParse(siteid, out SiteId) && (SiteId == _alias.SiteId || SiteId == -1)) { - if (string.IsNullOrEmpty(packagename)) + List languages = new List(); + if (SiteId == -1) { - packagename = "Oqtane"; - } - var languages = _languages.GetLanguages(SiteId).ToList(); - foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}.*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) - { - var code = Path.GetFileName(Path.GetDirectoryName(file)); - if (languages.Any(item => item.Code == code)) + if (!string.IsNullOrEmpty(packagename)) { - languages.Single(item => item.Code == code).Version = FileVersionInfo.GetVersionInfo(file).FileVersion; + foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}.*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) + { + var code = Path.GetFileName(Path.GetDirectoryName(file)); + languages.Add(new Language { Code = code, Name = CultureInfo.GetCultureInfo(code).DisplayName, Version = FileVersionInfo.GetVersionInfo(file).FileVersion, IsDefault = false }); + } } } - var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); - languages.Add(new Language { Code = defaultCulture.Name, Name = defaultCulture.DisplayName, Version = Constants.Version, IsDefault = !languages.Any(l => l.IsDefault) }); + else + { + languages = _languages.GetLanguages(SiteId).ToList(); + if (!string.IsNullOrEmpty(packagename)) + { + foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}.*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) + { + var code = Path.GetFileName(Path.GetDirectoryName(file)); + if (languages.Any(item => item.Code == code)) + { + languages.Single(item => item.Code == code).Version = FileVersionInfo.GetVersionInfo(file).FileVersion; + } + } + } + var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); + languages.Add(new Language { Code = defaultCulture.Name, Name = defaultCulture.DisplayName, Version = Constants.Version, IsDefault = !languages.Any(l => l.IsDefault) }); + } return languages.OrderBy(item => item.Name); } else diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 9337ec58..60d23951 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -205,14 +205,14 @@ namespace Oqtane.Controllers } } - // GET api//export?moduleid=x + // GET api//export?moduleid=x&pageid=y [HttpGet("export")] [Authorize(Roles = RoleNames.Registered)] - public string Export(int moduleid) + public string Export(int moduleid, int pageid) { string content = ""; var module = _modules.GetModule(moduleid); - if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, EntityNames.Module, module.ModuleId, PermissionNames.Edit)) + if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, EntityNames.Page, pageid, PermissionNames.Edit)) { content = _modules.ExportModule(moduleid); if (!string.IsNullOrEmpty(content)) @@ -232,14 +232,14 @@ namespace Oqtane.Controllers return content; } - // POST api//import?moduleid=x + // POST api//import?moduleid=x&pageid=y [HttpPost("import")] [Authorize(Roles = RoleNames.Registered)] - public bool Import(int moduleid, [FromBody] string content) + public bool Import(int moduleid, int pageid, [FromBody] string content) { bool success = false; var module = _modules.GetModule(moduleid); - if (ModelState.IsValid && module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, EntityNames.Module, module.ModuleId, PermissionNames.Edit)) + if (ModelState.IsValid && module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, EntityNames.Page, pageid, PermissionNames.Edit)) { success = _modules.ImportModule(moduleid, content); if (success) diff --git a/Oqtane.Server/Controllers/PageModuleController.cs b/Oqtane.Server/Controllers/PageModuleController.cs index d7015e09..9b396d0d 100644 --- a/Oqtane.Server/Controllers/PageModuleController.cs +++ b/Oqtane.Server/Controllers/PageModuleController.cs @@ -54,7 +54,7 @@ namespace Oqtane.Controllers public PageModule Get(int pageid, int moduleid) { PageModule pagemodule = _pageModules.GetPageModule(pageid, moduleid); - if (pagemodule != null && pagemodule.Module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User,PermissionNames.View, pagemodule.Module.Permissions)) + if (pagemodule != null && pagemodule.Module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, pagemodule.Module.Permissions)) { return pagemodule; } @@ -93,7 +93,7 @@ namespace Oqtane.Controllers public PageModule Put(int id, [FromBody] PageModule pageModule) { var page = _pages.GetPage(pageModule.PageId); - if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, EntityNames.Module, pageModule.ModuleId, PermissionNames.Edit)) + if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, EntityNames.Page, pageModule.PageId, PermissionNames.Edit)) { pageModule = _pageModules.UpdatePageModule(pageModule); _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId); diff --git a/Oqtane.Server/Controllers/SiteController.cs b/Oqtane.Server/Controllers/SiteController.cs index 9a50b91f..a2b7f2b7 100644 --- a/Oqtane.Server/Controllers/SiteController.cs +++ b/Oqtane.Server/Controllers/SiteController.cs @@ -8,6 +8,11 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; using System.Net; +using Oqtane.Security; +using System.Globalization; +using Microsoft.Extensions.Caching.Memory; +using Oqtane.Extensions; +using System; namespace Oqtane.Controllers { @@ -15,17 +20,31 @@ namespace Oqtane.Controllers public class SiteController : Controller { private readonly ISiteRepository _sites; + private readonly IPageRepository _pages; + private readonly IModuleRepository _modules; + private readonly IPageModuleRepository _pageModules; + private readonly IModuleDefinitionRepository _moduleDefinitions; + private readonly ILanguageRepository _languages; + private readonly IUserPermissions _userPermissions; private readonly ISettingRepository _settings; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; + private readonly IMemoryCache _cache; private readonly Alias _alias; - public SiteController(ISiteRepository sites, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public SiteController(ISiteRepository sites, IPageRepository pages, IModuleRepository modules, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache) { _sites = sites; + _pages = pages; + _modules = modules; + _pageModules = pageModules; + _moduleDefinitions = moduleDefinitions; + _languages = languages; + _userPermissions = userPermissions; _settings = settings; _syncManager = syncManager; _logger = logger; + _cache = cache; _alias = tenantManager.GetAlias(); } @@ -41,17 +60,93 @@ namespace Oqtane.Controllers [HttpGet("{id}")] public Site Get(int id) { - var site = _sites.GetSite(id); + if (!User.Identity.IsAuthenticated) + { + return _cache.GetOrCreate($"site:{HttpContext.GetAlias().SiteKey}", entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return GetSite(id); + }); + } + else + { + return GetSite(id); + } + } + + private Site GetSite(int siteid) + { + var site = _sites.GetSite(siteid); if (site.SiteId == _alias.SiteId) { + // site settings site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId) .Where(item => !item.IsPrivate || User.IsInRole(RoleNames.Admin)) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + + // pages + List settings = _settings.GetSettings(EntityNames.Page).ToList(); + site.Pages = new List(); + foreach (Page page in _pages.GetPages(site.SiteId)) + { + if (_userPermissions.IsAuthorized(User, PermissionNames.View, page.Permissions)) + { + page.Settings = settings.Where(item => item.EntityId == page.PageId) + .Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(User, PermissionNames.Edit, page.Permissions)) + .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + site.Pages.Add(page); + } + } + site.Pages = GetPagesHierarchy(site.Pages); + + // modules + List moduledefinitions = _moduleDefinitions.GetModuleDefinitions(site.SiteId).ToList(); + settings = _settings.GetSettings(EntityNames.Module).ToList(); + site.Modules = new List(); + foreach (PageModule pagemodule in _pageModules.GetPageModules(site.SiteId)) + { + if (_userPermissions.IsAuthorized(User, PermissionNames.View, pagemodule.Module.Permissions)) + { + Module module = new Module(); + module.SiteId = pagemodule.Module.SiteId; + module.ModuleDefinitionName = pagemodule.Module.ModuleDefinitionName; + module.AllPages = pagemodule.Module.AllPages; + module.Permissions = pagemodule.Module.Permissions; + module.CreatedBy = pagemodule.Module.CreatedBy; + module.CreatedOn = pagemodule.Module.CreatedOn; + module.ModifiedBy = pagemodule.Module.ModifiedBy; + module.ModifiedOn = pagemodule.Module.ModifiedOn; + module.DeletedBy = pagemodule.DeletedBy; + module.DeletedOn = pagemodule.DeletedOn; + module.IsDeleted = pagemodule.IsDeleted; + + module.PageModuleId = pagemodule.PageModuleId; + module.ModuleId = pagemodule.ModuleId; + module.PageId = pagemodule.PageId; + module.Title = pagemodule.Title; + module.Pane = pagemodule.Pane; + module.Order = pagemodule.Order; + module.ContainerType = pagemodule.ContainerType; + + module.ModuleDefinition = moduledefinitions.Find(item => item.ModuleDefinitionName == module.ModuleDefinitionName); + module.Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId) + .Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(User, PermissionNames.Edit, pagemodule.Module.Permissions)) + .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); + + site.Modules.Add(module); + } + } + + // languages + site.Languages = _languages.GetLanguages(site.SiteId).ToList(); + var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); + site.Languages.Add(new Language { Code = defaultCulture.Name, Name = defaultCulture.DisplayName, Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) }); + return site; } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", siteid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } @@ -119,5 +214,45 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } + + private static List GetPagesHierarchy(List pages) + { + List hierarchy = new List(); + Action, Page> getPath = null; + getPath = (pageList, page) => + { + IEnumerable children; + int level; + if (page == null) + { + level = -1; + children = pages.Where(item => item.ParentId == null); + } + else + { + level = page.Level; + children = pages.Where(item => item.ParentId == page.PageId); + } + foreach (Page child in children) + { + child.Level = level + 1; + child.HasChildren = pages.Any(item => item.ParentId == child.PageId); + hierarchy.Add(child); + getPath(pageList, child); + } + }; + pages = pages.OrderBy(item => item.Order).ToList(); + getPath(pages, null); + + // add any non-hierarchical items to the end of the list + foreach (Page page in pages) + { + if (hierarchy.Find(item => item.PageId == page.PageId) == null) + { + hierarchy.Add(page); + } + } + return hierarchy; + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index da38900e..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))) @@ -315,7 +315,7 @@ namespace Oqtane.Controllers // POST api//login [HttpPost("login")] - public async Task Login([FromBody] User user) + public async Task Login([FromBody] User user, bool setCookie, bool isPersistent) { User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; @@ -358,6 +358,11 @@ namespace Oqtane.Controllers loginUser.LastIPAddress = LastIPAddress; _users.UpdateUser(loginUser); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); + + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } } else { @@ -559,7 +564,7 @@ namespace Oqtane.Controllers var secret = sitesettings.GetValue("JwtOptions:Secret", ""); if (!string.IsNullOrEmpty(secret)) { - token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Audience", "20"))); + token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); } return token; } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 72e013b1..fec61c63 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -29,10 +29,10 @@ namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtane(this IServiceCollection services, string[] supportedCultures) + public static IServiceCollection AddOqtane(this IServiceCollection services, string[] installedCultures) { LoadAssemblies(); - LoadSatelliteAssemblies(supportedCultures); + LoadSatelliteAssemblies(installedCultures); services.AddOqtaneServices(); return services; @@ -326,14 +326,14 @@ namespace Microsoft.Extensions.DependencyInjection } } - private static void LoadSatelliteAssemblies(string[] supportedCultures) + private static void LoadSatelliteAssemblies(string[] installedCultures) { AssemblyLoadContext.Default.Resolving += ResolveDependencies; foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) { var code = Path.GetFileName(Path.GetDirectoryName(file)); - if (supportedCultures.Contains(code)) + if (installedCultures.Contains(code)) { try { diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index e3da4de5..8c217265 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -485,63 +485,67 @@ namespace Oqtane.Infrastructure foreach (var moduleDefinition in moduleDefinitions.GetModuleDefinitions()) { - if (!string.IsNullOrEmpty(moduleDefinition.ReleaseVersions) && !string.IsNullOrEmpty(moduleDefinition.ServerManagerType)) + if (!string.IsNullOrEmpty(moduleDefinition.ReleaseVersions)) { - var moduleType = Type.GetType(moduleDefinition.ServerManagerType); - if (moduleType != null) + var versions = moduleDefinition.ReleaseVersions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + using (var db = GetInstallationContext()) { - var versions = moduleDefinition.ReleaseVersions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - using (var db = GetInstallationContext()) + if (!string.IsNullOrEmpty(moduleDefinition.ServerManagerType)) { - foreach (var tenant in db.Tenant.ToList()) + var moduleType = Type.GetType(moduleDefinition.ServerManagerType); + if (moduleType != null) { - var index = Array.FindIndex(versions, item => item == moduleDefinition.Version); - if (tenant.Name == install.TenantName && install.TenantName != TenantNames.Master) + foreach (var tenant in db.Tenant.ToList()) { - index = -1; - } - if (index != (versions.Length - 1)) - { - for (var i = (index + 1); i < versions.Length; i++) + var index = Array.FindIndex(versions, item => item == moduleDefinition.Version); + if (tenant.Name == install.TenantName && install.TenantName != TenantNames.Master) { - try + index = -1; + } + if (index != (versions.Length - 1)) + { + for (var i = (index + 1); i < versions.Length; i++) { - if (moduleType.GetInterface("IInstallable") != null) + try { - tenantManager.SetTenant(tenant.TenantId); - var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable; - if (moduleObject == null || !moduleObject.Install(tenant, versions[i])) + if (moduleType.GetInterface("IInstallable") != null) { - result.Message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType; + tenantManager.SetTenant(tenant.TenantId); + var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable; + if (moduleObject == null || !moduleObject.Install(tenant, versions[i])) + { + result.Message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType; + } + } + else + { + if (!sql.ExecuteScript(tenant, moduleType.Assembly, Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql")) + { + result.Message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql"; + } } } - else + catch (Exception ex) { - if (!sql.ExecuteScript(tenant, moduleType.Assembly, Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql")) - { - result.Message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql"; - } + result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.Message; } } - catch (Exception ex) - { - result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.Message; - } } } } - if (string.IsNullOrEmpty(result.Message) && moduleDefinition.Version != versions[versions.Length - 1]) - { - // get module definition from database to retain user customizable property values - var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId); - moduleDefinition.Name = moduledef.Name; - moduleDefinition.Description = moduledef.Description; - moduleDefinition.Categories = moduledef.Categories; - // update version - moduleDefinition.Version = versions[versions.Length - 1]; - db.Entry(moduleDefinition).State = EntityState.Modified; - db.SaveChanges(); - } + } + + if (string.IsNullOrEmpty(result.Message) && moduleDefinition.Version != versions[versions.Length - 1]) + { + // get module definition from database to retain user customizable property values + var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId); + moduleDefinition.Name = moduledef.Name; + moduleDefinition.Description = moduledef.Description; + moduleDefinition.Categories = moduledef.Categories; + // update version + moduleDefinition.Version = versions[versions.Length - 1]; + db.Entry(moduleDefinition).State = EntityState.Modified; + db.SaveChanges(); } } } diff --git a/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs index d85f0923..b6de309a 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs @@ -3,7 +3,7 @@ namespace Oqtane.Infrastructure public interface ILocalizationManager { string GetDefaultCulture(); - string[] GetSupportedCultures(); + string[] GetInstalledCultures(); } } diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 8eb22a97..ff4a88f8 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -17,7 +17,7 @@ namespace Oqtane.Infrastructure Name = "Purge Job"; Frequency = "d"; // daily Interval = 1; - StartDate = DateTime.ParseExact("03:00", "H:mm", null, System.Globalization.DateTimeStyles.None); // 3 AM + StartDate = DateTime.ParseExact("03:00", "H:mm", null, System.Globalization.DateTimeStyles.AssumeLocal).ToUniversalTime(); // 3 AM IsEnabled = true; } diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index cee23406..7faf27ca 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -1,6 +1,10 @@ +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Options; +using Oqtane.Models; using Oqtane.Shared; namespace Oqtane.Infrastructure @@ -29,8 +33,18 @@ namespace Oqtane.Infrastructure } public string[] GetSupportedCultures() - { + { return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(item => item.Name).OrderBy(c => c).ToArray(); } + + public string[] GetInstalledCultures() + { + var cultures = new List(); + foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"Oqtane.Client{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) + { + cultures.Add(Path.GetFileName(Path.GetDirectoryName(file))); + } + return cultures.OrderBy(c => c).ToArray(); + } } } diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index c1a65d4a..29058fea 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -51,18 +51,18 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }.EncodePermissions() , PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Welcome To Oqtane...", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Welcome To Oqtane...", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }.EncodePermissions(), - Content = "

Oqtane is an open source modular application framework that provides advanced functionality for developing web and mobile applications on .NET Core. It leverages the Blazor component model to compose a fully dynamic web development experience which can be hosted either client-side or server-side. Whether you are looking for a platform to accelerate your web development efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.

" + + Content = "

Oqtane is an open source modular application framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET Core. It leverages the Blazor component model to compose a fully dynamic web development experience which can be hosted either client-side or server-side. Whether you are looking for a platform to accelerate your web development efforts, or simply interested in exploring the anatomy of a large-scale Blazor application, Oqtane provides a solid foundation based on proven enterprise architectural principles.

" + "

Join Our Community  Clone Our Repo

" + - "

Blazor is an open source and cross-platform web UI framework for building single-page apps using .NET and C# instead of JavaScript. Blazor WebAssembly relies on Wasm, an open web standard that does not require plugins or code transpilation in order to run natively in a web browser. Blazor Server uses SignalR to host your application on a web server and provide a responsive and robust development experience. Blazor applications work in all modern web browsers, including mobile browsers.

" + + "

Blazor is an open source and cross-platform web UI framework for building single-page applications using .NET and C#. Blazor applications can be hosted in a variety of ways. Blazor Server uses SignalR (WebSockets) to host your application on a web server and provide a responsive and robust development experience. Blazor WebAssembly relies on Wasm, an open web standard that does not require plugins in order for applications to run natively in a web browser. Blazor Hybrid is part of .NET MAUI and uses a Web View to render components natively on mobile and desktop devices. Razor components can be used with all of the hosting models without any modification.

" + "

Blazor is a feature of .NET Core, the popular cross platform web development framework from Microsoft that extends the .NET developer platform with tools and libraries for building web apps.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -73,7 +73,7 @@ namespace Oqtane.SiteTemplates "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -98,7 +98,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }.EncodePermissions(), PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -123,7 +123,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }.EncodePermissions(), PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "My Page", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "My Page", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -147,14 +147,14 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Host, true) }.EncodePermissions(), PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Software Development", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Software Development", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), new Permission(PermissionNames.Edit, RoleNames.Host, true) }.EncodePermissions(), Content = "

Oqtane offers a Module Creator which allows you to create new modules to extend the framework with additional capabilities. Simply provide some basic information and the system will scaffold a completely functional module which includes all of the necessary code files and assets to get you up and running as quickly as possible.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.ModuleCreator, Oqtane.Client", Title = "Module Creator", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.ModuleCreator, Oqtane.Client", Title = "Module Creator", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), new Permission(PermissionNames.Edit, RoleNames.Host, true) diff --git a/Oqtane.Server/Infrastructure/SyncManager.cs b/Oqtane.Server/Infrastructure/SyncManager.cs index b4f4c551..b5ac1dc5 100644 --- a/Oqtane.Server/Infrastructure/SyncManager.cs +++ b/Oqtane.Server/Infrastructure/SyncManager.cs @@ -1,17 +1,21 @@ +using Microsoft.Extensions.Caching.Memory; using Oqtane.Models; +using Oqtane.Shared; using System; using System.Collections.Generic; using System.Linq; -using Oqtane.Repository; +using System.Reflection; namespace Oqtane.Infrastructure { public class SyncManager : ISyncManager { + private readonly IMemoryCache _cache; private List SyncEvents { get; set; } - public SyncManager() + public SyncManager(IMemoryCache cache) { + _cache = cache; SyncEvents = new List(); } @@ -28,6 +32,10 @@ namespace Oqtane.Infrastructure public void AddSyncEvent(int tenantId, string entityName, int entityId, bool reload) { SyncEvents.Add(new SyncEvent { TenantId = tenantId, EntityName = entityName, EntityId = entityId, Reload = reload, ModifiedOn = DateTime.UtcNow }); + if (entityName == EntityNames.Site) +{ + _cache.Remove($"site:{tenantId}:{entityId}"); + } // trim sync events SyncEvents.RemoveAll(item => item.ModifiedOn < DateTime.UtcNow.AddHours(-1)); } diff --git a/Oqtane.Server/Infrastructure/TenantManager.cs b/Oqtane.Server/Infrastructure/TenantManager.cs index de3ca94e..f34bd3ca 100644 --- a/Oqtane.Server/Infrastructure/TenantManager.cs +++ b/Oqtane.Server/Infrastructure/TenantManager.cs @@ -32,12 +32,13 @@ namespace Oqtane.Infrastructure else { // if there is http context - if (_httpContextAccessor.HttpContext != null) + var httpcontext = _httpContextAccessor.HttpContext; + if (httpcontext != null) { // legacy support for client api requests which would include the alias as a path prefix ( ie. {alias}/api/[controller] ) int aliasId; - string[] segments = _httpContextAccessor.HttpContext.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (segments.Length > 1 && (segments[1] == "api" || segments[1] == "pages") && int.TryParse(segments[0], out aliasId)) + string[] segments = httpcontext.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + 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); } @@ -45,13 +46,19 @@ namespace Oqtane.Infrastructure // resolve alias based on host name and path if (alias == null) { - string name = _httpContextAccessor.HttpContext.Request.Host.Value + _httpContextAccessor.HttpContext.Request.Path; + string name = httpcontext.Request.Host.Value + httpcontext.Request.Path; alias = _aliasRepository.GetAlias(name); } // if there is a match save it if (alias != null) { + alias.Protocol = (httpcontext.Request.IsHttps) ? "https://" : "http://"; + alias.BaseUrl = ""; + if (httpcontext.Request.Headers.ContainsKey("User-Agent") && httpcontext.Request.Headers["User-Agent"] == Shared.Constants.MauiUserAgent) + { + alias.BaseUrl = alias.Protocol + alias.Name; + } _siteState.Alias = alias; } } diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index cdefb7c7..8ce40fb2 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; } } } @@ -140,7 +143,7 @@ namespace Oqtane.Infrastructure { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -169,7 +172,7 @@ namespace Oqtane.Infrastructure { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -216,7 +219,7 @@ namespace Oqtane.Infrastructure }.EncodePermissions(), PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Not Found", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Not Found", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -238,5 +241,28 @@ namespace Oqtane.Infrastructure } } } + + private void Upgrade_3_2_0(Tenant tenant, IServiceScope scope) + { + try + { + // convert folder paths to cross platform format + var siteRepository = scope.ServiceProvider.GetRequiredService(); + var folderRepository = scope.ServiceProvider.GetRequiredService(); + foreach (Site site in siteRepository.GetSites().ToList()) + { + foreach (Folder folder in folderRepository.GetFolders(site.SiteId).ToList()) + { + 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/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index d537bc9b..3637dc54 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -33,6 +33,18 @@ namespace Oqtane.Migrations.EntityBuilders protected string Schema { get; init; } + private string RewriteSqlEntityTableName(string name) + { + if (Schema == null) + { + return RewriteName(name); + } + else + { + return $"{Schema}.{RewriteName(name)}"; + } + } + private string RewriteName(string name) { return ActiveDatabase.RewriteName(name); @@ -52,12 +64,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddBooleanColumn(string name, bool nullable = false) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); } public void AddBooleanColumn(string name, bool nullable, bool defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddBooleanColumn(ColumnsBuilder table, string name, bool nullable = false) @@ -72,12 +84,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddDateTimeColumn(string name, bool nullable = false) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); } public void AddDateTimeColumn(string name, bool nullable, DateTime defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddDateTimeColumn(ColumnsBuilder table, string name, bool nullable = false) @@ -92,12 +104,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddDateTimeOffsetColumn(string name, bool nullable = false) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); } public void AddDateTimeOffsetColumn(string name, bool nullable, DateTimeOffset defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddDateTimeOffsetColumn(ColumnsBuilder table, string name, bool nullable = false) @@ -112,12 +124,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddIntegerColumn(string name, bool nullable = false) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); } public void AddIntegerColumn(string name, bool nullable, int defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddIntegerColumn(ColumnsBuilder table, string name, bool nullable = false) @@ -132,12 +144,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, schema: Schema); } public void AddMaxStringColumn(string name, bool nullable, bool unicode, string defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddMaxStringColumn(ColumnsBuilder table, string name, bool nullable = false, bool unicode = true) @@ -152,12 +164,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, schema: Schema); } public void AddStringColumn(string name, int length, bool nullable, bool unicode, string defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddStringColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true) @@ -172,12 +184,12 @@ namespace Oqtane.Migrations.EntityBuilders public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, schema: Schema); } public void AddDecimalColumn(string name, int precision, int scale, bool nullable, decimal defaultValue) { - _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue); + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue, schema: Schema); } protected OperationBuilder AddDecimalColumn(ColumnsBuilder table, string name, int precision, int scale, bool nullable = false) @@ -226,7 +238,8 @@ namespace Oqtane.Migrations.EntityBuilders name: RewriteName(indexName), table: RewriteName(EntityTableName), column: RewriteName(columnName), - unique: isUnique); + unique: isUnique, + schema: Schema); } public virtual void AddForeignKey(string foreignKeyName, string columnName, string principalTable, string principalColumn, ReferentialAction onDelete) @@ -237,7 +250,8 @@ namespace Oqtane.Migrations.EntityBuilders column: RewriteName(columnName), principalTable: RewriteName(principalTable), principalColumn: RewriteName(principalColumn), - onDelete: onDelete ); + onDelete: onDelete, + schema: Schema); } /// @@ -252,7 +266,8 @@ namespace Oqtane.Migrations.EntityBuilders name: RewriteName(indexName), table: RewriteName(EntityTableName), columns: columnNames.Select(RewriteName).ToArray(), - unique: isUnique); + unique: isUnique, + schema: Schema); } /// @@ -261,7 +276,7 @@ namespace Oqtane.Migrations.EntityBuilders /// The name of the Index to drop public virtual void DropIndex(string indexName) { - _migrationBuilder.DropIndex(RewriteName(indexName), RewriteName(EntityTableName)); + _migrationBuilder.DropIndex(RewriteName(indexName), RewriteName(EntityTableName), schema: Schema); } @@ -301,7 +316,8 @@ namespace Oqtane.Migrations.EntityBuilders column: RewriteName(foreignKey.ColumnName), principalTable: RewriteName(foreignKey.PrincipalTable), principalColumn: RewriteName(foreignKey.PrincipalColumn), - onDelete: foreignKey.OnDeleteAction); + onDelete: foreignKey.OnDeleteAction, + schema: Schema); } public void DropForeignKey(ForeignKey foreignKey) @@ -311,7 +327,7 @@ namespace Oqtane.Migrations.EntityBuilders public void DropForeignKey(string keyName) { - _migrationBuilder.DropForeignKey(RewriteName(keyName), RewriteName(EntityTableName)); + _migrationBuilder.DropForeignKey(RewriteName(keyName), RewriteName(EntityTableName), schema: Schema); } @@ -330,7 +346,7 @@ namespace Oqtane.Migrations.EntityBuilders /// public void Drop() { - _migrationBuilder.DropTable(RewriteName(EntityTableName)); + _migrationBuilder.DropTable(RewriteName(EntityTableName), schema: Schema); } @@ -338,7 +354,7 @@ namespace Oqtane.Migrations.EntityBuilders public void DeleteFromTable(string condition = "") { - var deleteSql = $"DELETE FROM {RewriteName(EntityTableName)} "; + var deleteSql = $"DELETE FROM {RewriteSqlEntityTableName(EntityTableName)} "; if(!string.IsNullOrEmpty(condition)) { deleteSql += $"WHERE {condition}"; @@ -358,7 +374,7 @@ namespace Oqtane.Migrations.EntityBuilders public void UpdateColumn(string columnName, string value, string type, string condition) { - var updateSql = $"UPDATE {RewriteName(EntityTableName)} SET {RewriteName(columnName)} = {RewriteValue(value, type)} "; + var updateSql = $"UPDATE {RewriteSqlEntityTableName(EntityTableName)} SET {RewriteName(columnName)} = {RewriteValue(value, type)} "; if (!string.IsNullOrEmpty(condition)) { updateSql += $"WHERE {condition}"; diff --git a/Oqtane.Server/Migrations/Tenant/03020001_AddSiteHomePage.cs b/Oqtane.Server/Migrations/Tenant/03020001_AddSiteHomePage.cs new file mode 100644 index 00000000..5242fa89 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/03020001_AddSiteHomePage.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.03.02.00.01")] + public class AddSiteHomePage : MultiDatabaseMigration + { + public AddSiteHomePage(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddIntegerColumn("HomePageId", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.DropColumn("HomePageId"); + } + } +} diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index fdd32df4..21527009 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -1,6 +1,9 @@ using System.Linq; using Oqtane.Documentation; using System.Collections.Generic; +using Microsoft.Extensions.Caching.Memory; +using Oqtane.Infrastructure; +using System; namespace Oqtane.Modules.HtmlText.Repository { @@ -8,15 +11,23 @@ namespace Oqtane.Modules.HtmlText.Repository public class HtmlTextRepository : IHtmlTextRepository, ITransientService { private readonly HtmlTextContext _db; + private readonly IMemoryCache _cache; + private readonly SiteState _siteState; - public HtmlTextRepository(HtmlTextContext context) + public HtmlTextRepository(HtmlTextContext context, IMemoryCache cache, SiteState siteState) { _db = context; + _cache = cache; + _siteState = siteState; } public IEnumerable GetHtmlTexts(int moduleId) { - return _db.HtmlText.Where(item => item.ModuleId == moduleId); + return _cache.GetOrCreate($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}", entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return _db.HtmlText.Where(item => item.ModuleId == moduleId).ToList(); + }); } public Models.HtmlText GetHtmlText(int htmlTextId) @@ -28,6 +39,7 @@ namespace Oqtane.Modules.HtmlText.Repository { _db.HtmlText.Add(htmlText); _db.SaveChanges(); + ClearCache(htmlText.ModuleId); return htmlText; } @@ -35,7 +47,13 @@ namespace Oqtane.Modules.HtmlText.Repository { Models.HtmlText htmlText = _db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId); if (htmlText != null) _db.HtmlText.Remove(htmlText); + ClearCache(htmlText.ModuleId); _db.SaveChanges(); } + + private void ClearCache(int moduleId) + { + _cache.Remove($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}"); + } } } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 426a82fc..886290c5 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,15 +3,15 @@ net6.0 Debug;Release - 3.1.4 + 3.2.0 Oqtane Shaun Walker .NET Foundation - Modular Application Framework for Blazor + Modular Application Framework for Blazor and MAUI .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 https://github.com/oqtane/oqtane.framework Git Oqtane 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/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 394dd296..e8de6ad5 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -27,7 +27,7 @@ { @(Html.AntiForgeryToken()) - +
@@ -46,7 +46,7 @@ { } - else + @if (Model.Runtime == "Server") { } diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 58cdeb29..77e215b5 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -8,7 +8,6 @@ using System.Reflection; using Oqtane.Repository; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -61,7 +60,7 @@ namespace Oqtane.Pages public string AntiForgeryToken = ""; public string AuthorizationToken = ""; public string Runtime = "Server"; - public RenderMode RenderMode = RenderMode.Server; + public string RenderMode = "ServerPrerendered"; public int VisitorId = -1; public string RemoteIPAddress = ""; public string HeadResources = ""; @@ -84,10 +83,10 @@ namespace Oqtane.Pages if (_configuration.GetSection("RenderMode").Exists()) { - RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), _configuration.GetSection("RenderMode").Value, true); + RenderMode = _configuration.GetSection("RenderMode").Value; } - // if framework is installed + // if framework is installed if (_configuration.IsInstalled()) { var alias = _tenantManager.GetAlias(); @@ -113,7 +112,7 @@ namespace Oqtane.Pages } var site = _sites.GetSite(alias.SiteId); - if (site != null && !site.IsDeleted) + if (site != null && !site.IsDeleted && site.Runtime != "Hybrid") { Route route = new Route(url, alias.Path); @@ -123,7 +122,7 @@ namespace Oqtane.Pages } if (!string.IsNullOrEmpty(site.RenderMode)) { - RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), site.RenderMode, true); + RenderMode = site.RenderMode; } if (site.FaviconFileId != null) { @@ -153,6 +152,10 @@ namespace Oqtane.Pages } var page = _pages.GetPage(route.PagePath, site.SiteId); + if (page == null && route.PagePath == "" && site.HomePageId != null) + { + page = _pages.GetPage(site.HomePageId.Value); + } if (page != null && !page.IsDeleted) { // set page title @@ -216,7 +219,7 @@ namespace Oqtane.Pages SetLocalizationCookie(culture); } - // set language for page + // set language for page if (!string.IsNullOrEmpty(culture)) { // localization cookie value in form of c=en|uic=en @@ -226,7 +229,7 @@ namespace Oqtane.Pages } else { - Message = "Site Is Either Disabled Or Not Configured Correctly"; + Message = "Site Is Disabled"; } } else @@ -243,6 +246,7 @@ namespace Oqtane.Pages { // get request attributes string useragent = (Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Request.Headers[HeaderNames.UserAgent] : "(none)"; + useragent = (useragent.Length > 256) ? useragent.Substring(0, 256) : useragent; string language = (Request.Headers[HeaderNames.AcceptLanguage] != StringValues.Empty) ? Request.Headers[HeaderNames.AcceptLanguage] : ""; language = (language.Contains(",")) ? language.Substring(0, language.IndexOf(",")) : language; language = (language.Contains(";")) ? language.Substring(0, language.IndexOf(";")) : language; @@ -431,7 +435,7 @@ namespace Oqtane.Pages { int count = 1; foreach (var resource in obj.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet)) - { + { resource.Level = ResourceLevel.Page; ProcessResource(resource, count++); } 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 4ac5403c..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 = "/" + 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/PermissionRepository.cs b/Oqtane.Server/Repository/PermissionRepository.cs index 1941f85f..3028488e 100644 --- a/Oqtane.Server/Repository/PermissionRepository.cs +++ b/Oqtane.Server/Repository/PermissionRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Oqtane.Extensions; using Oqtane.Models; diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index ded32453..fbb5e5da 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), @@ -293,7 +293,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Login.Index).ToModuleDefinitionName(), Title = "User Login", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Login.Index).ToModuleDefinitionName(), Title = "User Login", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -322,7 +322,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Register.Index).ToModuleDefinitionName(), Title = "User Registration", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Register.Index).ToModuleDefinitionName(), Title = "User Registration", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -352,7 +352,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Reset.Index).ToModuleDefinitionName(), Title = "Password Reset", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Reset.Index).ToModuleDefinitionName(), Title = "Password Reset", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -381,7 +381,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UserProfile.Index).ToModuleDefinitionName(), Title = "User Profile", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UserProfile.Index).ToModuleDefinitionName(), Title = "User Profile", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -408,7 +408,7 @@ namespace Oqtane.Repository }.EncodePermissions(), PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Not Found", Pane = PaneNames.Admin, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Not Found", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -437,7 +437,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Dashboard.Index).ToModuleDefinitionName(), Title = "Admin Dashboard", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Dashboard.Index).ToModuleDefinitionName(), Title = "Admin Dashboard", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -465,7 +465,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Site.Index).ToModuleDefinitionName(), Title = "Site Settings", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Site.Index).ToModuleDefinitionName(), Title = "Site Settings", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -493,7 +493,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Pages.Index).ToModuleDefinitionName(), Title = "Page Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Pages.Index).ToModuleDefinitionName(), Title = "Page Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -521,7 +521,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Users.Index).ToModuleDefinitionName(), Title = "User Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Users.Index).ToModuleDefinitionName(), Title = "User Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -549,7 +549,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Profiles.Index).ToModuleDefinitionName(), Title = "Profile Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Profiles.Index).ToModuleDefinitionName(), Title = "Profile Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -577,7 +577,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Roles.Index).ToModuleDefinitionName(), Title = "Role Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Roles.Index).ToModuleDefinitionName(), Title = "Role Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -605,7 +605,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Files.Index).ToModuleDefinitionName(), Title = "File Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Files.Index).ToModuleDefinitionName(), Title = "File Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -633,7 +633,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.RecycleBin.Index).ToModuleDefinitionName(), Title = "Recycle Bin", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.RecycleBin.Index).ToModuleDefinitionName(), Title = "Recycle Bin", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -661,7 +661,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -690,7 +690,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -720,7 +720,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Logs.Index).ToModuleDefinitionName(), Title = "Event Log", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Logs.Index).ToModuleDefinitionName(), Title = "Event Log", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -748,7 +748,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sites.Index).ToModuleDefinitionName(), Title = "Site Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sites.Index).ToModuleDefinitionName(), Title = "Site Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -776,7 +776,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.ModuleDefinitions.Index).ToModuleDefinitionName(), Title = "Module Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.ModuleDefinitions.Index).ToModuleDefinitionName(), Title = "Module Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -804,7 +804,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Themes.Index).ToModuleDefinitionName(), Title = "Theme Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Themes.Index).ToModuleDefinitionName(), Title = "Theme Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -834,7 +834,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Languages.Index).ToModuleDefinitionName(), Title = "Language Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Languages.Index).ToModuleDefinitionName(), Title = "Language Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -864,7 +864,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Jobs.Index).ToModuleDefinitionName(), Title = "Scheduled Jobs", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Jobs.Index).ToModuleDefinitionName(), Title = "Scheduled Jobs", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -892,7 +892,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sql.Index).ToModuleDefinitionName(), Title = "Sql Management", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sql.Index).ToModuleDefinitionName(), Title = "Sql Management", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -920,7 +920,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.SystemInfo.Index).ToModuleDefinitionName(), Title = "System Info", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.SystemInfo.Index).ToModuleDefinitionName(), Title = "System Info", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), @@ -948,7 +948,7 @@ namespace Oqtane.Repository { new PageTemplateModule { - ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Upgrade.Index).ToModuleDefinitionName(), Title = "System Update", Pane = PaneNames.Admin, + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Upgrade.Index).ToModuleDefinitionName(), Title = "System Update", Pane = PaneNames.Default, ModulePermissions = new List { new Permission(PermissionNames.View, RoleNames.Host, true), diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 7f72eb9c..3794f4ab 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -115,7 +115,7 @@ namespace Oqtane.Repository TypeName = themeControlType.FullName + ", " + themeControlType.Assembly.GetName().Name, Name = theme.Name + " - " + ((string.IsNullOrEmpty(themecontrolobject.Name)) ? Utilities.GetTypeNameLastSegment(themeControlType.FullName, 0) : themecontrolobject.Name), Thumbnail = themecontrolobject.Thumbnail, - Panes = (!string.IsNullOrEmpty(themecontrolobject.Panes)) ? themecontrolobject.Panes : PaneNames.Admin + Panes = themecontrolobject.Panes } ); 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.Server/Security/AutoValidateAntiforgeryTokenFilter.cs b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs index 5efe69fc..f6fb29cf 100644 --- a/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs +++ b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs @@ -56,6 +56,12 @@ namespace Oqtane.Security return false; } + // ignore antiforgery validation if client is a MAUI app + if (context.HttpContext.Request.Headers["User-Agent"] == Constants.MauiUserAgent) + { + return false; + } + // ignore antiforgery validation for GET, HEAD, TRACE, OPTIONS var method = context.HttpContext.Request.Method; if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsTrace(method) || HttpMethods.IsOptions(method)) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 4d4c2b21..309f0c09 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -23,7 +23,7 @@ namespace Oqtane { private readonly bool _useSwagger; private readonly IWebHostEnvironment _env; - private readonly string[] _supportedCultures; + private readonly string[] _installedCultures; public IConfigurationRoot Configuration { get; } @@ -35,7 +35,7 @@ namespace Oqtane .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true); Configuration = builder.Build(); - _supportedCultures = localizationManager.GetSupportedCultures(); + _installedCultures = localizationManager.GetInstalledCultures(); //add possibility to switch off swagger on production. _useSwagger = Configuration.GetSection("UseSwagger").Value != "false"; @@ -91,7 +91,7 @@ namespace Oqtane services.AddOqtaneTransientServices(); // load the external assemblies into the app domain, install services - services.AddOqtane(_supportedCultures); + services.AddOqtane(_installedCultures); services.AddOqtaneDbContext(); services.AddAntiforgery(options => diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Index.razor index d93cdb20..df7b157a 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Index.razor @@ -26,7 +26,7 @@ else - + @context.Name diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Settings.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Settings.razor index 63a13216..51ff22e6 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Settings.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].[Module]/Settings.razor @@ -5,7 +5,7 @@
- +
@@ -13,6 +13,7 @@
@code { + private string resourceType = "[Owner].[Module].Settings, [Owner].[Module].Client.Oqtane"; // for localization public override string Title => "[Module] Settings"; string _value; diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Resources/[Owner].[Module]/Index.resx b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Resources/[Owner].[Module]/Index.resx index 2c863947..721a853a 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Resources/[Owner].[Module]/Index.resx +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Resources/[Owner].[Module]/Index.resx @@ -120,12 +120,21 @@ Name - + + Add [Module] + + Edit - + Delete + + Delete [Module] + + + Are You Sure You Wish To Delete This [Module]? + No [Module]s To Display diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak index 50bb369d..530eae4a 100644 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak index 4634a97e..a2fdf20a 100644 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak index 5b690cbf..d7b0953d 100644 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak index 3714b733..9e7dbc0c 100644 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak differ diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor index 8f8f2456..bcdb50ba 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor @@ -101,8 +101,8 @@ public override List Resources => new List () { - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", Integrity = "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", CrossOrigin = "anonymous" }, + new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/css/bootstrap.min.css", Integrity = "sha512-XWTTruHZEYJsxV3W/lSXG1n3Q39YIWOstqvmFsdNEEQfHoZ6vm6E9GK2OrF6DSJSpIbRbi+Nn0WDPID9O7xB2Q==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", Integrity = "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", CrossOrigin = "anonymous" } + new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", Integrity = "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", CrossOrigin = "anonymous" } }; } diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index b62c006c..8a677952 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -389,6 +389,55 @@ Oqtane.Interop = { behavior: "smooth", block: "start", inline: "nearest" - }); + }); + } + }, + getCaretPosition: function (id) { + var element = document.getElementById(id); + return element.selectionStart; + }, + manageIndexedDBItems: async function (action, key, value) { + var idb = indexedDB.open("oqtane", 1); + + idb.onupgradeneeded = function () { + let db = idb.result; + db.createObjectStore("items"); + } + + if (action.startsWith("get")) { + let request = new Promise((resolve) => { + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readonly"); + let collection = transaction.objectStore("items"); + let result; + if (action === "get") { + result = collection.get(key); + } + if (action === "getallkeys") { + result = collection.getAllKeys(); + } + + result.onsuccess = function (e) { + resolve(result.result); + } + } + }); + + let result = await request; + + return result; + } + else { + idb.onsuccess = function () { + let transaction = idb.result.transaction("items", "readwrite"); + let collection = transaction.objectStore("items"); + if (action === "put") { + collection.put(value, key); + } + if (action === "delete") { + collection.delete(key); + } + } + } } -}}; +}; diff --git a/Oqtane.Shared/Enums/Runtime.cs b/Oqtane.Shared/Enums/Runtime.cs index 4cca8235..3758b765 100644 --- a/Oqtane.Shared/Enums/Runtime.cs +++ b/Oqtane.Shared/Enums/Runtime.cs @@ -3,6 +3,7 @@ namespace Oqtane.Shared public enum Runtime { Server, - WebAssembly + WebAssembly, + Hybrid } } diff --git a/Oqtane.Shared/Models/Alias.cs b/Oqtane.Shared/Models/Alias.cs index 70e1c886..c3697be0 100644 --- a/Oqtane.Shared/Models/Alias.cs +++ b/Oqtane.Shared/Models/Alias.cs @@ -82,9 +82,15 @@ namespace Oqtane.Models } /// - /// Site-specific settings (only available on the server via HttpContext for security reasons) + /// Protocol for the request from which the alias was resolved (ie. http or https ) /// - //[NotMapped] - //public Dictionary SiteSettings { get; set; } + [NotMapped] + public string Protocol { get; set; } + + /// + /// Base Url for static resources (note that this will only be set for remote clients) + /// + [NotMapped] + public string BaseUrl { get; set; } } } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index 472ddcbc..0e44db71 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -109,7 +109,6 @@ namespace Oqtane.Models #endregion #region IModuleControl properties - // TODO: unclear why these are IModuleControl properties - there is no such interface [NotMapped] public SecurityAccessLevel SecurityAccessLevel { get; set; } [NotMapped] diff --git a/Oqtane.Shared/Models/Route.cs b/Oqtane.Shared/Models/Route.cs index bff8f17f..9b4cc683 100644 --- a/Oqtane.Shared/Models/Route.cs +++ b/Oqtane.Shared/Models/Route.cs @@ -38,13 +38,13 @@ namespace Oqtane.Models if (pos != -1) { UrlParameters = PagePath.Substring(pos + 3); - PagePath = PagePath.Substring(1, pos); + PagePath = PagePath.Substring(0, pos); } pos = PagePath.IndexOf("/" + Constants.ModuleDelimiter + "/"); if (pos != -1) { ModuleId = PagePath.Substring(pos + 3); - PagePath = PagePath.Substring(1, pos); + PagePath = PagePath.Substring(0, pos); } if (ModuleId.Length != 0) { diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index d66f2a18..3918f2ca 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -83,6 +83,23 @@ namespace Oqtane.Models ///
public string Version { get; set; } + /// + /// The home page of the site which will be used as a fallback if no page has a path of "/" + /// + public int? HomePageId { get; set; } + + [NotMapped] + public Dictionary Settings { get; set; } + + [NotMapped] + public List Pages { get; set; } + + [NotMapped] + public List Modules { get; set; } + + [NotMapped] + public List Languages { get; set; } + #region IAuditable Properties /// @@ -101,17 +118,16 @@ namespace Oqtane.Models public string DeletedBy { get; set; } public DateTime? DeletedOn { get; set; } public bool IsDeleted { get; set; } - + #endregion - + [NotMapped] public string SiteTemplateType { get; set; } - [NotMapped] - public Dictionary Settings { get; set; } - + #region Obsolete properties [NotMapped] [Obsolete("This property is deprecated.", false)] public string DefaultLayoutType { get; set; } + #endregion } } diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 5221702d..752b8bf0 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,15 +3,15 @@ net6.0 Debug;Release - 3.1.4 + 3.2.0 Oqtane Shaun Walker .NET Foundation - Modular Application Framework for Blazor + Modular Application Framework for Blazor and MAUI .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index bda57c59..16169495 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -119,6 +119,16 @@ namespace Oqtane.Security return false; } + public static bool ContainsRole(string permissionStrings, string permissionName, string roleName) + { + return GetPermissionStrings(permissionStrings).FirstOrDefault(item => item.PermissionName == permissionName).Permissions.Split(';').Contains(roleName); + } + + public static bool ContainsUser(string permissionStrings, string permissionName, int userId) + { + return GetPermissionStrings(permissionStrings).FirstOrDefault(item => item.PermissionName == permissionName).Permissions.Split(';').Contains($"[{userId}]"); + } + public static ClaimsIdentity CreateClaimsIdentity(Alias alias, User user, List userroles) { user.Roles = ""; diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 627a57e4..aabe2ef6 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "3.1.4"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4"; + public static readonly string Version = "3.2.0"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0"; public const string PackageId = "Oqtane.Framework"; public const string UpdaterPackageId = "Oqtane.Updater"; public const string PackageRegistryUrl = "https://www.oqtane.net"; @@ -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 = "!"; @@ -89,5 +91,7 @@ namespace Oqtane.Shared public static readonly string HttpContextAliasKey = "Alias"; public static readonly string HttpContextSiteSettingsKey = "SiteSettings"; + + public static readonly string MauiUserAgent = "MAUI"; } } diff --git a/Oqtane.Shared/Shared/PaneNames.cs b/Oqtane.Shared/Shared/PaneNames.cs index 16209b8a..dc618e2e 100644 --- a/Oqtane.Shared/Shared/PaneNames.cs +++ b/Oqtane.Shared/Shared/PaneNames.cs @@ -1,5 +1,6 @@ namespace Oqtane.Shared { public class PaneNames { public const string Admin = "Admin"; + public const string Default = "Default"; } } diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 89b33341..03a6cabc 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -108,7 +108,13 @@ namespace Oqtane.Shared var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; var method = asAttachment ? "/attach" : ""; - return $"{aliasUrl}{Constants.ContentUrl}{fileId}{method}"; + 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) @@ -118,17 +124,18 @@ namespace Oqtane.Shared public static string ImageUrl(Alias alias, int fileId, int width, int height, string mode, string position, string background, int rotate, bool recreate) { - var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; + var url = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; mode = string.IsNullOrEmpty(mode) ? "crop" : mode; position = string.IsNullOrEmpty(position) ? "center" : position; background = string.IsNullOrEmpty(background) ? "000000" : background; - return $"{aliasUrl}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}/{position}/{background}/{rotate}/{recreate}"; + return $"{alias.BaseUrl}{url}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}/{position}/{background}/{rotate}/{recreate}"; } public static string TenantUrl(Alias alias, string url) { url = (!url.StartsWith("/")) ? "/" + url : url; - return (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path + url : url; + url = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path + url : url; + return $"{alias.BaseUrl}{url}"; } public static string FormatContent(string content, Alias alias, string operation) @@ -360,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("\\", "/"); @@ -438,5 +446,50 @@ namespace Oqtane.Shared { return $"[{@class.GetType()}] {message}"; } + + public static DateTime? LocalDateAndTimeAsUtc(DateTime? date, string time, TimeZoneInfo localTimeZone = null) + { + localTimeZone ??= TimeZoneInfo.Local; + if (date != null) + { + if (!string.IsNullOrEmpty(time)) + { + return TimeZoneInfo.ConvertTime(DateTime.Parse(date.Value.Date.ToShortDateString() + " " + time), localTimeZone, TimeZoneInfo.Utc); + } + return TimeZoneInfo.ConvertTime(date.Value.Date, localTimeZone, TimeZoneInfo.Utc); + } + return null; + } + + public static (DateTime? date, string time) UtcAsLocalDateAndTime(DateTime? dateTime, TimeZoneInfo timeZone = null) + { + timeZone ??= TimeZoneInfo.Local; + DateTime? localDateTime = null; + string localTime = string.Empty; + + if (dateTime.HasValue && dateTime?.Kind != DateTimeKind.Local) + { + if (dateTime?.Kind == DateTimeKind.Unspecified) + { + // Treat Unspecified as Utc not Local. This is due to EF Core, on some databases, after retrieval will have DateTimeKind as Unspecified. + // All values in database should be UTC. + // Normal .net conversion treats Unspecified as local. + // https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.converttime?view=net-6.0 + localDateTime = TimeZoneInfo.ConvertTime(new DateTime(dateTime.Value.Ticks, DateTimeKind.Utc), timeZone); + } + else + { + localDateTime = TimeZoneInfo.ConvertTime(dateTime.Value, timeZone); + } + } + + if (localDateTime != null && localDateTime.Value.TimeOfDay.TotalSeconds != 0) + { + localTime = localDateTime.Value.ToString("HH:mm"); + } + + return (localDateTime?.Date, localTime); + } + } } diff --git a/Oqtane.Test/Oqtane.Shared.Tests/UtilitiesTests.cs b/Oqtane.Test/Oqtane.Shared.Tests/UtilitiesTests.cs index 29adc5d4..0a2dc47c 100644 --- a/Oqtane.Test/Oqtane.Shared.Tests/UtilitiesTests.cs +++ b/Oqtane.Test/Oqtane.Shared.Tests/UtilitiesTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Globalization; using Oqtane.Shared; using Xunit; @@ -27,5 +29,61 @@ namespace Oqtane.Test.Oqtane.Shared.Tests // Assert Assert.Equal(expectedUrl, navigatedUrl); } + + [Theory] + [InlineData(2022, 02, 01, "21:00", "Eastern Standard Time", 2022, 2, 2, 2)] + [InlineData(2022, 02, 02, "15:00", "Eastern Standard Time", 2022, 2, 2, 20)] + [InlineData(2022, 02, 02, "", "Eastern Standard Time", 2022, 2, 2, 5)] + [InlineData(0, 0, 0, "", "Eastern Standard Time", 0, 0, 0, 0)] + public void LocalDateAndTimeAsUtcTest(int yr, int mo, int day, string timeString, string zone, int yrUtc, int moUtc, int dayUtc, int hrUtc) + { + // Arrange + DateTime? srcDate = null; + if (yr > 0) + { + srcDate = new DateTime(yr, mo, day); + } + + // Act + var dateTime = Utilities.LocalDateAndTimeAsUtc(srcDate, timeString, TimeZoneInfo.FindSystemTimeZoneById(zone)); + + // Assert + DateTime? expected = null; + if (yrUtc > 0) + { + expected = new DateTime(yrUtc, moUtc, dayUtc, hrUtc, 0, 0, DateTimeKind.Utc); + } + Assert.Equal(expected, dateTime); + } + + [Theory] + // Standard Time + [InlineData(2022, 2, 2, 2, DateTimeKind.Unspecified, "Eastern Standard Time", "2022/02/01", "21:00")] + [InlineData(2022, 2, 2, 2, DateTimeKind.Utc, "Eastern Standard Time", "2022/02/01", "21:00")] + [InlineData(2022, 2, 2, 20, DateTimeKind.Unspecified, "Eastern Standard Time", "2022/02/02", "15:00")] + [InlineData(2022, 2, 2, 20, DateTimeKind.Utc, "Eastern Standard Time", "2022/02/02", "15:00")] + [InlineData(2022, 2, 2, 5, DateTimeKind.Unspecified, "Eastern Standard Time", "2022/02/02", "")] + [InlineData(2022, 2, 2, 5, DateTimeKind.Utc, "Eastern Standard Time", "2022/02/02", "")] + // Daylight Savings Time + [InlineData(2022, 7, 2, 20, DateTimeKind.Unspecified, "Eastern Standard Time", "2022/07/02", "16:00")] + [InlineData(2022, 7, 2, 20, DateTimeKind.Utc, "Eastern Standard Time", "2022/07/02", "16:00")] + [InlineData(2022, 7, 2, 4, DateTimeKind.Unspecified, "Eastern Standard Time", "2022/07/02", "")] + [InlineData(2022, 7, 2, 4, DateTimeKind.Utc, "Eastern Standard Time", "2022/07/02", "")] + public void UtcAsLocalDateAndTimeTest(int yr, int mo, int day, int hr, DateTimeKind dateTimeKind, string zone, string expectedDate, string expectedTime) + { + // Arrange + DateTime? srcDate = null; + if (yr > 0) + { + srcDate = new DateTime(yr, mo, day, hr, 0, 0, dateTimeKind); + } + + // Act + var dateAndTime = Utilities.UtcAsLocalDateAndTime(srcDate, TimeZoneInfo.FindSystemTimeZoneById(zone)); + + // Assert + Assert.Equal(expectedDate, dateAndTime.date.Value.ToString("yyyy/MM/dd")); + Assert.Equal(expectedTime, dateAndTime.time); + } } } diff --git a/Oqtane.Test/Oqtane.Test.csproj b/Oqtane.Test/Oqtane.Test.csproj index d0e48adc..8448cfc5 100644 --- a/Oqtane.Test/Oqtane.Test.csproj +++ b/Oqtane.Test/Oqtane.Test.csproj @@ -3,15 +3,15 @@ net6.0 Debug;Release - 3.1.4 + 3.2.0 Oqtane Shaun Walker .NET Foundation - Modular Application Framework for Blazor + Modular Application Framework for Blazor and MAUI .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index da67af36..26dbc8f2 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,15 +3,15 @@ net6.0 Exe - 3.1.3 + 3.2.0 Oqtane Shaun Walker .NET Foundation - Modular Application Framework for Blazor + Modular Application Framework for Blazor and MAUI .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3 + https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.sln b/Oqtane.sln index f82fa77c..2ef03009 100644 --- a/Oqtane.sln +++ b/Oqtane.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28822.285 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{083BB22D-DF24-43A2-95E5-8F385CCB3318}" EndProject diff --git a/README.md b/README.md index 51db0b39..903be6fb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,12 @@ There is a separate [Documentation repository](https://github.com/oqtane/oqtane. This project is open source, and therefore is a work in progress... V.4.0.0 ( Q4 2022 ) -- [ ] MAUI / Blazor Hybrid support +- [ ] Migration to .NET 7 +- [ ] Folder Providers + +V.3.2.0 ( Q3 2022 ) +- [x] MAUI / Blazor Hybrid support +- [x] Upgrade to Bootstrap 5.2 [3.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3) ( June 27, 2022 ) - [x] Stabilization improvements