Merge pull request #6081 from oqtane/dev

10.1.0 Release
This commit is contained in:
Shaun Walker
2026-02-25 14:23:35 -05:00
committed by GitHub
150 changed files with 4772 additions and 1098 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>10.0.4</Version> <Version>10.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
</PropertyGroup> </PropertyGroup>

View File

@@ -12,10 +12,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.4" /> <PackageReference Include="Oqtane.Client" Version="10.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -9,6 +9,7 @@
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using Oqtane @using Oqtane
@using Oqtane.Models @using Oqtane.Models

View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Application.Template</id> <id>Oqtane.Application.Template</id>
<version>10.0.4</version> <version>10.1.0</version>
<title>Oqtane Application Template For Blazor</title> <title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@@ -22,9 +22,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.4" /> <PackageReference Include="Oqtane.Server" Version="10.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,7 +4,7 @@
@namespace [Owner].Module.[Module] @namespace [Owner].Module.[Module]
@inherits ModuleBase @inherits ModuleBase
@inject I[Module]Service [Module]Service @inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@@ -50,7 +50,7 @@
if (PageState.Action == "Edit") if (PageState.Action == "Edit")
{ {
_id = Int32.Parse(PageState.QueryString["id"]); _id = Int32.Parse(PageState.QueryString["id"]);
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
if ([Module] != null) if ([Module] != null)
{ {
_name = [Module].Name; _name = [Module].Name;
@@ -81,14 +81,14 @@
[Module] [Module] = new [Module](); [Module] [Module] = new [Module]();
[Module].ModuleId = ModuleState.ModuleId; [Module].ModuleId = ModuleState.ModuleId;
[Module].Name = _name; [Module].Name = _name;
[Module] = await [Module]Service.Add[Module]Async([Module]); [Module] = await Client[Module]Service.Add[Module]Async([Module]);
await logger.LogInformation("[Module] Added {[Module]}", [Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]);
} }
else else
{ {
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module].Name = _name; [Module].Name = _name;
await [Module]Service.Update[Module]Async([Module]); await Client[Module]Service.Update[Module]Async([Module]);
await logger.LogInformation("[Module] Updated {[Module]}", [Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]);
} }
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@@ -3,7 +3,7 @@
@namespace [Owner].Module.[Module] @namespace [Owner].Module.[Module]
@inherits ModuleBase @inherits ModuleBase
@inject I[Module]Service [Module]Service @inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@@ -52,7 +52,7 @@ else
{ {
try try
{ {
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -65,9 +65,9 @@ else
{ {
try try
{ {
await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -7,22 +7,10 @@ using Oqtane.Shared;
namespace [Owner].Module.[Module].Services namespace [Owner].Module.[Module].Services
{ {
public interface I[Module]Service
public class Client[Module]Service : ServiceBase, I[Module]Service
{ {
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId); public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
public class [Module]Service : ServiceBase, I[Module]Service
{
public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("[Module]"); private string Apiurl => CreateApiUrl("[Module]");

View File

@@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup
{ {
if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) if (!services.Any(s => s.ServiceType == typeof(I[Module]Service)))
{ {
services.AddScoped<I[Module]Service, [Module]Service>(); services.AddScoped<I[Module]Service, Client[Module]Service>();
} }
} }
} }

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
}

View File

@@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="10.0.4" /> <PackageReference Include="Oqtane.Shared" Version="10.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -57,6 +57,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ICookieConsentService, CookieConsentService>(); services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>(); services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>(); services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<ISiteGroupService, SiteGroupService>();
services.AddScoped<ISiteGroupMemberService, SiteGroupMemberService>();
services.AddScoped<ISiteTaskService, SiteTaskService>();
services.AddScoped<IOutputCacheService, OutputCacheService>(); services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers // providers

View File

@@ -0,0 +1,117 @@
@namespace Oqtane.Modules.Admin.GlobalReplace
@using System.Text.Json
@inherits ModuleBase
@inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="find" HelpText="Specify the content which needs to be replaced" ResourceKey="Find">Find What: </Label>
<div class="col-sm-9">
<input id="find" class="form-control" @bind="@_find" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="replace" HelpText="Specify the replacement content" ResourceKey="Replace">Replace With: </Label>
<div class="col-sm-9">
<input id="replace" class="form-control" @bind="@_replace" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="casesensitive" HelpText="Specify if the replacement operation should be case sensitive" ResourceKey="CaseSensitive">Match Case? </Label>
<div class="col-sm-9">
<select id="casesensitive" class="form-select" @bind="@_caseSensitive">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="site" HelpText="Specify if site properties should be updated (ie. name, headcontent, bodycontent)" ResourceKey="Site">Site Properties? </Label>
<div class="col-sm-9">
<select id="site" class="form-select" @bind="@_site">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pages" HelpText="Specify if page properties should be updated (ie. name, title, headcontent, bodycontent)" ResourceKey="Pages">Page Properties? </Label>
<div class="col-sm-9">
<select id="pages" class="form-select" @bind="@_pages">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="modules" HelpText="Specify if module properties should be updated (ie. title, header, footer)" ResourceKey="Modules">Module Properties? </Label>
<div class="col-sm-9">
<select id="modules" class="form-select" @bind="@_modules">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Specify if module content should be updated" ResourceKey="Content">Module Content? </Label>
<div class="col-sm-9">
<select id="content" class="form-select" @bind="@_content">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br /><br />
<ActionDialog Header="Global Replace" Message="This Operation is Permanent. Are You Sure You Wish To Proceed?" Action="Replace" Class="btn btn-primary" OnClick="@(async () => await Save())" ResourceKey="GlobalReplace" />
<br /><br />
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Global Replace";
private string _find;
private string _replace;
private string _caseSensitive = "True";
private string _site = "True";
private string _pages = "True";
private string _modules = "True";
private string _content = "True";
private async Task Save()
{
try
{
if (!string.IsNullOrEmpty(_find) && !string.IsNullOrEmpty(_replace))
{
var replace = new GlobalReplace
{
Find = _find,
Replace = _replace,
CaseSensitive = bool.Parse(_caseSensitive),
Site = bool.Parse(_site),
Pages = bool.Parse(_pages),
Modules = bool.Parse(_modules),
Content = bool.Parse(_content)
};
var siteTask = new SiteTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace));
await SiteTaskService.AddSiteTaskAsync(siteTask);
AddModuleMessage(Localizer["Success.Save"], MessageType.Success);
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Global Replace Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}

View File

@@ -46,7 +46,8 @@
</div> </div>
</div> </div>
</form> </form>
<Section Name="Information" ResourceKey="Information"> <br />
<Section Name="Information" ResourceKey="Information">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label> <Label Class="col-sm-3" For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
@@ -97,7 +98,13 @@
} }
</div> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div>
</Section> </Section>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
@@ -231,6 +238,7 @@
private string _url = ""; private string _url = "";
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@@ -266,6 +274,7 @@
_url = moduleDefinition.Url; _url = moduleDefinition.Url;
_contact = moduleDefinition.Contact; _contact = moduleDefinition.Contact;
_license = moduleDefinition.License; _license = moduleDefinition.License;
_fingerprint = moduleDefinition.Fingerprint;
_permissions = moduleDefinition.PermissionList; _permissions = moduleDefinition.PermissionList;
_createdby = moduleDefinition.CreatedBy; _createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn; _createdon = moduleDefinition.CreatedOn;

View File

@@ -73,27 +73,29 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label> <Label Class="col-sm-3" For="page" HelpText="The page that the module is located on. Please note that shared modules cannot be moved to other pages." ResourceKey="Page">Page: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required> @if (PageState.Page.UserId != null || _isShared)
@if (PageState.Page.UserId != null) {
{ <select id="page" class="form-select" @bind="@_pageId" required disabled>
<option value="@PageState.Page.PageId">@(PageState.Page.Name)</option> <option value="@PageState.Page.PageId">@(PageState.Page.Name)</option>
} </select>
else }
else
{
<select id="page" class="form-select" @bind="@_pageId" required>
@if (_pages != null)
{ {
if (_pages != null) foreach (Page p in _pages)
{ {
foreach (Page p in _pages) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList)) <option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
} }
} }
} }
</select> </select>
}
</div> </div>
</div> </div>
</div> </div>
@@ -161,6 +163,7 @@
private string _pane; private string _pane;
private string _containerType; private string _containerType;
private string _allPages = "false"; private string _allPages = "false";
private bool _isShared = false;
private string _header = ""; private string _header = "";
private string _footer = ""; private string _footer = "";
private string _permissionNames = ""; private string _permissionNames = "";
@@ -207,6 +210,7 @@
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate); _expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString(); _allPages = pagemodule.Module.AllPages.ToString();
_isShared = pagemodule.Module.IsShared;
createdby = pagemodule.Module.CreatedBy; createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn; createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy; modifiedby = pagemodule.Module.ModifiedBy;
@@ -276,15 +280,39 @@
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
if (!string.IsNullOrEmpty(_title)) if (!string.IsNullOrEmpty(_title))
{ {
if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate))
{ {
AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning);
return; return;
} }
// update module settings first
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
// update container settings
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
// update page module
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
var pageId = pagemodule.PageId; // preserve
var pane = pagemodule.Pane; // preserve
pagemodule.PageId = int.Parse(_pageId); pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title; pagemodule.Title = _title;
pagemodule.Pane = _pane; pagemodule.Pane = _pane;
@@ -302,33 +330,21 @@
pagemodule.Header = _header; pagemodule.Header = _header;
pagemodule.Footer = _footer; pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
// update page module order if page or pane changed
if (pageId != pagemodule.PageId || pane != pagemodule.Pane)
{
await PageModuleService.UpdatePageModuleOrderAsync(pageId, pane); // old page/pane
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); // new page/pane
}
// update module
var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId); var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages); module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId; module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList(); module.PermissionList = _permissionGrid.GetPermissionList();
await ModuleService.UpdateModuleAsync(module); await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
NavigationManager.NavigateTo(PageState.ReturnUrl); NavigationManager.NavigateTo(PageState.ReturnUrl);
} }
else else

View File

@@ -46,7 +46,7 @@
<Label Class="col-sm-3" For="move" HelpText="Select the location where you would like the page to be moved in relation to other pages" ResourceKey="Move">Move: </Label> <Label Class="col-sm-3" For="move" HelpText="Select the location where you would like the page to be moved in relation to other pages" ResourceKey="Move">Move: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="move" class="form-select" @bind="@_insert" required> <select id="move" class="form-select" @bind="@_insert" required>
@if (_parentid == _currentparentid) @if (_parentid == _currentparentid && !_copy)
{ {
<option value="=">&lt;@Localizer["ThisLocation.Keep"]&gt;</option> <option value="=">&lt;@Localizer["ThisLocation.Keep"]&gt;</option>
} }
@@ -225,7 +225,10 @@
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /> <br />
<br /> <br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo> @if (!_copy)
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
</TabPanel> </TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions"> <TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container"> <div class="container">
@@ -241,36 +244,40 @@
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
</div> </div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div> </div>
</TabPanel> </TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules"> @if (!_copy)
<Pager Items="_pageModules"> {
<Header> <TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th> <th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th> <th>@Localizer["ModuleDefinition"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td> <td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td> <td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td> <td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td> <td>@context.ModuleDefinition?.Name</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel> </TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
}
} }
</TabStrip> </TabStrip>
} }
@@ -349,6 +356,7 @@
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages; private List<Page> _pages;
private int _pageId; private int _pageId;
private bool _copy = false;
private string _name; private string _name;
private string _currentparentid; private string _currentparentid;
private string _parentid = "-1"; private string _parentid = "-1";
@@ -394,6 +402,10 @@
{ {
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pageId = Int32.Parse(PageState.QueryString["id"]); _pageId = Int32.Parse(PageState.QueryString["id"]);
if (PageState.QueryString.ContainsKey("copy"))
{
_copy = bool.Parse(PageState.QueryString["copy"]);
}
_page = await PageService.GetPageAsync(_pageId); _page = await PageService.GetPageAsync(_pageId);
_icons = await SystemService.GetIconsAsync(); _icons = await SystemService.GetIconsAsync();
_iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName); _iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName);
@@ -413,7 +425,7 @@
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture))))
{ {
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) if ((p.PageId != _pageId || _copy) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_children.Add(p); _children.Add(p);
} }
@@ -440,6 +452,12 @@
_expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate); _expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate);
_ispersonalizable = _page.IsPersonalizable.ToString(); _ispersonalizable = _page.IsPersonalizable.ToString();
if (_copy)
{
_insert = ">";
_childid = _page.PageId;
}
// appearance // appearance
_title = _page.Title; _title = _page.Title;
_themetype = _page.ThemeType; _themetype = _page.ThemeType;
@@ -470,6 +488,19 @@
// permissions // permissions
_permissions = _page.PermissionList; _permissions = _page.PermissionList;
_updatemodulepermissions = "True"; _updatemodulepermissions = "True";
if (_copy)
{
_permissions = _page.PermissionList.Select(item => new Permission
{
SiteId = item.SiteId,
EntityName = item.EntityName,
EntityId = -1,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
}).ToList();
}
// page modules // page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@@ -484,6 +515,13 @@
_deletedon = _page.DeletedOn; _deletedon = _page.DeletedOn;
ThemeSettings(); ThemeSettings();
if (_copy)
{
_name = "";
_path = "";
}
_initialized = true; _initialized = true;
} }
else else
@@ -554,7 +592,7 @@
builder.OpenComponent(0, _themeSettingsType); builder.OpenComponent(0, _themeSettingsType);
builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary); builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary);
builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
@@ -576,10 +614,18 @@
await ScrollToPageTop(); await ScrollToPageTop();
return; return;
} }
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{ {
string currentPath = _page.Path; string currentPath = _page.Path;
if (_copy)
{
_page = new Page();
_page.SiteId = PageState.Site.SiteId;
currentPath = "";
}
_page.Name = _name; _page.Name = _name;
if (_parentid == "-1") if (_parentid == "-1")
@@ -635,6 +681,13 @@
return; return;
} }
// update theme settings
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
// default page properties
if (_insert != "=") if (_insert != "=")
{ {
Page child; Page child;
@@ -688,7 +741,21 @@
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions); _page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
} }
_page = await PageService.UpdatePageAsync(_page); if (_copy)
{
// create page
_page = await PageService.AddPageAsync(_page);
await PageService.CopyPageAsync(_pageId, _page.PageId, bool.Parse(_updatemodulepermissions));
await logger.LogInformation("Page Added {Page}", _page);
}
else
{
// update page
_page = await PageService.UpdatePageAsync(_page);
await logger.LogInformation("Page Saved {Page}", _page);
}
// update page order
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId); await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId);
if (_currentparentid == string.Empty) if (_currentparentid == string.Empty)
{ {
@@ -699,12 +766,6 @@
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid)); await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid));
} }
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
await logger.LogInformation("Page Saved {Page}", _page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{ {
NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload

View File

@@ -3,10 +3,11 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUserService UserService @inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService @inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized) @if (_initialized)
{ {
@@ -71,6 +72,18 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button> <button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@@ -95,6 +108,8 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private List<Models.TimeZone> _timezones; private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private string _passwordrequirements; private string _passwordrequirements;
private string _username = string.Empty; private string _username = string.Empty;
private ElementReference form; private ElementReference form;
@@ -106,6 +121,7 @@
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private bool _userCreated = false; private bool _userCreated = false;
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
@@ -113,10 +129,13 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId; _timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_initialized = true; _initialized = true;
} }
@@ -147,6 +166,7 @@
Email = _email, Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname), DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid, TimeZoneId = _timezoneid,
CultureCode = _culturecode,
PhotoFileId = null PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@@ -11,12 +11,15 @@
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService @inject ITimeZoneService TimeZoneService
@inject ILocalizationService LocalizationService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IJobService JobService @inject IJobService JobService
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService @inject IOutputCacheService CacheService
@inject ISiteGroupService SiteGroupService
@inject ISiteGroupMemberService SiteGroupMemberService
@if (_initialized) @if (_initialized)
{ {
@@ -29,22 +32,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label> <Label Class="col-sm-3" For="timezone" HelpText="The default time zone for the site" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (Page page in _pages)
{
if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid"> <select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option> <option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@@ -55,6 +43,18 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The default language of the site's content" ResourceKey="Culture">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var culture in _cultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -333,57 +333,6 @@
</Section> </Section>
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"])</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting"> <Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -409,7 +358,7 @@
</div> </div>
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The render mode for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label> <Label Class="col-sm-3" For="runtime" HelpText="The hosting model for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required> <select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option> <option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
@@ -438,6 +387,180 @@
</div> </div>
</div> </div>
</Section> </Section>
<Section Name="Aliases" Heading="Urls" ResourceKey="Aliases">
<div class="container">
@if (!_addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="aliases" class="form-select" value="@_aliasid" @onchange="(e => AliasChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var alias in _aliases)
{
<option value="@alias.AliasId">@alias.Name @((alias.IsDefault) ? "(" + Localizer["Default"] + ")" : "")</option>
}
</select>
@if (!_addAlias)
{
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_aliasid != -1 || _addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliasname" HelpText="A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="AliasName">Url: </Label>
<div class="col-sm-9">
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default? </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if (_aliasid != -1 || _addAlias)
{
<button type="button" class="btn btn-success me-2" @onclick="SaveAlias">@SharedLocalizer["Save"]</button>
}
@if (_aliasid != -1 && !_addAlias)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias())" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", _aliasname])" />
}
@if (_addAlias)
{
<button type="button" class="btn btn-secondary" @onclick="CancelAlias">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="SiteGroupMembers" Heading="Site Groups" ResourceKey="SiteGroupMembers">
<div class="container">
@if (!_addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="group" HelpText="The site groups in this tenant (database)" ResourceKey="SiteGroupMembers">Group: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="group" class="form-select" value="@_siteGroupId" @onchange="(e => SiteGroupChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var siteGroup in _siteGroups)
{
<option value="@siteGroup.SiteGroupId">@siteGroup.Name</option>
}
</select>
@if (!_addSiteGroup)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroup">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 || _addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="groupname" HelpText="Name of the site group" ResourceKey="GroupName">Name: </Label>
<div class="col-sm-9">
<input id="groupname" class="form-control" @bind="@_groupName" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="grouptype" HelpText="Defines the specific behavior of the site group" ResourceKey="GroupType">Type: </Label>
<div class="col-sm-9">
<select id="grouptype" class="form-select" @bind="@_groupType">
<option value="@SiteGroupTypes.Synchronization">@Localizer[@SiteGroupTypes.Synchronization]</option>
<option value="@SiteGroupTypes.ChangeDetection">@Localizer[@SiteGroupTypes.ChangeDetection]</option>
<option value="@SiteGroupTypes.Localization">@Localizer[SiteGroupTypes.Localization]</option>
</select>
</div>
</div>
}
@if (_siteGroupId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="site" HelpText="The sites which are members of this site group" ResourceKey="Site">Members: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="site" class="form-select" value="@_siteId" @onchange="(e => SiteChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var site in _sites)
{
<option value="@site.SiteId">@site.Name</option>
}
</select>
@if (!_addSiteGroupMember)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Add"]</button>
}
else
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Select"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 && _siteId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="primary" HelpText="Indicates if the selected member is the primary site of the site group" ResourceKey="Primary">Primary? </Label>
<div class="col-sm-9">
<select id="primary" class="form-select" @bind="@_primary">
<option value="False">@SharedLocalizer["No"]</option>
<option value="True">@SharedLocalizer["Yes"]</option>
</select>
</div>
</div>
@if (_primary == "False" && !string.IsNullOrEmpty(_synchronized) && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronized" HelpText="The date/time when the site was last synchronized" ResourceKey="Synchronized">Synchronized: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="synchronized" class="form-control" @bind="@_synchronized" disabled />
@if (!string.IsNullOrEmpty(_synchronized))
{
<button type="button" class="btn btn-primary" @onclick="ResetSiteGroupMember">@SharedLocalizer["Reset"]</button>
}
</div>
</div>
</div>
}
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if ((_siteGroupId != -1 || _addSiteGroup))
{
<button type="button" class="btn btn-success me-2" @onclick="SaveSiteGroupMember">@SharedLocalizer["Save"]</button>
}
@if (_siteGroupId != -1 && !_addSiteGroup && _siteId != -1 && !_addSiteGroupMember)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteSiteGroupMember())" ResourceKey="DeleteSiteGroupMember" Class="btn btn-danger" Header="Delete Site Group" Message="@Localizer["Confirm.SiteGroupMember.Delete"]" />
}
@if (_addSiteGroup)
{
<button type="button" class="btn btn-secondary" @onclick="CancelSiteGroupMember">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation"> <Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -478,10 +601,11 @@
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages; private List<Page> _pages;
private List<Models.TimeZone> _timezones; private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Culture> _cultures;
private string _name = string.Empty; private string _name = string.Empty;
private string _homepageid = "-";
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted; private string _isdeleted;
private string _sitemap = ""; private string _sitemap = "";
private string _siteguid = ""; private string _siteguid = "";
@@ -527,17 +651,29 @@
private int _pwasplashiconfileid = -1; private int _pwasplashiconfileid = -1;
private FileManager _pwasplashiconfilemanager; private FileManager _pwasplashiconfilemanager;
private List<Alias> _aliases;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private string _rendermode = RenderModes.Interactive; private string _rendermode = RenderModes.Interactive;
private string _enhancednavigation = "True"; private string _enhancednavigation = "True";
private string _runtime = Runtimes.Server; private string _runtime = Runtimes.Server;
private string _prerender = "True"; private string _prerender = "True";
private string _hybrid = "False"; private string _hybrid = "False";
private List<Alias> _aliases;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private bool _addAlias = false;
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private List<Site> _sites = new List<Site>();
private int _siteGroupId = -1;
private int _siteId;
private string _groupName = string.Empty;
private string _groupType = SiteGroupTypes.Synchronization;
private string _primary = "True";
private string _synchronized = string.Empty;
private bool _addSiteGroup = false;
private bool _addSiteGroupMember = false;
private string _tenant = string.Empty; private string _tenant = string.Empty;
private string _database = string.Empty; private string _database = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
@@ -564,16 +700,14 @@
if (site != null) if (site != null)
{ {
_timezones = TimeZoneService.GetTimeZones(); _timezones = TimeZoneService.GetTimeZones();
_cultures = await LocalizationService.GetNeutralCulturesAsync();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name; _name = site.Name;
_timezoneid = site.TimeZoneId; _timezoneid = site.TimeZoneId;
if (site.HomePageId != null) _culturecode = site.CultureCode;
{
_homepageid = site.HomePageId.Value.ToString();
}
_isdeleted = site.IsDeleted.ToString(); _isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml"; _sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml";
_siteguid = site.SiteGuid; _siteguid = site.SiteGuid;
@@ -652,7 +786,7 @@
} }
// aliases // aliases
await GetAliases(); await LoadAliases();
// hosting model // hosting model
_rendermode = site.RenderMode; _rendermode = site.RenderMode;
@@ -676,6 +810,12 @@
} }
} }
// site groups
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await LoadSiteGroups();
}
// audit // audit
_createdby = site.CreatedBy; _createdby = site.CreatedBy;
_createdon = site.CreatedOn; _createdon = site.CreatedOn;
@@ -743,7 +883,7 @@
{ {
site.Name = _name; site.Name = _name;
site.TimeZoneId = _timezoneid; site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); site.CultureCode = _culturecode;
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
// appearance // appearance
@@ -992,95 +1132,100 @@
} }
} }
private async Task GetAliases() private async Task LoadAliases()
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) _aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList();
_aliasid = -1;
_addAlias = false;
}
private async void AliasChanged(ChangeEventArgs e)
{
_aliasid = int.Parse(e.Value.ToString());
if (_aliasid != -1)
{ {
_aliases = await AliasService.GetAliasesAsync(); var alias = _aliases.FirstOrDefault(item => item.AliasId == _aliasid);
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); if (alias != null)
{
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
}
} }
else
{
_aliasname = "";
_defaultalias = "False";
}
StateHasChanged();
} }
private void AddAlias() private void AddAlias()
{ {
_aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false }); _aliasid = -1;
_aliasid = 0;
_aliasname = ""; _aliasname = "";
_defaultalias = "False"; _defaultalias = "False";
_addAlias = true;
StateHasChanged(); StateHasChanged();
} }
private void EditAlias(Alias alias) private async Task DeleteAlias()
{ {
_aliasid = alias.AliasId; await AliasService.DeleteAliasAsync(_aliasid);
_aliasname = alias.Name; await LoadAliases();
_defaultalias = alias.IsDefault.ToString();
StateHasChanged(); StateHasChanged();
} }
private async Task DeleteAlias(Alias alias)
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
await GetAliases();
StateHasChanged();
}
}
private async Task SaveAlias() private async Task SaveAlias()
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (!string.IsNullOrEmpty(_aliasname))
{ {
if (!string.IsNullOrEmpty(_aliasname)) var aliases = await AliasService.GetAliasesAsync();
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (protocolIndex != -1)
{ {
var aliases = await AliasService.GetAliasesAsync(); _aliasname = _aliasname.Substring(protocolIndex + 3);
}
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
if (protocolIndex != -1)
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{
if (_aliasid == 0)
{ {
_aliasname = _aliasname.Substring(protocolIndex + 3); alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
} }
else
var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{ {
if (_aliasid == 0) alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{ {
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; alias.Name = _aliasname;
await AliasService.AddAliasAsync(alias); alias.IsDefault = bool.Parse(_defaultalias);
} await AliasService.UpdateAliasAsync(alias);
else
{
alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
} }
}
await GetAliases(); await LoadAliases();
_aliasid = -1; _aliasid = -1;
_aliasname = ""; _aliasname = "";
StateHasChanged(); StateHasChanged();
} }
else // Duplicate alias else // Duplicate alias
{ {
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
await ScrollToPageTop(); await ScrollToPageTop();
}
} }
} }
} }
private async Task CancelAlias() private async Task CancelAlias()
{ {
await GetAliases(); await LoadAliases();
_aliasid = -1; _aliasid = -1;
_aliasname = ""; _aliasname = "";
StateHasChanged(); StateHasChanged();
@@ -1090,4 +1235,202 @@
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag); await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success); AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
} }
private async Task LoadSiteGroups()
{
_siteGroups = await SiteGroupService.GetSiteGroupsAsync();
_siteGroupId = -1;
_addSiteGroup = false;
StateHasChanged();
}
private async Task SiteGroupChanged(ChangeEventArgs e)
{
_siteGroupId = int.Parse(e.Value.ToString());
if (_siteGroupId != -1)
{
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (group != null)
{
_groupName = group.Name;
_groupType = group.Type;
_siteId = -1;
_primary = "False";
_addSiteGroupMember = false;
await LoadSites();
}
}
StateHasChanged();
}
private async Task LoadSites()
{
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
_sites = await SiteService.GetSitesAsync();
if (_addSiteGroupMember)
{
// include sites which are not members
_sites = _sites.ExceptBy(siteGroupMembers.Select(item => item.SiteId), item => item.SiteId).ToList();
}
else
{
// include sites which are members
_sites = _sites.Where(item => siteGroupMembers.Any(item2 => item2.SiteId == item.SiteId)).ToList();
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
foreach (var site in _sites)
{
if (group.PrimarySiteId == site.SiteId)
{
site.Name += $" ({Localizer["Primary"]})";
}
else
{
site.Name += $" ({Localizer["Secondary"]})";
}
}
var siteGroupMember = siteGroupMembers.FirstOrDefault(item => item.SiteId == _siteId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
}
}
private async Task SiteChanged(ChangeEventArgs e)
{
_siteId = int.Parse(e.Value.ToString());
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
StateHasChanged();
}
private async Task AddSiteGroup()
{
_groupName = "";
_siteId = PageState.Site.SiteId;
_primary = "True";
_synchronized = "";
_addSiteGroup = true;
}
private async Task AddSiteGroupMember()
{
_addSiteGroupMember = !_addSiteGroupMember;
_siteId = -1;
await LoadSites();
}
private async Task SaveSiteGroupMember()
{
if (string.IsNullOrEmpty(_groupName))
{
AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning);
await ScrollToPageTop();
return;
}
SiteGroup siteGroup = null;
if (_siteGroupId == -1)
{
siteGroup = new SiteGroup
{
Name = _groupName,
Type = _groupType,
PrimarySiteId = _siteId,
Synchronize = false
};
siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup);
}
else
{
siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (siteGroup != null)
{
siteGroup.Name = _groupName;
siteGroup.Type = _groupType;
siteGroup.PrimarySiteId = (_primary == "True") ? _siteId : siteGroup.PrimarySiteId;
siteGroup = await SiteGroupService.UpdateSiteGroupAsync(siteGroup);
}
else
{
siteGroup = null;
}
}
if (siteGroup != null)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId);
if (siteGroupMember == null)
{
siteGroupMember = new SiteGroupMember
{
SiteGroupId = siteGroup.SiteGroupId,
SiteId = _siteId
};
await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember);
}
else
{
siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn;
await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember);
}
}
if (siteGroup.Type == SiteGroupTypes.Synchronization)
{
// enable synchronization job if it is not enabled already
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
await LoadSiteGroups();
}
}
private async Task CancelSiteGroupMember()
{
_groupName = "";
await LoadSiteGroups();
}
private async Task DeleteSiteGroupMember()
{
if (_siteGroupId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(PageState.Site.SiteId, _siteGroupId);
if (siteGroupMember != null)
{
await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupId);
}
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
if (!siteGroupMembers.Any())
{
await SiteGroupService.DeleteSiteGroupAsync(_siteGroupId);
}
await LoadSiteGroups();
}
}
private async Task ResetSiteGroupMember()
{
_synchronized = "";
}
} }

View File

@@ -6,7 +6,6 @@
@inject ITenantService TenantService @inject ITenantService TenantService
@inject IAliasService AliasService @inject IAliasService AliasService
@inject ISiteService SiteService @inject ISiteService SiteService
@inject IThemeService ThemeService
@inject ISiteTemplateService SiteTemplateService @inject ISiteTemplateService SiteTemplateService
@inject IUserService UserService @inject IUserService UserService
@inject IInstallationService InstallationService @inject IInstallationService InstallationService
@@ -29,33 +28,9 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label> <Label Class="col-sm-3" For="alias" HelpText="The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder)." ResourceKey="Aliases">Url: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea> <input id="alias" class="form-control" @bind="@_urls" required></input>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9">
<select id="defaultContainer" class="form-select" @bind="@_containertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -70,26 +45,6 @@ else
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="rendermode" HelpText="The default render mode for the site" ResourceKey="Rendermode">Render Mode: </Label>
<div class="col-sm-9">
<select id="rendermode" class="form-select" @bind="@_rendermode" required>
<option value="@RenderModes.Interactive">@(SharedLocalizer["RenderMode" + @RenderModes.Interactive])</option>
<option value="@RenderModes.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option>
<option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The render mode for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
<option value="@Runtimes.WebAssembly">@(SharedLocalizer["Runtime" + @Runtimes.WebAssembly])</option>
<option value="@Runtimes.Auto">@(SharedLocalizer["Runtime" + @Runtimes.Auto])</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the database for the site" ResourceKey="Tenant">Database: </Label> <Label Class="col-sm-3" For="tenant" HelpText="Select the database for the site" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -186,9 +141,6 @@ else
private bool _showConnectionString = false; private bool _showConnectionString = false;
private string _connectionString = string.Empty; private string _connectionString = string.Empty;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates; private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants; private List<Tenant> _tenants;
private string _tenantid = "-"; private string _tenantid = "-";
@@ -200,11 +152,7 @@ else
private string _name = string.Empty; private string _name = string.Empty;
private string _urls = string.Empty; private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _sitetemplatetype = "-"; private string _sitetemplatetype = "-";
private string _rendermode = RenderModes.Static;
private string _runtime = Runtimes.Server;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -215,19 +163,11 @@ else
{ {
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
} }
_urls = PageState.Alias.Name; _urls = PageState.Alias.Name + "/sitename";
_themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{
_themetype = Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); _siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
if (_siteTemplates.Any(item => item.TypeName == Constants.DefaultSiteTemplate)) if (_siteTemplates.Any(item => item.TypeName == Constants.EmptySiteTemplate))
{ {
_sitetemplatetype = Constants.DefaultSiteTemplate; _sitetemplatetype = Constants.EmptySiteTemplate;
} }
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
@@ -281,37 +221,13 @@ else
StateHasChanged(); StateHasChanged();
} }
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
else
{
_containers = new List<ThemeControl>();
_containertype = "-";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error);
}
}
private async Task SaveSite() private async Task SaveSite()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-") if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _sitetemplatetype != "-")
{ {
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); _urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>(); var duplicates = new List<string>();
@@ -399,12 +315,12 @@ else
{ {
config.SiteName = _name; config.SiteName = _name;
config.Aliases = _urls; config.Aliases = _urls;
config.DefaultTheme = _themetype; config.DefaultTheme = Constants.DefaultTheme;
config.DefaultContainer = _containertype; config.DefaultContainer = Constants.DefaultContainer;
config.DefaultAdminContainer = ""; config.DefaultAdminContainer = "";
config.SiteTemplate = _sitetemplatetype; config.SiteTemplate = _sitetemplatetype;
config.RenderMode = _rendermode; config.RenderMode = RenderModes.Static;
config.Runtime = _runtime; config.Runtime = Runtimes.Server;
config.Register = false; config.Register = false;
ShowProgressIndicator(); ShowProgressIndicator();

View File

@@ -30,6 +30,7 @@
</div> </div>
</div> </div>
</form> </form>
<br />
<Section Name="Information" ResourceKey="Information" Heading="Information"> <Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -81,6 +82,12 @@
} }
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div> </div>
</Section> </Section>
<br /> <br />
@@ -117,6 +124,7 @@
private string _url = ""; private string _url = "";
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@@ -143,6 +151,7 @@
_url = theme.Url; _url = theme.Url;
_contact = theme.Contact; _contact = theme.Contact;
_license = theme.License; _license = theme.License;
_fingerprint = theme.Fingerprint;
_permissions = theme.PermissionList; _permissions = theme.PermissionList;
_createdby = theme.CreatedBy; _createdby = theme.CreatedBy;
_createdon = theme.CreatedOn; _createdon = theme.CreatedOn;

View File

@@ -10,6 +10,7 @@
@inject IFileService FileService @inject IFileService FileService
@inject IFolderService FolderService @inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService @inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IJSRuntime jsRuntime @inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@@ -58,6 +59,18 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label> <Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -448,6 +461,9 @@
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private bool _initialized = false; private bool _initialized = false;
private bool _allowtwofactor = false; private bool _allowtwofactor = false;
private bool _allowpasskeys = false; private bool _allowpasskeys = false;
@@ -458,8 +474,8 @@
private string _displayname = string.Empty; private string _displayname = string.Empty;
private FileManager _filemanager; private FileManager _filemanager;
private int _folderid = -1; private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private int _photofileid = -1; private int _photofileid = -1;
private File _photo = null; private File _photo = null;
private string _imagefiles = string.Empty; private string _imagefiles = string.Empty;
@@ -493,12 +509,15 @@
if (PageState.User != null) if (PageState.User != null)
{ {
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
// identity section // identity section
_username = PageState.User.Username; _username = PageState.User.Username;
_email = PageState.User.Email; _email = PageState.User.Email;
_displayname = PageState.User.DisplayName; _displayname = PageState.User.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId; _timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
{ {
@@ -572,6 +591,7 @@
user.Email = _email; user.Email = _email;
user.DisplayName = (_displayname == string.Empty ? _username : _displayname); user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.TimeZoneId = _timezoneid; user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = _filemanager.GetFileId(); user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {

View File

@@ -6,6 +6,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService @inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label> <Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -129,12 +142,15 @@
@code { @code {
private List<Models.TimeZone> _timezones; private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private bool _initialized = false; private bool _initialized = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _confirmed = "True"; private string _confirmed = "True";
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _notify = "True"; private string _notify = "True";
private List<Profile> _profiles; private List<Profile> _profiles;
private Dictionary<string, string> _settings; private Dictionary<string, string> _settings;
@@ -147,6 +163,11 @@
try try
{ {
_timezones = TimeZoneService.GetTimeZones(); _timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles) foreach (var profile in _profiles)
{ {
@@ -157,8 +178,9 @@
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}")); profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
} }
} }
_settings = new Dictionary<string, string>(); _settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@@ -194,6 +216,7 @@
user.EmailConfirmed = bool.Parse(_confirmed); user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid; user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = null; user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify); user.SuppressNotification = !bool.Parse(_notify);

View File

@@ -7,6 +7,7 @@
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject ITimeZoneService TimeZoneService @inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -211,7 +224,7 @@
{ {
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button> <button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
} }
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _candelete)
{ {
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
} }
@@ -224,17 +237,21 @@
private bool _allowpasskeys = false; private bool _allowpasskeys = false;
private bool _allowexternallogin = false; private bool _allowexternallogin = false;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private int _userid; private int _userid;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _confirmed = string.Empty; private string _confirmed = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted; private string _isdeleted;
private string _lastlogin; private string _lastlogin;
private string _lastipaddress; private string _lastipaddress;
private bool _ishost = false; private bool _ishost = false;
private bool _candelete = false;
private string _passwordrequirements; private string _passwordrequirements;
private string _password = string.Empty; private string _password = string.Empty;
@@ -270,13 +287,17 @@
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_username = user.Username; _username = user.Username;
_email = user.Email; _email = user.Email;
_confirmed = user.EmailConfirmed.ToString(); _confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName; _displayname = user.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId; _timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
_isdeleted = user.IsDeleted.ToString(); _isdeleted = user.IsDeleted.ToString();
_candelete = user.IsDeleted;
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn)); _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress; _lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); _ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
@@ -344,6 +365,7 @@
user.EmailConfirmed = bool.Parse(_confirmed); user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid; user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));

View File

@@ -1,7 +1,7 @@
@namespace Oqtane.Modules.Admin.Users @namespace Oqtane.Modules.Admin.Users
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUserService UserService @inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Users> Localizer @inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -43,17 +43,9 @@
var fileid = _filemanager.GetFileId(); var fileid = _filemanager.GetFileId();
if (fileid != -1) if (fileid != -1)
{ {
ShowProgressIndicator(); var siteTask = new SiteTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}");
var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify)); await SiteTaskService.AddSiteTaskAsync(siteTask);
if (bool.Parse(results["Success"])) AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success);
{
AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
} }
else else
{ {

View File

@@ -61,6 +61,12 @@
{ {
<input type="file" id="@_fileinputid" name="file" accept="@_filter" /> <input type="file" id="@_fileinputid" name="file" accept="@_filter" />
} }
@if (MaxUploadFileSize > 0)
{
<div class="row my-1">
<small class="fw-light">@string.Format(Localizer["File.MaxSize"], MaxUploadFileSize)</small>
</div>
}
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@@ -163,6 +169,9 @@
[Parameter] [Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter]
public int MaxUploadFileSize { get; set; } = -1; // optional - maximum upload file size in MB
[Parameter] [Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
@@ -381,16 +390,39 @@
if (uploads.Length > 0) if (uploads.Length > 0)
{ {
string restricted = ""; string restricted = "";
string tooLarge = "";
foreach (var upload in uploads) foreach (var upload in uploads)
{ {
var filename = upload.Split(':')[0]; var fileparts = upload.Split(':');
var filename = fileparts[0];
if (MaxUploadFileSize > 0)
{
var filesizeBytes = long.Parse(fileparts[1]);
var filesizeMB = (double)filesizeBytes / (1024 * 1024);
if (filesizeMB > MaxUploadFileSize)
{
tooLarge += (tooLarge == "" ? "" : ",") + filename;
}
}
var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : ""; var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : "";
if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower())) if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower()))
{ {
restricted += (restricted == "" ? "" : ",") + extension; restricted += (restricted == "" ? "" : ",") + extension;
} }
} }
if (restricted == "") if (restricted != "")
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
else if (tooLarge != "")
{
_message = string.Format(Localizer["Message.File.TooLarge"], tooLarge, MaxUploadFileSize);
_messagetype = MessageType.Warning;
}
else
{ {
CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationTokenSource tokenSource = new CancellationTokenSource();
@@ -490,11 +522,6 @@
tokenSource.Dispose(); tokenSource.Dispose();
} }
} }
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
} }
else else
{ {

View File

@@ -31,8 +31,8 @@
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["CreatedOn"]</th> <th>@Localizer["CreatedOn"]</th>
<th>@SharedLocalizer["CreatedBy"]</th> <th>@Localizer["CreatedBy"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="View" Security="SecurityAccessLevel.Edit" OnClick="@(async () => await View(context))" ResourceKey="View" /></td> <td><ActionLink Action="View" Security="SecurityAccessLevel.Edit" OnClick="@(async () => await View(context))" ResourceKey="View" /></td>
@@ -122,13 +122,9 @@
{ {
try try
{ {
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); _view = htmltext.Content;
if (htmltext != null) _view = Utilities.FormatContent(_view, PageState.Alias, "render");
{ StateHasChanged();
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -141,19 +137,15 @@
{ {
try try
{ {
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); var content = htmltext.Content;
if (htmltext != null) htmltext = new HtmlText();
{ htmltext.ModuleId = ModuleState.ModuleId;
var content = htmltext.Content; htmltext.Content = content;
htmltext = new HtmlText(); await HtmlTextService.AddHtmlTextAsync(htmltext);
htmltext.ModuleId = ModuleState.ModuleId; await logger.LogInformation("Content Restored {HtmlText}", htmltext);
htmltext.Content = content; AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await HtmlTextService.AddHtmlTextAsync(htmltext); await LoadContent();
await logger.LogInformation("Content Restored {HtmlText}", htmltext); StateHasChanged();
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -166,15 +158,11 @@
{ {
try try
{ {
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
if (htmltext != null) await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
{ AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); await LoadContent();
await logger.LogInformation("Content Deleted {HtmlText}", htmltext); StateHasChanged();
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -7,6 +7,18 @@ using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Services namespace Oqtane.Modules.HtmlText.Services
{ {
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService
{ {
@@ -24,11 +36,6 @@ namespace Oqtane.Modules.HtmlText.Services
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId)); return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
} }
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText) public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
{ {
return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText);

View File

@@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
namespace Oqtane.Modules.HtmlText.Services
{
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
}

View File

@@ -16,6 +16,12 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="versions" ResourceKey="Versions" ResourceType="@resourceType" HelpText="The number of content versions to preserve (note that zero means unlimited)">Versions: </Label>
<div class="col-sm-9">
<input id="versions" type="number" min="0" max="9" step="1" class="form-control" @bind="@_versions" />
</div>
</div>
</div> </div>
</form> </form>
@@ -26,12 +32,14 @@
private bool validated = false; private bool validated = false;
private string _dynamictokens; private string _dynamictokens;
private string _versions = "5";
protected override void OnInitialized() protected override void OnInitialized()
{ {
try try
{ {
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"); _dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
_versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "5");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -45,6 +53,10 @@
{ {
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens); settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
if (int.TryParse(_versions, out int versions) && versions >= 0 && versions <= 9)
{
settings = SettingService.SetSetting(settings, "Versions", versions.ToString());
}
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageReference Include="Radzen.Blazor" Version="8.6.0" /> <PackageReference Include="Radzen.Blazor" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CaseSensitive.Text" xml:space="preserve">
<value>Match Case?</value>
</data>
<data name="CaseSensitive.HelpText" xml:space="preserve">
<value>Specify if the replacement operation should be case sensitive</value>
</data>
<data name="Content.Text" xml:space="preserve">
<value>Module Content?</value>
</data>
<data name="Content.HelpText" xml:space="preserve">
<value>Specify if module content should be updated</value>
</data>
<data name="Pages.Text" xml:space="preserve">
<value>Page Properties?</value>
</data>
<data name="Pages.HelpText" xml:space="preserve">
<value>Specify if page properties should be updated (ie. name, title, headcontent, bodycontent)</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Site Properties?</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>Specify if site properties should be updated (ie. name, headcontent, bodycontent)</value>
</data>
<data name="Replace.Text" xml:space="preserve">
<value>Replace With:</value>
</data>
<data name="Replace.HelpText" xml:space="preserve">
<value>Specify the replacement content</value>
</data>
<data name="Modules.Text" xml:space="preserve">
<value>Module Properties?</value>
</data>
<data name="Modules.HelpText" xml:space="preserve">
<value>Specify if module properties should be updated (ie. title, header, footer)</value>
</data>
<data name="Success.Save" xml:space="preserve">
<value>Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data>
<data name="Error.Save" xml:space="preserve">
<value>Error Saving Global Replace</value>
</data>
<data name="Find.HelpText" xml:space="preserve">
<value>Specify the content which needs to be replaced</value>
</data>
<data name="Find.Text" xml:space="preserve">
<value>Find What:</value>
</data>
<data name="GlobalReplace.Header" xml:space="preserve">
<value>Global Replace</value>
</data>
<data name="GlobalReplace.Message" xml:space="preserve">
<value>This Operation is Permanent. Are You Sure You Wish To Proceed?</value>
</data>
</root>

View File

@@ -234,4 +234,10 @@
<data name="Pages.Heading" xml:space="preserve"> <data name="Pages.Heading" xml:space="preserve">
<value>Pages</value> <value>Pages</value>
</data> </data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root> </root>

View File

@@ -130,7 +130,7 @@
<value>Indicate if this module should be displayed on all pages</value> <value>Indicate if this module should be displayed on all pages</value>
</data> </data>
<data name="Page.HelpText" xml:space="preserve"> <data name="Page.HelpText" xml:space="preserve">
<value>The page that the module is located on</value> <value>The page that the module is located on. Please note that shared modules cannot be moved to other pages.</value>
</data> </data>
<data name="Title.Text" xml:space="preserve"> <data name="Title.Text" xml:space="preserve">
<value>Title: </value> <value>Title: </value>

View File

@@ -186,4 +186,10 @@
<data name="TimeZone.HelpText" xml:space="preserve"> <data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value> <value>Your time zone</value>
</data> </data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root> </root>

View File

@@ -319,16 +319,16 @@
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value> <value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data> </data>
<data name="DefaultAlias.Text" xml:space="preserve"> <data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value> <value>Default?</value>
</data> </data>
<data name="Aliases.Heading" xml:space="preserve"> <data name="Aliases.Heading" xml:space="preserve">
<value>Site Urls</value> <value>Site Urls</value>
</data> </data>
<data name="AliasName" xml:space="preserve"> <data name="AliasName.Text" xml:space="preserve">
<value>Url</value> <value>Url:</value>
</data> </data>
<data name="AliasDefault" xml:space="preserve"> <data name="Default" xml:space="preserve">
<value>Default?</value> <value>Default</value>
</data> </data>
<data name="Confirm.Alias.Delete" xml:space="preserve"> <data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value> <value>Are You Sure You Wish To Delete {0}?</value>
@@ -400,7 +400,7 @@
<value>Hybrid Enabled?</value> <value>Hybrid Enabled?</value>
</data> </data>
<data name="Runtime.HelpText" xml:space="preserve"> <data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value> <value>The hosting model for UI components which require interactivity</value>
</data> </data>
<data name="Runtime.Text" xml:space="preserve"> <data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value> <value>Interactivity:</value>
@@ -495,4 +495,76 @@
<data name="StartTlsWhenAvailable" xml:space="preserve"> <data name="StartTlsWhenAvailable" xml:space="preserve">
<value>Use TLS When Available</value> <value>Use TLS When Available</value>
</data> </data>
<data name="AliasName.HelpText" xml:space="preserve">
<value>A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value>
</data>
<data name="SiteGroupMembers.Text" xml:space="preserve">
<value>Group:</value>
</data>
<data name="SiteGroupMembers.HelpText" xml:space="preserve">
<value>The site groups in this tenant (database)</value>
</data>
<data name="Primary.Text" xml:space="preserve">
<value>Primary?</value>
</data>
<data name="Primary.HelpText" xml:space="preserve">
<value>Indicates if the selected member is the primary site of the site group</value>
</data>
<data name="GroupName.Text" xml:space="preserve">
<value>Name:</value>
</data>
<data name="GroupName.HelpText" xml:space="preserve">
<value>Name of the site group</value>
</data>
<data name="Primary" xml:space="preserve">
<value>Primary</value>
</data>
<data name="Secondary" xml:space="preserve">
<value>Secondary</value>
</data>
<data name="Compare" xml:space="preserve">
<value>Compare</value>
</data>
<data name="Update" xml:space="preserve">
<value>Update</value>
</data>
<data name="DeleteSiteGroupMember.Header" xml:space="preserve">
<value>Delete Site Group Member</value>
</data>
<data name="Confirm.SiteGroupMember.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete This Member From The Site Group?</value>
</data>
<data name="Message.Required.GroupName" xml:space="preserve">
<value>Site Group Name Is Required</value>
</data>
<data name="Message.Site.Synchronize" xml:space="preserve">
<value>Site Submitted For Synchronization</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Members:</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>The sites which are members of this site group</value>
</data>
<data name="Synchronized.Text" xml:space="preserve">
<value>Synchronized:</value>
</data>
<data name="GroupType.Text" xml:space="preserve">
<value>Type:</value>
</data>
<data name="GroupType.HelpText" xml:space="preserve">
<value>Defines the specific behavior of the site group</value>
</data>
<data name="Synchronized.HelpText" xml:space="preserve">
<value>The date/time when the site was last synchronized</value>
</data>
<data name="Synchronization" xml:space="preserve">
<value>Synchronization</value>
</data>
<data name="Localization" xml:space="preserve">
<value>Localization</value>
</data>
<data name="ChangeDetection" xml:space="preserve">
<value>Change Detection</value>
</data>
</root> </root>

View File

@@ -123,29 +123,14 @@
<data name="SqlServer" xml:space="preserve"> <data name="SqlServer" xml:space="preserve">
<value>SQL Server</value> <value>SQL Server</value>
</data> </data>
<data name="Container.Select" xml:space="preserve">
<value>Select Container</value>
</data>
<data name="DefaultContainer.Text" xml:space="preserve">
<value>Default Container: </value>
</data>
<data name="Theme.Select" xml:space="preserve">
<value>Select Theme</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve"> <data name="Aliases.HelpText" xml:space="preserve">
<value>The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value> <value>The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="DefaultContainer.HelpText" xml:space="preserve">
<value>Select the default container for the site</value>
</data> </data>
<data name="Tenant.Text" xml:space="preserve"> <data name="Tenant.Text" xml:space="preserve">
<value>Database: </value> <value>Database: </value>
</data> </data>
<data name="Aliases.Text" xml:space="preserve"> <data name="Aliases.Text" xml:space="preserve">
<value>Urls: </value> <value>Url: </value>
</data>
<data name="DefaultTheme.Text" xml:space="preserve">
<value>Default Theme: </value>
</data> </data>
<data name="SiteTemplate.Select" xml:space="preserve"> <data name="SiteTemplate.Select" xml:space="preserve">
<value>Select Site Template</value> <value>Select Site Template</value>
@@ -177,9 +162,6 @@
<data name="Name.HelpText" xml:space="preserve"> <data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the site</value> <value>Enter the name of the site</value>
</data> </data>
<data name="DefaultTheme.HelpText" xml:space="preserve">
<value>Select the default theme for the site</value>
</data>
<data name="SiteTemplate.HelpText" xml:space="preserve"> <data name="SiteTemplate.HelpText" xml:space="preserve">
<value>Select the site template</value> <value>Select the site template</value>
</data> </data>
@@ -222,12 +204,6 @@
<data name="Error.Database.LoadConfig" xml:space="preserve"> <data name="Error.Database.LoadConfig" xml:space="preserve">
<value>Error loading Database Configuration Control</value> <value>Error loading Database Configuration Control</value>
</data> </data>
<data name="RenderMode.HelpText" xml:space="preserve">
<value>The default render mode for the site</value>
</data>
<data name="RenderMode.Text" xml:space="preserve">
<value>Render Mode: </value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve"> <data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value> <value>Enter a complete connection string including all parameters and delimiters</value>
</data> </data>
@@ -240,10 +216,4 @@
<data name="EnterConnectionString" xml:space="preserve"> <data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value> <value>Enter Connection String</value>
</data> </data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value>
</data>
</root> </root>

View File

@@ -186,4 +186,10 @@
<data name="Permissions.Heading" xml:space="preserve"> <data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value> <value>Permissions</value>
</data> </data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root> </root>

View File

@@ -288,4 +288,10 @@
<data name="Error.Passkey.Fail" xml:space="preserve"> <data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value> <value>Passkey Could Not Be Created</value>
</data> </data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root> </root>

View File

@@ -168,4 +168,10 @@
<data name="Confirmed.HelpText" xml:space="preserve"> <data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value> <value>Indicates if the user's email is verified</value>
</data> </data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value />
</data>
</root> </root>

View File

@@ -252,4 +252,10 @@
<data name="Message.Logins.None" xml:space="preserve"> <data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value> <value>You Do Not Have Any External Logins For This Site</value>
</data> </data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>The user's preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root> </root>

View File

@@ -129,11 +129,8 @@
<data name="Import" xml:space="preserve"> <data name="Import" xml:space="preserve">
<value>Import</value> <value>Import</value>
</data> </data>
<data name="Message.Import.Failure" xml:space="preserve">
<value>User Import Failed. Please Review Your Event Log For More Detailed Information.</value>
</data>
<data name="Message.Import.Success" xml:space="preserve"> <data name="Message.Import.Success" xml:space="preserve">
<value>User Import Successful. {0} Users Imported.</value> <value>Your User Import Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data> </data>
<data name="Message.Import.Validation" xml:space="preserve"> <data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Specify A User File For Import</value> <value>You Must Specify A User File For Import</value>

View File

@@ -144,4 +144,10 @@
<data name="Message.File.Restricted" xml:space="preserve"> <data name="Message.File.Restricted" xml:space="preserve">
<value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value> <value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value>
</data> </data>
<data name="File.MaxSize" xml:space="preserve">
<value>Maximum upload file size: {0} MB</value>
</data>
<data name="Message.File.TooLarge" xml:space="preserve">
<value>File(s) {0} exceed(s) the maximum upload size of {1} MB</value>
</data>
</root> </root>

View File

@@ -123,4 +123,10 @@
<data name="DynamicTokens.Text" xml:space="preserve"> <data name="DynamicTokens.Text" xml:space="preserve">
<value>Dynamic Tokens?</value> <value>Dynamic Tokens?</value>
</data> </data>
<data name="Versions.HelpText" xml:space="preserve">
<value>The number of content versions to preserve (note that zero means unlimited)</value>
</data>
<data name="Versions.Text" xml:space="preserve">
<value>Versions:</value>
</data>
</root> </root>

View File

@@ -204,6 +204,9 @@
<data name="System Update" xml:space="preserve"> <data name="System Update" xml:space="preserve">
<value>System Update</value> <value>System Update</value>
</data> </data>
<data name="Setting Management" xml:space="preserve">
<value>Setting Management</value>
</data>
<data name="Download" xml:space="preserve"> <data name="Download" xml:space="preserve">
<value>Download</value> <value>Download</value>
</data> </data>
@@ -480,4 +483,7 @@
<data name="Installed" xml:space="preserve"> <data name="Installed" xml:space="preserve">
<value>Installed</value> <value>Installed</value>
</data> </data>
<data name="Global Replace" xml:space="preserve">
<value>Global Replace</value>
</data>
</root> </root>

View File

@@ -174,9 +174,6 @@
<data name="Title" xml:space="preserve"> <data name="Title" xml:space="preserve">
<value>Title:</value> <value>Title:</value>
</data> </data>
<data name="System.Update" xml:space="preserve">
<value>Check For System Updates</value>
</data>
<data name="Visibility" xml:space="preserve"> <data name="Visibility" xml:space="preserve">
<value>Visibility:</value> <value>Visibility:</value>
</data> </data>
@@ -200,5 +197,11 @@
</data> </data>
<data name="Module.CopyExisting" xml:space="preserve"> <data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value> <value>Copy Existing Module</value>
</data> </data>
<data name="Synchronize" xml:space="preserve">
<value>Synchronize Site</value>
</data>
<data name="Copy" xml:space="preserve">
<value>Copy Page</value>
</data>
</root> </root>

View File

@@ -14,8 +14,9 @@ namespace Oqtane.Services
/// Set the localization cookie /// Set the localization cookie
/// </summary> /// </summary>
/// <param name="culture"></param> /// <param name="culture"></param>
/// <param name="uiCulture"></param>
/// <returns></returns> /// <returns></returns>
Task SetLocalizationCookieAsync(string culture); Task SetLocalizationCookieAsync(string culture, string uiCulture);
} }
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -23,7 +24,7 @@ namespace Oqtane.Services
{ {
public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { } public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Task SetLocalizationCookieAsync(string culture) public Task SetLocalizationCookieAsync(string culture, string uiCulture)
{ {
return Task.CompletedTask; // only used in server side rendering return Task.CompletedTask; // only used in server side rendering
} }

View File

@@ -13,10 +13,16 @@ namespace Oqtane.Services
public interface ILocalizationService public interface ILocalizationService
{ {
/// <summary> /// <summary>
/// Returns a collection of supported cultures /// Returns a collection of supported or installed cultures
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<Culture>> GetCulturesAsync(bool installed); Task<IEnumerable<Culture>> GetCulturesAsync(bool installed);
/// <summary>
/// Returns a collection of neutral cultures
/// </summary>
/// <returns></returns>
Task<IEnumerable<Culture>> GetNeutralCulturesAsync();
} }
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -26,6 +32,14 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("Localization"); private string Apiurl => CreateApiUrl("Localization");
public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed) => await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}"); public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed)
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
}
public async Task<IEnumerable<Culture>> GetNeutralCulturesAsync()
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}/neutral");
}
} }
} }

View File

@@ -71,6 +71,15 @@ namespace Oqtane.Services
/// <param name="pageId"></param> /// <param name="pageId"></param>
/// <returns></returns> /// <returns></returns>
Task DeletePageAsync(int pageId); Task DeletePageAsync(int pageId);
/// <summary>
/// Copies the modules from one page to another
/// </summary>
/// <param name="fromPageId"></param>
/// <param name="toPageId"></param>
/// <param name="usePagePermissions"></param>
/// <returns></returns>
Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions);
} }
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -129,5 +138,10 @@ namespace Oqtane.Services
{ {
await DeleteAsync($"{Apiurl}/{pageId}"); await DeleteAsync($"{Apiurl}/{pageId}");
} }
public async Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions)
{
await PostAsync($"{Apiurl}/{fromPageId}/{toPageId}/{usePagePermissions}");
}
} }
} }

View File

@@ -0,0 +1,104 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupMemberService
{
/// <summary>
/// Get all <see cref="SiteGroupMember"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroupMember"/> to the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Update a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Delete a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task DeleteSiteGroupMemberAsync(int siteGroupMemberId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupMemberService : ServiceBase, ISiteGroupMemberService
{
public SiteGroupMemberService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroupMember");
public async Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId)
{
return await GetJsonAsync<List<SiteGroupMember>>($"{Apiurl}?siteid={siteId}&groupid={siteGroupId}", Enumerable.Empty<SiteGroupMember>().ToList());
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId)
{
return await GetJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMemberId}");
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId)
{
var siteGroupMembers = await GetSiteGroupMembersAsync(siteId, siteGroupId);
if (siteGroupMembers != null && siteGroupMembers.Count > 0)
{
return siteGroupMembers[0];
}
else
{
return null;
}
}
public async Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PostJsonAsync<SiteGroupMember>(Apiurl, siteGroupMember);
}
public async Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PutJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMember.SiteGroupId}", siteGroupMember);
}
public async Task DeleteSiteGroupMemberAsync(int siteGroupMemberId)
{
await DeleteAsync($"{Apiurl}/{siteGroupMemberId}");
}
}
}

View File

@@ -0,0 +1,94 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupService
{
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync();
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId);
/// <summary>
/// Get one specific <see cref="SiteGroup"/>
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroup> GetSiteGroupAsync(int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroup"/> to the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Update a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Delete a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task DeleteSiteGroupAsync(int siteGroupId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupService : ServiceBase, ISiteGroupService
{
public SiteGroupService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroup");
public async Task<List<SiteGroup>> GetSiteGroupsAsync()
{
return await GetSiteGroupsAsync(-1);
}
public async Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId)
{
return await GetJsonAsync<List<SiteGroup>>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty<SiteGroup>().ToList());
}
public async Task<SiteGroup> GetSiteGroupAsync(int siteGroupId)
{
return await GetJsonAsync<SiteGroup>($"{Apiurl}/{siteGroupId}");
}
public async Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup)
{
return await PostJsonAsync<SiteGroup>(Apiurl, siteGroup);
}
public async Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup)
{
return await PutJsonAsync<SiteGroup>($"{Apiurl}/{siteGroup.SiteGroupId}", siteGroup);
}
public async Task DeleteSiteGroupAsync(int siteGroupId)
{
await DeleteAsync($"{Apiurl}/{siteGroupId}");
}
}
}

View File

@@ -0,0 +1,46 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage tasks (<see cref="SiteTask"/>)
/// </summary>
public interface ISiteTaskService
{
/// <summary>
/// Return a specific task
/// </summary>
/// <param name="siteTaskId"></param>
/// <returns></returns>
Task<SiteTask> GetSiteTaskAsync(int siteTaskId);
/// <summary>
/// Adds a new task
/// </summary>
/// <param name="siteTask"></param>
/// <returns></returns>
Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteTaskService : ServiceBase, ISiteTaskService
{
public SiteTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteTask");
public async Task<SiteTask> GetSiteTaskAsync(int siteTaskId)
{
return await GetJsonAsync<SiteTask>($"{Apiurl}/{siteTaskId}");
}
public async Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask)
{
return await PostJsonAsync<SiteTask>(Apiurl, siteTask);
}
}
}

View File

@@ -161,15 +161,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<string> GetPasswordRequirementsAsync(int siteId); Task<string> GetPasswordRequirementsAsync(int siteId);
/// <summary>
/// Bulk import of users
/// </summary>
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <param name="fileId">ID of a <see cref="File"/></param>
/// <param name="notify">Indicates if new users should be notified by email</param>
/// <returns></returns>
Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify);
/// <summary> /// <summary>
/// Get passkeys for a user /// Get passkeys for a user
/// </summary> /// </summary>
@@ -351,11 +342,6 @@ namespace Oqtane.Services
return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement); return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
} }
public async Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify)
{
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId) public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
{ {
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}"); return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");

View File

@@ -97,6 +97,7 @@
Alias = PageState.Alias, Alias = PageState.Alias,
Site = new Site Site = new Site
{ {
SiteId = PageState.Site.SiteId,
DefaultContainerType = PageState.Site.DefaultContainerType, DefaultContainerType = PageState.Site.DefaultContainerType,
Settings = PageState.Site.Settings, Settings = PageState.Site.Settings,
Themes = PageState.Site.Themes Themes = PageState.Site.Themes

View File

@@ -11,6 +11,7 @@
@inject ILogService logger @inject ILogService logger
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IJSRuntime jsRuntime @inject IJSRuntime jsRuntime
@inject ISiteGroupService SiteGroupService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject ILogService LoggingService @inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer @inject IStringLocalizer<ControlPanelInteractive> Localizer
@@ -34,6 +35,11 @@
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button> <button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div> </div>
</div> </div>
@if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
<hr class="app-rule" />
<button type="button" class="btn btn-secondary col-12 mt-1" @onclick="SynchronizeSite">@Localizer["Synchronize"]</button>
}
<hr class="app-rule" /> <hr class="app-rule" />
} }
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) @if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -53,18 +59,29 @@
<button type="button" class="btn btn-danger col ms-1" @onclick="ConfirmDelete">@SharedLocalizer["Delete"]</button> <button type="button" class="btn btn-danger col ms-1" @onclick="ConfirmDelete">@SharedLocalizer["Delete"]</button>
</div> </div>
</div> </div>
<div class="row d-flex"> @if (PageState.Page.UserId == null)
<div class="col"> {
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone)) <div class="row d-flex mb-2">
{ <div class="col">
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button> <button type="button" class="btn btn-secondary col-12" data-bs-dismiss="offcanvas" @onclick=@(async () => Navigate("Copy"))>@Localizer["Copy"]</button>
} </div>
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
</div> </div>
</div> }
@if (!PageState.Page.Path.StartsWith("admin/"))
{
<div class="row d-flex">
<div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
}
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
</div>
</div>
}
<hr class="app-rule" /> <hr class="app-rule" />
@if (_deleteConfirmation) @if (_deleteConfirmation)
@@ -149,7 +166,7 @@
<option value="@p.PageId">@p.Name</option> <option value="@p.PageId">@p.Name</option>
} }
</select> </select>
<select class="form-select mt-1" @bind="@_moduleId"> <select class="form-select mt-1" value="@_moduleId" @onchange="(e => PageModuleChanged(e))">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option> <option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules) @foreach (Module module in _modules)
{ {
@@ -257,6 +274,7 @@
private List<Page> _pages = new List<Page>(); private List<Page> _pages = new List<Page>();
private List<Module> _modules = new List<Module>(); private List<Module> _modules = new List<Module>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private string _category = "Common"; private string _category = "Common";
private string _pane = ""; private string _pane = "";
@@ -287,6 +305,7 @@
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId); _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId);
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList();
_siteGroups = await SiteGroupService.GetSiteGroupsAsync(PageState.Site.SiteId);
} }
} }
@@ -339,6 +358,20 @@
StateHasChanged(); StateHasChanged();
} }
private async Task PageModuleChanged(ChangeEventArgs e)
{
_moduleId = (string)e.Value;
if (_moduleId != "-")
{
_title = _modules.First(item => item.ModuleId == int.Parse(_moduleId)).Title;
}
else
{
_title = "";
}
StateHasChanged();
}
private async Task AddModule() private async Task AddModule()
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -488,7 +521,12 @@
case "Edit": case "Edit":
// get page management moduleid // get page management moduleid
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]); moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
case "Copy":
// get page management moduleid
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&copy=true&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break; break;
} }
} }
@@ -631,4 +669,14 @@
{ {
_message = ""; _message = "";
} }
private async Task SynchronizeSite()
{
foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
group.Synchronize = true;
await SiteGroupService.UpdateSiteGroupAsync(group);
}
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true);
}
} }

View File

@@ -6,22 +6,29 @@
@inject ILocalizationCookieService LocalizationCookieService @inject ILocalizationCookieService LocalizationCookieService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1) @if (PageState.Site.Languages.Count() > 1)
{ {
<div class="app-languages btn-group pe-1" role="group"> <div class="app-languages btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span> <span class="oi oi-globe"></span>
</button> </button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures"> <div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures) @foreach (var language in PageState.Site.Languages)
{ {
@if (PageState.RenderMode == RenderModes.Interactive) @if (_contentLocalization)
{ {
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))" @onclick:preventDefault="true">@culture.DisplayName</a> <a class="dropdown-item @(PageState.Site.CultureCode == language.Code ? "active" : String.Empty)" href="@(PageState.Alias.Protocol + language.AliasName)" data-enhance-nav="false">@language.Name</a>
} }
else else
{ {
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)" data-enhance-nav="false">@culture.DisplayName</a> @if (PageState.RenderMode == RenderModes.Interactive)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(language.Code))" @onclick:preventDefault="true">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + language.Code)" data-enhance-nav="false">@language.Name</a>
}
} }
} }
</div> </div>
@@ -29,7 +36,7 @@
} }
@code{ @code{
private IEnumerable<Culture> _supportedCultures; private bool _contentLocalization;
private string MenuAlignment = string.Empty; private string MenuAlignment = string.Empty;
[Parameter] [Parameter]
@@ -41,14 +48,15 @@
{ {
MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty;
_supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); // if AliasName is populated it means the site is using content localization
_contentLocalization = PageState.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName));
if (PageState.QueryString.ContainsKey("culture")) if (PageState.QueryString.ContainsKey("culture"))
{ {
var culture = PageState.QueryString["culture"]; var culture = PageState.QueryString["culture"];
if (_supportedCultures.Any(item => item.Name == culture)) if (PageState.Site.Languages.Any(item => item.Code == culture))
{ {
await LocalizationCookieService.SetLocalizationCookieAsync(culture); await LocalizationCookieService.SetLocalizationCookieAsync(PageState.Site.CultureCode, culture);
} }
NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", "")); NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""));
} }
@@ -58,7 +66,7 @@
{ {
if (culture != CultureInfo.CurrentUICulture.Name) if (culture != CultureInfo.CurrentUICulture.Name)
{ {
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(PageState.Site.CultureCode, culture));
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
NavigationManager.NavigateTo(NavigationManager.Uri, true); NavigationManager.NavigateTo(NavigationManager.Uri, true);

View File

@@ -233,15 +233,8 @@
if (page == null && route.PagePath == "") // naked path refers to site home page if (page == null && route.PagePath == "") // naked path refers to site home page
{ {
if (site.HomePageId != null) // fallback to use the first page in the collection
{ page = site.Pages.FirstOrDefault();
page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
}
if (page == null)
{
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
} }
if (page == null) if (page == null)
{ {
@@ -632,11 +625,11 @@
{ {
if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site) if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
{ {
if (resource.Url.StartsWith("~")) if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~"))
{ {
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
} }
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{ {
resource.Url = alias.BaseUrl + resource.Url; resource.Url = alias.BaseUrl + resource.Url;
} }

View File

@@ -227,38 +227,4 @@
} }
return stylesheets; return stylesheets;
} }
private string ManageScripts(List<Resource> resources, Alias alias)
{
var scripts = "";
if (resources != null)
{
foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script && item.Location == ResourceLocation.Head))
{
var script = CreateScript(resource, alias);
if (!scripts.Contains(script, StringComparison.OrdinalIgnoreCase))
{
scripts += script + Environment.NewLine;
}
}
}
return scripts;
}
private string CreateScript(Resource resource, Alias alias)
{
if (!string.IsNullOrEmpty(resource.Url))
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
return "<script src=\"" + url + "\"" +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
"></script>";
}
else
{
// inline script
return "<script>" + resource.Content + "</script>";
}
}
} }

View File

@@ -11,6 +11,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.Extensions.Localization @using Microsoft.Extensions.Localization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Oqtane.Client @using Oqtane.Client

View File

@@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId> <ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>10.0.4</ApplicationDisplayVersion> <ApplicationDisplayVersion>10.1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@@ -54,11 +54,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.2" /> <PackageReference Include="System.Net.Http.Json" Version="10.0.3" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>10.0.4</version> <version>10.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,18 +12,18 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Oqtane.Shared" version="10.0.4" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.1.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Localization" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Http" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.6.0" exclude="Build,Analyzers" /> <dependency id="Radzen.Blazor" version="9.0.0" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>10.0.4</version> <version>10.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright> <copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.1.0/Oqtane.Framework.10.1.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane framework</tags> <tags>oqtane framework</tags>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>10.0.4</version> <version>10.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,29 +12,29 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Oqtane.Client" version="10.0.4" exclude="Build,Analyzers" /> <dependency id="Oqtane.Client" version="10.1.0" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.4" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.1.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" /> <dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" /> <dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.1.0" exclude="Build,Analyzers" /> <dependency id="Swashbuckle.AspNetCore" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" /> <dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.5.0" exclude="Build,Analyzers" /> <dependency id="MySql.Data" version="9.6.0" exclude="Build,Analyzers" />
<dependency id="MySql.EntityFrameworkCore" version="10.0.0-rc" exclude="Build,Analyzers" /> <dependency id="MySql.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.0" exclude="Build,Analyzers" /> <dependency id="EFCore.NamingConventions" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" /> <dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.Data.Sqlite.Core" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.3" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
<frameworkReferences> <frameworkReferences>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>10.0.4</version> <version>10.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,14 +12,14 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.2" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.3" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.3.0" exclude="Build,Analyzers" /> <dependency id="NodaTime" version="3.3.0" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>10.0.4</version> <version>10.1.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Install.zip" -Force

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Upgrade.zip" -Force

View File

@@ -79,6 +79,7 @@
@((MarkupString)_scripts) @((MarkupString)_scripts)
@((MarkupString)_bodyResources) @((MarkupString)_bodyResources)
@((MarkupString)_bodyContent)
@if (_renderMode == RenderModes.Static) @if (_renderMode == RenderModes.Static)
{ {
<page-script src="./js/reload.js?v=@_fingerprint"></page-script> <page-script src="./js/reload.js?v=@_fingerprint"></page-script>
@@ -107,6 +108,7 @@
private string _language = "en"; private string _language = "en";
private string _headResources = ""; private string _headResources = "";
private string _bodyResources = ""; private string _bodyResources = "";
private string _bodyContent = "";
private string _styleSheets = ""; private string _styleSheets = "";
private string _scripts = ""; private string _scripts = "";
private string _message = ""; private string _message = "";
@@ -157,15 +159,8 @@
var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
if (page == null && route.PagePath == "") // naked path refers to site home page if (page == null && route.PagePath == "") // naked path refers to site home page
{ {
if (site.HomePageId != null) // fallback to use the first page in the collection
{ page = site.Pages.FirstOrDefault();
page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
}
if (page == null)
{
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
} }
if (page == null) if (page == null)
{ {
@@ -193,43 +188,20 @@
await GetJwtToken(alias); await GetJwtToken(alias);
} }
// includes resources // include resources
var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action); var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action);
ManageStyleSheets(resources); ManageStyleSheets(resources);
ManageScripts(resources, alias); ManageScripts(resources, alias);
AddBodyContent(site.BodyContent);
AddBodyContent(page.BodyContent);
ManageLocalization(site);
// generate scripts // PWA script
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
{ {
_scripts += CreatePWAScript(alias, site, route); _scripts += CreatePWAScript(alias, site, route);
} }
// set culture if not specified
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (cultureCookie == null)
{
// get default language for site
if (site.Languages.Any())
{
// use default language if specified otherwise use first language in collection
cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
}
else
{
// fallback language
cultureCookie = LocalizationManager.GetDefaultCulture();
}
// convert language code to culture cookie format (ie. "c=en|uic=en")
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie));
SetLocalizationCookie(cultureCookie);
}
// set language for page
if (!string.IsNullOrEmpty(cultureCookie))
{
_language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name;
}
// create initial PageState // create initial PageState
_pageState = new PageState _pageState = new PageState
{ {
@@ -550,8 +522,6 @@
} }
else else
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = ""; var dataAttributes = "";
if (!resource.DataAttributes.ContainsKey("data-reload")) if (!resource.DataAttributes.ContainsKey("data-reload"))
{ {
@@ -573,12 +543,24 @@
} }
} }
return "<script src=\"" + url + "\"" + if (!string.IsNullOrEmpty(resource.Url))
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") + {
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") + var url = (string.IsNullOrEmpty(resource.Url) || resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + return "<script src=\"" + url + "\"" +
"></script>"; ((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
}
else
{
return "<script" +
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
">" + resource.Content + "</script>";
}
} }
} }
@@ -751,11 +733,11 @@
{ {
if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site) if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site)
{ {
if (resource.Url.StartsWith("~")) if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~"))
{ {
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
} }
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{ {
resource.Url = alias.BaseUrl + resource.Url; resource.Url = alias.BaseUrl + resource.Url;
} }
@@ -803,4 +785,71 @@
} }
} }
} }
private void AddBodyContent(string content)
{
if (!string.IsNullOrEmpty(content))
{
var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries);
foreach (var element in elements)
{
if (_renderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script")))
{
if (!_bodyContent.Contains("<" + element) || element.StartsWith("/"))
{
_bodyContent += "<" + element;
}
}
}
_bodyContent += "\n";
}
}
private void ManageLocalization(Site site)
{
// get user culture (ui localization)
var uiCulture = site?.User?.CultureCode;
if (string.IsNullOrEmpty(uiCulture))
{
// get default language for site
if (site.Languages.Any())
{
// use default language if specified otherwise use first language in collection
uiCulture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
}
else
{
// fallback language
uiCulture = LocalizationManager.GetDefaultCulture();
}
}
// get site culture (content localization)
var culture = site?.CultureCode;
if (string.IsNullOrEmpty(culture))
{
culture = uiCulture;
}
_language = culture; // html element language attribute
// get culture cookie
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (cultureCookie != null)
{
// verify culture cookie (which could be inaccurate when using subfolders ie. domain.com/fr as cookies are based on domain)
var requestCulture = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie);
if (culture != requestCulture.Culture.Name || uiCulture != requestCulture.UICulture.Name)
{
// convert to culture cookie format (ie. "c=en|uic=en") and save cookie
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture));
SetLocalizationCookie(cultureCookie);
}
}
else
{
// convert to culture cookie format (ie. "c=en|uic=en") and save cookie
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture));
SetLocalizationCookie(cultureCookie);
}
}
} }

View File

@@ -43,7 +43,7 @@ namespace Oqtane.Controllers
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{ {
var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList()); var hierarchy = _folders.GetFolders(SiteId).ToList();
foreach (Folder folder in hierarchy) foreach (Folder folder in hierarchy)
{ {
// note that Browse permission is used for this method // note that Browse permission is used for this method
@@ -281,47 +281,5 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }
private static List<Folder> GetFoldersHierarchy(List<Folder> folders)
{
List<Folder> hierarchy = new List<Folder>();
Action<List<Folder>, Folder> getPath = null;
getPath = (folderList, folder) =>
{
IEnumerable<Folder> children;
int level;
if (folder == null)
{
level = -1;
children = folders.Where(item => item.ParentId == null);
}
else
{
level = folder.Level;
children = folders.Where(item => item.ParentId == folder.FolderId);
}
foreach (Folder child in children)
{
child.Level = level + 1;
child.HasChildren = folders.Any(item => item.ParentId == child.FolderId);
hierarchy.Add(child);
getPath(folderList, child);
}
};
folders = folders.OrderBy(item => item.Name).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;
}
} }
} }

View File

@@ -19,26 +19,54 @@ namespace Oqtane.Controllers
_localizationManager = localizationManager; _localizationManager = localizationManager;
} }
// GET: api/localization // GET: api/localization?installed=true/false
[HttpGet()] [HttpGet()]
public IEnumerable<Culture> Get(bool installed) public IEnumerable<Culture> Get(bool installed)
{ {
string[] culturecodes; string[] cultureCodes;
if (installed) if (installed)
{ {
culturecodes = _localizationManager.GetInstalledCultures(); cultureCodes = _localizationManager.GetInstalledCultures();
} }
else else
{ {
culturecodes = _localizationManager.GetSupportedCultures(); cultureCodes = _localizationManager.GetSupportedCultures();
} }
var cultures = culturecodes.Select(c => new Culture
var cultures = cultureCodes.Select(c => new Culture
{ {
Name = CultureInfo.GetCultureInfo(c).Name, Name = CultureInfo.GetCultureInfo(c).Name,
DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, DisplayName = CultureInfo.GetCultureInfo(c).DisplayName,
IsDefault = _localizationManager.GetDefaultCulture() IsDefault = _localizationManager.GetDefaultCulture()
.Equals(CultureInfo.GetCultureInfo(c).Name, StringComparison.OrdinalIgnoreCase) .Equals(CultureInfo.GetCultureInfo(c).Name, StringComparison.OrdinalIgnoreCase)
}); }).ToList();
if (cultures.Count == 0)
{
cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = true });
}
return cultures.OrderBy(item => item.DisplayName);
}
// GET: api/localization/neutral
[HttpGet("neutral")]
public IEnumerable<Culture> Get()
{
var cultureCodes = _localizationManager.GetNeutralCultures();
var cultures = cultureCodes.Select(c => new Culture
{
Name = CultureInfo.GetCultureInfo(c).Name,
DisplayName = CultureInfo.GetCultureInfo(c).DisplayName,
IsDefault = false
}).ToList();
if (cultures.Count == 0)
{
cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = false });
}
return cultures.OrderBy(item => item.DisplayName); return cultures.OrderBy(item => item.DisplayName);
} }
} }

View File

@@ -164,6 +164,7 @@ namespace Oqtane.Controllers
{ {
_moduleDefinitions.UpdateModuleDefinition(moduleDefinition); _moduleDefinitions.UpdateModuleDefinition(moduleDefinition);
_syncManager.AddSyncEvent(_alias, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update); _syncManager.AddSyncEvent(_alias, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Definition Updated {ModuleDefinition}", moduleDefinition); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Definition Updated {ModuleDefinition}", moduleDefinition);
} }
else else

View File

@@ -1,16 +1,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Oqtane.Shared;
using System.Linq; using System.Linq;
using Oqtane.Security;
using System.Net; using System.Net;
using System.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using System.Xml.Linq; using Oqtane.Security;
using Microsoft.AspNetCore.Diagnostics; using Oqtane.Shared;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@@ -498,6 +497,91 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }
}
// POST api/<controller>/5/6
[HttpPost("{fromPageId}/{toPageId}/{usePagePermissions}")]
[Authorize(Roles = RoleNames.Registered)]
public void Post(int fromPageId, int toPageId, bool usePagePermissions)
{
var fromPage = _pages.GetPage(fromPageId);
if (fromPage != null && fromPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, fromPage.PermissionList))
{
var toPage = _pages.GetPage(toPageId);
if (toPage != null && toPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, toPage.PermissionList))
{
// copy modules
List<PageModule> pageModules = _pageModules.GetPageModules(fromPage.SiteId).ToList();
foreach (PageModule pm in pageModules.Where(item => item.PageId == fromPage.PageId && !item.Module.AllPages && !item.IsDeleted))
{
Module module;
// determine if module is a shared instance (ie. exists on other pages)
if (!pageModules.Any(item => item.ModuleId == pm.ModuleId && item.PageId != fromPage.PageId))
{
// create new module
module = new Module();
module.SiteId = fromPage.SiteId;
module.PageId = toPageId;
module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
module.AllPages = false;
if (usePagePermissions)
{
module.PermissionList = toPage.PermissionList;
}
else
{
module.PermissionList = pm.Module.PermissionList;
}
module.PermissionList = module.PermissionList.Select(item => new Permission
{
SiteId = item.SiteId,
EntityName = EntityNames.Module,
EntityId = -1,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
}).ToList();
module = _modules.AddModule(module);
string content = _modules.ExportModule(pm.ModuleId);
if (content != "")
{
_modules.ImportModule(module.ModuleId, content);
}
}
else
{
// use existing module
module = pm.Module;
}
PageModule pageModule = new PageModule();
pageModule.PageId = toPageId;
pageModule.ModuleId = module.ModuleId;
pageModule.Title = pm.Title;
pageModule.Pane = pm.Pane;
pageModule.Order = pm.Order;
pageModule.ContainerType = pm.ContainerType;
pageModule.EffectiveDate = pm.EffectiveDate;
pageModule.ExpiryDate = pm.ExpiryDate;
pageModule.Header = pm.Header;
pageModule.Footer = pm.Footer;
_pageModules.AddPageModule(pageModule);
}
_syncManager.AddSyncEvent(_alias, EntityNames.Site, fromPage.SiteId, SyncEventActions.Refresh);
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
} }

View File

@@ -0,0 +1,135 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Policy;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteGroupController : Controller
{
private readonly ISiteGroupRepository _siteGroupRepository;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteGroupController(ISiteGroupRepository siteGroupRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_siteGroupRepository = siteGroupRepository;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>?siteid=x
[HttpGet]
[Authorize(Roles = RoleNames.Admin)]
public IEnumerable<SiteGroup> Get(string siteid)
{
if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId))
{
var siteGroups = _siteGroupRepository.GetSiteGroups();
if (!User.IsInRole(RoleNames.Host))
{
siteGroups = siteGroups.Where(item => item.PrimarySiteId == _alias.SiteId);
}
return siteGroups.ToList();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroup Get(int id)
{
var group = _siteGroupRepository.GetSiteGroup(id);
if (group != null)
{
return group;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Host)]
public SiteGroup Post([FromBody] SiteGroup siteGroup)
{
if (ModelState.IsValid)
{
siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {Group}", siteGroup);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {Group}", siteGroup);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroup = null;
}
return siteGroup;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Admin)]
public SiteGroup Put(int id, [FromBody] SiteGroup siteGroup)
{
if (ModelState.IsValid && siteGroup.SiteGroupId == id)
{
if (!User.IsInRole(RoleNames.Host) && siteGroup.Synchronize)
{
// admins can only update the synchronize field
siteGroup = _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupId, false);
siteGroup.Synchronize = true;
}
siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {Group}", siteGroup);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {Group}", siteGroup);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroup = null;
}
return siteGroup;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Host)]
public void Delete(int id)
{
var siteGroup = _siteGroupRepository.GetSiteGroup(id);
if (siteGroup != null)
{
_siteGroupRepository.DeleteSiteGroup(id);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {siteGroupId}", id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {siteGroupId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteGroupMemberController : Controller
{
private readonly ISiteGroupMemberRepository _siteGroupMemberRepository;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteGroupMemberController(ISiteGroupMemberRepository siteGroupMemberRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_siteGroupMemberRepository = siteGroupMemberRepository;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>?siteid=x&groupid=y
[HttpGet]
[Authorize(Roles = RoleNames.Host)]
public IEnumerable<SiteGroupMember> Get(string siteid, string groupid)
{
if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupId))
{
return _siteGroupMemberRepository.GetSiteGroupMembers(SiteId, SiteGroupId).ToList();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Get Attempt for SiteId {SiteId} And SiteGroupId {SiteGroupId}", siteid, groupid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Get(int id)
{
var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id);
if (siteGroupMember != null)
{
return siteGroupMember;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Post([FromBody] SiteGroupMember siteGroupMember)
{
if (ModelState.IsValid)
{
siteGroupMember = _siteGroupMemberRepository.AddSiteGroupMember(siteGroupMember);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Member Added {SiteGroupMember}", siteGroupMember);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Post Attempt {SiteGroupMember}", siteGroupMember);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroupMember = null;
}
return siteGroupMember;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Host)]
public SiteGroupMember Put(int id, [FromBody] SiteGroupMember siteGroupMember)
{
if (ModelState.IsValid && siteGroupMember.SiteGroupId == id && _siteGroupMemberRepository.GetSiteGroupMember(siteGroupMember.SiteGroupId, false) != null)
{
siteGroupMember = _siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Member Updated {SiteGroupMember}", siteGroupMember);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Put Attempt {SiteGroupMember}", siteGroupMember);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteGroupMember = null;
}
return siteGroupMember;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Host)]
public void Delete(int id)
{
var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id);
if (siteGroupMember != null)
{
_siteGroupMemberRepository.DeleteSiteGroupMember(id);
_syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupMemberId, SyncEventActions.Delete);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Member Deleted {SiteGroupMemberId}", id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Delete Attempt {SiteGroupMemberId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SiteTaskController : Controller
{
private readonly ISiteTaskRepository _siteTasks;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteTaskController(ISiteTaskRepository siteTasks, ILogManager logger, ITenantManager tenantManager)
{
_siteTasks = siteTasks;
_logger = logger;
_alias = tenantManager.GetAlias();
}
// GET api/<controller>/5
[HttpGet("{id}")]
[Authorize(Roles = RoleNames.Admin)]
public SiteTask Get(int id)
{
var siteTask = _siteTasks.GetSiteTask(id);
if (siteTask.SiteId == _alias.SiteId)
{
return siteTask;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
public SiteTask Post([FromBody] SiteTask siteTask)
{
if (ModelState.IsValid && siteTask.SiteId == _alias.SiteId)
{
siteTask.IsCompleted = false;
siteTask = _siteTasks.AddSiteTask(siteTask);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Task Added {SiteTask}", siteTask);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Task Post Attempt {SiteTask}", siteTask);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
siteTask = null;
}
return siteTask;
}
}
}

View File

@@ -117,6 +117,7 @@ namespace Oqtane.Controllers
{ {
_themes.UpdateTheme(theme); _themes.UpdateTheme(theme);
_syncManager.AddSyncEvent(_alias, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update); _syncManager.AddSyncEvent(_alias, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme);
} }
else else

View File

@@ -418,42 +418,6 @@ namespace Oqtane.Controllers
return requirements; return requirements;
} }
// POST api/<controller>/import?siteid=x&fileid=y&notify=z
[HttpPost("import")]
[Authorize(Roles = RoleNames.Admin)]
public async Task<Dictionary<string, string>> Import(string siteid, string fileid, string notify)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify))
{
var file = _files.GetFile(FileId);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET: api/<controller>/passkey?id=x // GET: api/<controller>/passkey?id=x
[HttpGet("passkey")] [HttpGet("passkey")]
[Authorize] [Authorize]

View File

@@ -233,6 +233,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ICookieConsentService, ServerCookieConsentService>(); services.AddScoped<ICookieConsentService, ServerCookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>(); services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>(); services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<ISiteGroupService, SiteGroupService>();
services.AddScoped<ISiteGroupMemberService, SiteGroupMemberService>();
services.AddScoped<ISiteTaskService, SiteTaskService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
@@ -282,6 +285,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>(); services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();
services.AddTransient<ISearchContentRepository, SearchContentRepository>(); services.AddTransient<ISearchContentRepository, SearchContentRepository>();
services.AddTransient<IMigrationHistoryRepository, MigrationHistoryRepository>(); services.AddTransient<IMigrationHistoryRepository, MigrationHistoryRepository>();
services.AddTransient<ISiteGroupRepository, SiteGroupRepository>();
services.AddTransient<ISiteGroupMemberRepository, SiteGroupMemberRepository>();
services.AddTransient<ISiteTaskRepository, SiteTaskRepository>();
// managers // managers
services.AddTransient<IDBContextDependencies, DBContextDependencies>(); services.AddTransient<IDBContextDependencies, DBContextDependencies>();

View File

@@ -594,7 +594,8 @@ namespace Oqtane.Infrastructure
Prerender = (rendermode == RenderModes.Interactive), Prerender = (rendermode == RenderModes.Interactive),
Hybrid = false, Hybrid = false,
EnhancedNavigation = true, EnhancedNavigation = true,
TenantId = tenant.TenantId CultureCode = "en",
TenantId = tenant.TenantId // required for site creation
}; };
site = sites.AddSite(site); site = sites.AddSite(site);

View File

@@ -22,7 +22,6 @@ namespace Oqtane.Infrastructure
{ {
void InstallPackages(); void InstallPackages();
bool UninstallPackage(string PackageName); bool UninstallPackage(string PackageName);
int RegisterAssemblies();
Task UpgradeFramework(bool backup); Task UpgradeFramework(bool backup);
void RestartApplication(); void RestartApplication();
} }
@@ -61,10 +60,6 @@ namespace Oqtane.Infrastructure
Directory.CreateDirectory(sourceFolder); Directory.CreateDirectory(sourceFolder);
} }
// read assembly log
var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// install Nuget packages in secure Packages folder // install Nuget packages in secure Packages folder
var packages = Directory.GetFiles(sourceFolder, "*.nupkg"); var packages = Directory.GetFiles(sourceFolder, "*.nupkg");
foreach (string packagename in packages) foreach (string packagename in packages)
@@ -162,27 +157,6 @@ namespace Oqtane.Infrastructure
{ {
manifest = true; manifest = true;
} }
// register assembly
if (Path.GetExtension(filename) == ".dll")
{
// do not register licensing assemblies
if (!Path.GetFileName(filename).StartsWith("Oqtane.Licensing."))
{
// if package version was not installed previously
if (!File.Exists(Path.Combine(sourceFolder, name + ".log")))
{
if (assemblies.ContainsKey(Path.GetFileName(filename)))
{
assemblies[Path.GetFileName(filename)] += 1;
}
else
{
assemblies.Add(Path.GetFileName(filename), 1);
}
}
}
}
} }
} }
@@ -212,12 +186,6 @@ namespace Oqtane.Infrastructure
File.Delete(packagename); File.Delete(packagename);
} }
if (packages.Length != 0)
{
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
}
return errors; return errors;
} }
@@ -270,10 +238,6 @@ namespace Oqtane.Infrastructure
{ {
if (!string.IsNullOrEmpty(PackageName)) if (!string.IsNullOrEmpty(PackageName))
{ {
// read assembly log
var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// get manifest with highest version // get manifest with highest version
string packagename = ""; string packagename = "";
string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log"); string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log");
@@ -298,23 +262,7 @@ namespace Oqtane.Infrastructure
// do not remove licensing assemblies // do not remove licensing assemblies
if (!Path.GetFileName(filepath).StartsWith("Oqtane.Licensing.")) if (!Path.GetFileName(filepath).StartsWith("Oqtane.Licensing."))
{ {
// use assembly log to determine if assembly is used in other packages DeleteFile(filepath);
if (assemblies.ContainsKey(Path.GetFileName(filepath)))
{
if (assemblies[Path.GetFileName(filepath)] == 1)
{
DeleteFile(filepath);
assemblies.Remove(Path.GetFileName(filepath));
}
else
{
assemblies[Path.GetFileName(filepath)] -= 1;
}
}
else // does not exist in assembly log
{
DeleteFile(filepath);
}
} }
} }
else // not an assembly else // not an assembly
@@ -329,9 +277,6 @@ namespace Oqtane.Infrastructure
File.Delete(asset); File.Delete(asset);
} }
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
return true; return true;
} }
} }
@@ -351,64 +296,6 @@ namespace Oqtane.Infrastructure
} }
} }
public int RegisterAssemblies()
{
var assemblyLogPath = GetAssemblyLogPath();
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblies = GetAssemblyLog(assemblyLogPath);
// remove assemblies that no longer exist
foreach (var dll in assemblies)
{
if (!File.Exists(Path.Combine(binFolder, dll.Key)))
{
assemblies.Remove(dll.Key);
}
}
// add assemblies which are not registered
foreach (var dll in Directory.GetFiles(binFolder, "*.dll"))
{
if (!assemblies.ContainsKey(Path.GetFileName(dll)))
{
assemblies.Add(Path.GetFileName(dll), 1);
}
}
SetAssemblyLog(assemblyLogPath, assemblies);
return assemblies.Count;
}
private string GetAssemblyLogPath()
{
string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(packagesFolder))
{
Directory.CreateDirectory(packagesFolder);
}
return Path.Combine(packagesFolder, "assemblies.log");
}
private static Dictionary<string, int> GetAssemblyLog(string assemblyLogPath)
{
Dictionary<string, int> assemblies = new Dictionary<string, int>();
if (File.Exists(assemblyLogPath))
{
assemblies = JsonSerializer.Deserialize<Dictionary<string, int>>(File.ReadAllText(assemblyLogPath));
}
return assemblies;
}
private static void SetAssemblyLog(string assemblyLogPath, Dictionary<string, int> assemblies)
{
if (File.Exists(assemblyLogPath))
{
File.Delete(assemblyLogPath);
}
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true }));
}
public async Task UpgradeFramework(bool backup) public async Task UpgradeFramework(bool backup)
{ {
string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);

View File

@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteTask
{
string ExecuteTask(IServiceProvider provider, Site site, string parameters);
Task<string> ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters);
}
}

View File

@@ -38,7 +38,7 @@ namespace Oqtane.Infrastructure
// iterate through sites for current tenant // iterate through sites for current tenant
List<Site> sites = siteRepository.GetSites().ToList(); List<Site> sites = siteRepository.GetSites().ToList();
foreach (Site site in sites) foreach (Site site in sites.Where(item => !item.IsDeleted))
{ {
log += "Processing Notifications For Site: " + site.Name + "<br />"; log += "Processing Notifications For Site: " + site.Name + "<br />";
@@ -48,7 +48,7 @@ namespace Oqtane.Infrastructure
// get site settings // get site settings
var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1); var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1);
if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") if (settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True")
{ {
bool valid = true; bool valid = true;
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
@@ -121,11 +121,23 @@ namespace Oqtane.Infrastructure
{ {
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{ {
// it is possible to use basic without any authentication (not recommended)
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "")
{ {
await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), try
{
await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""),
settingRepository.GetSettingValue(settings, "SMTPPassword", "")); settingRepository.GetSettingValue(settings, "SMTPPassword", ""));
}
catch (Exception ex)
{
log += "SMTP Not Configured Properly In Site Settings - Basic Authentication Failed Using Username And Password - " + ex.Message + "<br />";
valid = false;
}
}
else
{
// it is possible to use basic without any authentication (not recommended)
} }
} }
else else
@@ -278,7 +290,7 @@ namespace Oqtane.Infrastructure
} }
else else
{ {
log += "Site Deleted Or SMTP Disabled In Site Settings<br />"; log += "SMTP Disabled In Site Settings<br />";
} }
} }
else else

View File

@@ -33,11 +33,11 @@ namespace Oqtane.Infrastructure
var visitorRepository = provider.GetRequiredService<IVisitorRepository>(); var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>(); var notificationRepository = provider.GetRequiredService<INotificationRepository>();
var urlMappingRepository = provider.GetRequiredService<IUrlMappingRepository>(); var urlMappingRepository = provider.GetRequiredService<IUrlMappingRepository>();
var installationManager = provider.GetRequiredService<IInstallationManager>(); var siteTaskRepository = provider.GetRequiredService<ISiteTaskRepository>();
// iterate through sites for current tenant // iterate through sites for current tenant
List<Site> sites = siteRepository.GetSites().ToList(); List<Site> sites = siteRepository.GetSites().ToList();
foreach (Site site in sites) foreach (Site site in sites.Where(item => !item.IsDeleted))
{ {
log += "<br />Processing Site: " + site.Name + "<br />"; log += "<br />Processing Site: " + site.Name + "<br />";
int count; int count;
@@ -95,17 +95,18 @@ namespace Oqtane.Infrastructure
{ {
log += $"Error Purging Broken Urls - {ex.Message}<br />"; log += $"Error Purging Broken Urls - {ex.Message}<br />";
} }
}
// register assemblies // purge completed site tasks
try retention = 30; // 30 day default
{ try
var assemblies = installationManager.RegisterAssemblies(); {
log += "<br />" + assemblies.ToString() + " Assemblies Registered<br />"; count = siteTaskRepository.DeleteSiteTasks(site.SiteId, retention);
} log += count.ToString() + " Completed Tasks Purged<br />";
catch (Exception ex) }
{ catch (Exception ex)
log += $"<br />Error Registering Assemblies - {ex.Message}<br />"; {
log += $"Error Purging Completed Site Tasks - {ex.Message}<br />";
}
} }
return log; return log;

View File

@@ -40,7 +40,7 @@ namespace Oqtane.Infrastructure
var searchService = provider.GetRequiredService<ISearchService>(); var searchService = provider.GetRequiredService<ISearchService>();
var sites = siteRepository.GetSites().ToList(); var sites = siteRepository.GetSites().ToList();
foreach (var site in sites) foreach (var site in sites.Where(item => !item.IsDeleted))
{ {
log += $"Indexing Site: {site.Name}<br />"; log += $"Indexing Site: {site.Name}<br />";

View File

@@ -0,0 +1,84 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Repository;
namespace Oqtane.Infrastructure
{
public class SiteTaskJob : HostedServiceBase
{
public SiteTaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
Name = "Site Task Job";
Frequency = "m"; // run every minute
Interval = 1;
IsEnabled = true;
}
// job is executed for each tenant in installation
public override async Task<string> ExecuteJobAsync(IServiceProvider provider)
{
var log = "";
// resolve services
var tenantManager = provider.GetRequiredService<ITenantManager>();
var siteRepository = provider.GetRequiredService<ISiteRepository>();
var siteTaskRepository = provider.GetRequiredService<ISiteTaskRepository>();
var tenant = tenantManager.GetTenant();
// iterate through sites for current tenant
var sites = siteRepository.GetSites().ToList();
foreach (var site in sites.Where(item => !item.IsDeleted))
{
log += $"Processing Site: {site.Name}<br />";
// get incomplete tasks for site
var tasks = siteTaskRepository.GetSiteTasks(site.SiteId).ToList();
if (tasks != null && tasks.Any())
{
foreach (var task in tasks)
{
log += $"Executing Task: {task.Name}<br />";
Type taskType = Type.GetType(task.Type);
if (taskType != null && taskType.GetInterface(nameof(ISiteTask)) != null)
{
try
{
tenantManager.SetAlias(tenant.TenantId, site.SiteId);
var taskObject = ActivatorUtilities.CreateInstance(provider, taskType);
var taskLog = ((ISiteTask)taskObject).ExecuteTask(provider, site, task.Parameters);
taskLog += await ((ISiteTask)taskObject).ExecuteTaskAsync(provider, site, task.Parameters);
task.Status = taskLog;
}
catch (Exception ex)
{
task.Status = "Error: " + ex.Message;
}
}
else
{
task.Status = $"Error: Task {task.Name} Has An Invalid Type {task.Type}<br />";
}
// update task
task.IsCompleted = true;
siteTaskRepository.UpdateSiteTask(task);
log += task.Status + "<br />";
}
}
else
{
log += "No Tasks To Execute<br />";
}
}
return log;
}
}
}

View File

@@ -0,0 +1,895 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
public class SynchronizationJob : HostedServiceBase
{
// JobType = "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server"
// synchronization only supports sites in the same tenant (database)
// module title is used as a key to identify module instances on a page
// modules must implement ISynchronizable interface for content synchronization
// change detection does not support deleted items as key values will usually be different due to localization
// define settings that should not be synchronized (should be extensible in the future)
List<Setting> excludedSettings = new List<Setting>() {
new Setting { EntityName = EntityNames.Site, SettingName = "Search_LastIndexedOn" }
};
public SynchronizationJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
Name = "Synchronization Job";
Frequency = "m"; // minute
Interval = 1;
IsEnabled = false;
}
// job is executed for each tenant in installation
public override string ExecuteJob(IServiceProvider provider)
{
string log = "";
var siteGroupRepository = provider.GetRequiredService<ISiteGroupRepository>();
var siteGroupMemberRepository = provider.GetRequiredService<ISiteGroupMemberRepository>();
var siteRepository = provider.GetRequiredService<ISiteRepository>();
var aliasRepository = provider.GetRequiredService<IAliasRepository>();
var tenantManager = provider.GetRequiredService<ITenantManager>();
var settingRepository = provider.GetRequiredService<ISettingRepository>();
List<SiteGroupMember> siteGroupMembers = null;
List<Site> sites = null;
List<Alias> aliases = null;
// get site groups
var siteGroups = siteGroupRepository.GetSiteGroups();
// iterate through site groups which need to be synchronized
foreach (var siteGroup in siteGroups.Where(item => item.Synchronize && (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection)))
{
// get data
if (siteGroupMembers == null)
{
siteGroupMembers = siteGroupMemberRepository.GetSiteGroupMembers().ToList();
sites = siteRepository.GetSites().ToList();
aliases = aliasRepository.GetAliases().ToList();
}
var primaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name;
log += (siteGroup.Type == SiteGroupTypes.Synchronization) ? "Synchronizing " : "Comparing ";
log += $"Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(primaryAliasName)}<br />";
// get primary site
var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId);
if (primarySite != null && !primarySite.IsDeleted)
{
// update flag to prevent job from processing group again
siteGroup.Synchronize = false;
siteGroupRepository.UpdateSiteGroup(siteGroup);
// iterate through sites in site group
foreach (var siteGroupMember in siteGroupMembers.Where(item => item.SiteGroupId == siteGroup.SiteGroupId && item.SiteId != siteGroup.PrimarySiteId))
{
// get secondary site
var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId);
if (secondarySite != null)
{
// get default alias for site
var secondaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name;
siteGroupMember.AliasName = (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) ? secondaryAliasName : primaryAliasName;
// initialize SynchronizedOn
if (siteGroupMember.SynchronizedOn == null)
{
siteGroupMember.SynchronizedOn = DateTime.MinValue;
}
// synchronize site
var siteLog = SynchronizeSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite);
if (string.IsNullOrEmpty(siteLog))
{
siteLog = (siteGroupMember.SynchronizedOn != DateTime.MinValue) ? "No Changes Identified<br />" : "Initialization Complete<br />";
}
// set synchronized date/time
siteGroupMember.SynchronizedOn = DateTime.UtcNow;
siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember);
log += $"With Secondary Site: {secondarySite.Name} - {CreateLink(secondaryAliasName)}<br />" + siteLog;
}
else
{
log += $"Site Group {siteGroup.Name} Has A SiteId {siteGroupMember.SiteId} Which Does Not Exist<br />";
}
}
}
else
{
log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist Or Is Deleted<br />";
}
}
if (string.IsNullOrEmpty(log))
{
log = "No Site Groups Require Synchronization<br />";
}
return log;
}
private string SynchronizeSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, Site primarySite, Site secondarySite)
{
var log = "";
// synchronize roles
log += SynchronizeRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId);
// synchronize folders/files
log += SynchronizeFolders(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId);
// synchronize pages/modules
log += SynchronizePages(provider, settingRepository, tenantManager, siteGroupMember, primarySite.SiteId, secondarySite.SiteId);
// synchronize site
if (primarySite.ModifiedOn > siteGroupMember.SynchronizedOn)
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
secondarySite.TimeZoneId = primarySite.TimeZoneId;
secondarySite.CultureCode = primarySite.CultureCode;
if (secondarySite.LogoFileId != primarySite.LogoFileId)
{
secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId);
}
if (secondarySite.FaviconFileId != primarySite.FaviconFileId)
{
secondarySite.FaviconFileId = ResolveFileId(provider, primarySite.FaviconFileId, secondarySite.SiteId); ;
}
secondarySite.DefaultThemeType = primarySite.DefaultThemeType;
secondarySite.DefaultContainerType = primarySite.DefaultContainerType;
secondarySite.AdminContainerType = primarySite.AdminContainerType;
secondarySite.PwaIsEnabled = primarySite.PwaIsEnabled;
if (secondarySite.PwaAppIconFileId != primarySite.PwaAppIconFileId)
{
secondarySite.PwaAppIconFileId = ResolveFileId(provider, primarySite.PwaAppIconFileId, secondarySite.SiteId); ;
}
if (secondarySite.PwaSplashIconFileId != primarySite.PwaSplashIconFileId)
{
secondarySite.PwaSplashIconFileId = ResolveFileId(provider, primarySite.PwaSplashIconFileId, secondarySite.SiteId); ;
}
secondarySite.AllowRegistration = primarySite.AllowRegistration;
secondarySite.VisitorTracking = primarySite.VisitorTracking;
secondarySite.CaptureBrokenUrls = primarySite.CaptureBrokenUrls;
secondarySite.SiteGuid = primarySite.SiteGuid;
secondarySite.RenderMode = primarySite.RenderMode;
secondarySite.Runtime = primarySite.Runtime;
secondarySite.Prerender = primarySite.Prerender;
secondarySite.Hybrid = primarySite.Hybrid;
secondarySite.EnhancedNavigation = primarySite.EnhancedNavigation;
secondarySite.Version = primarySite.Version;
secondarySite.HeadContent = primarySite.HeadContent;
secondarySite.BodyContent = primarySite.BodyContent;
secondarySite.CreatedBy = primarySite.CreatedBy;
secondarySite.CreatedOn = primarySite.CreatedOn;
secondarySite.ModifiedBy = primarySite.ModifiedBy;
secondarySite.ModifiedOn = primarySite.ModifiedOn;
secondarySite.IsDeleted = primarySite.IsDeleted;
secondarySite.DeletedBy = primarySite.DeletedBy;
secondarySite.DeletedOn = primarySite.DeletedOn;
var siteRepository = provider.GetRequiredService<ISiteRepository>();
siteRepository.UpdateSite(secondarySite);
log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}");
}
else // change detection
{
log += Log(siteGroupMember, $"Site Updated: {primarySite.Name} - {CreateLink(siteGroupMember.AliasName)}");
}
}
// site settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId);
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)))
{
// clear cache for secondary site if any content was Synchronized
var syncManager = provider.GetRequiredService<ISyncManager>();
var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySite.SiteId };
syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh);
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection && !string.IsNullOrEmpty(log))
{
// send change log to administrators
log += SendNotifications(provider, siteGroupMember, secondarySite.SiteId, secondarySite.Name, log);
}
return log;
}
private int? ResolveFileId(IServiceProvider provider, int? fileId, int siteId)
{
if (fileId != null)
{
var fileRepository = provider.GetRequiredService<IFileRepository>();
var file = fileRepository.GetFile(fileId.Value);
fileId = fileRepository.GetFile(siteId, file.Folder.Path, file.Name).FileId;
}
return fileId;
}
private string SynchronizeRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId)
{
// get roles
var roleRepository = provider.GetRequiredService<IRoleRepository>();
var primaryRoles = roleRepository.GetRoles(primarySiteId);
var secondaryRoles = roleRepository.GetRoles(secondarySiteId).ToList();
var log = "";
foreach (var primaryRole in primaryRoles)
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var role = secondaryRoles.FirstOrDefault(item => item.Name == primaryRole.Name);
var secondaryRole = role;
if (secondaryRole == null)
{
secondaryRole = new Role();
secondaryRole.SiteId = secondarySiteId;
}
if (role == null || primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn)
{
// set all properties
secondaryRole.Name = primaryRole.Name;
secondaryRole.Description = primaryRole.Description;
secondaryRole.IsAutoAssigned = primaryRole.IsAutoAssigned;
secondaryRole.IsSystem = primaryRole.IsSystem;
if (role == null)
{
roleRepository.AddRole(secondaryRole);
log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}");
}
else
{
roleRepository.UpdateRole(secondaryRole);
log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}");
}
}
if (role != null)
{
secondaryRoles.Remove(role);
}
}
else // change detection
{
if (primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"Role Updated: {primaryRole.Name}");
}
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// remove roles in the secondary site which do not exist in the primary site
foreach (var secondaryRole in secondaryRoles)
{
roleRepository.DeleteRole(secondaryRole.RoleId);
log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}");
}
}
// settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Role, primarySiteId, secondarySiteId);
return log;
}
private string SynchronizeFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId)
{
var folderRepository = provider.GetRequiredService<IFolderRepository>();
var fileRepository = provider.GetRequiredService<IFileRepository>();
var log = "";
// get folders (ignore personalized)
var primaryFolders = folderRepository.GetFolders(primarySiteId).Where(item => !item.Path.StartsWith("Users/"));
var secondaryFolders = folderRepository.GetFolders(secondarySiteId).Where(item => !item.Path.StartsWith("Users/")).ToList();
// iterate through folders
foreach (var primaryFolder in primaryFolders)
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var folder = secondaryFolders.FirstOrDefault(item => item.Path == primaryFolder.Path);
var secondaryFolder = folder;
if (secondaryFolder == null)
{
secondaryFolder = new Folder();
secondaryFolder.SiteId = secondarySiteId;
}
if (folder == null || primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn)
{
// set all properties
secondaryFolder.ParentId = null;
if (primaryFolder.ParentId != null)
{
var parentFolder = folderRepository.GetFolder(secondarySiteId, primaryFolders.First(item => item.FolderId == primaryFolder.ParentId).Path);
if (parentFolder != null)
{
secondaryFolder.ParentId = parentFolder.FolderId;
}
}
secondaryFolder.Type = primaryFolder.Type;
secondaryFolder.Name = primaryFolder.Name;
secondaryFolder.Order = primaryFolder.Order;
secondaryFolder.ImageSizes = primaryFolder.ImageSizes;
secondaryFolder.Capacity = primaryFolder.Capacity;
secondaryFolder.ImageSizes = primaryFolder.ImageSizes;
secondaryFolder.IsSystem = primaryFolder.IsSystem;
secondaryFolder.PermissionList = SynchronizePermissions(primaryFolder.PermissionList, secondarySiteId);
if (folder == null)
{
folderRepository.AddFolder(secondaryFolder);
log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}");
}
else
{
folderRepository.UpdateFolder(secondaryFolder);
log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}");
}
}
if (folder != null)
{
secondaryFolders.Remove(folder);
}
// folder settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId);
// files
log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, secondaryFolder);
}
else // change detection
{
if (primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"Folder Updated: {primaryFolder.Path}");
// folder settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, -1);
// files
log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, null);
}
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// remove folders in the secondary site which do not exist in the primary site
foreach (var secondaryFolder in secondaryFolders)
{
folderRepository.DeleteFolder(secondaryFolder.FolderId);
log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}");
}
}
return log;
}
private string SynchronizeFiles(IServiceProvider provider, IFolderRepository folderRepository, IFileRepository fileRepository, SiteGroupMember siteGroupMember, Folder primaryFolder, Folder secondaryFolder)
{
var log = "";
// get files for folder
var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId);
var secondaryFiles = new List<Models.File>();
if (secondaryFolder != null)
{
secondaryFiles = fileRepository.GetFiles(secondaryFolder.FolderId).ToList();
}
foreach (var primaryFile in primaryFiles)
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var file = secondaryFiles.FirstOrDefault(item => item.Name == primaryFile.Name);
var secondaryFile = file;
if (secondaryFile == null)
{
secondaryFile = new Models.File();
secondaryFile.FolderId = secondaryFolder.FolderId;
secondaryFile.Name = primaryFile.Name;
}
if (file == null || primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn)
{
// set all properties
secondaryFile.Extension = primaryFile.Extension;
secondaryFile.Size = primaryFile.Size;
secondaryFile.ImageHeight = primaryFile.ImageHeight;
secondaryFile.ImageWidth = primaryFile.ImageWidth;
secondaryFile.Description = primaryFile.Description;
if (file == null)
{
fileRepository.AddFile(secondaryFile);
SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile);
log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}");
}
else
{
fileRepository.UpdateFile(secondaryFile);
SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile);
log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}");
}
}
if (file != null)
{
secondaryFiles.Remove(file);
}
}
else // change detection
{
if (primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryFolder.Path + primaryFile.Name)}");
}
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// remove files in the secondary site which do not exist in the primary site
foreach (var secondaryFile in secondaryFiles)
{
fileRepository.DeleteFile(secondaryFile.FileId);
var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name);
System.IO.File.Delete(secondaryPath);
log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}");
}
}
return log;
}
private void SynchronizeFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile)
{
var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name);
if (System.IO.File.Exists(primaryPath))
{
var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name);
if (!Directory.Exists(Path.GetDirectoryName(secondaryPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(secondaryPath));
}
System.IO.File.Copy(primaryPath, secondaryPath, true);
}
}
private string SynchronizePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId)
{
var pageRepository = provider.GetRequiredService<IPageRepository>();
var pageModuleRepository = provider.GetRequiredService<IPageModuleRepository>();
var moduleRepository = provider.GetRequiredService<IModuleRepository>();
var log = "";
int tenantId = tenantManager.GetTenant().TenantId;
tenantManager.SetAlias(tenantId, primarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions()
var primaryPageModules = pageModuleRepository.GetPageModules(primarySiteId).ToList();
tenantManager.SetAlias(tenantId, secondarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions()
var secondaryPageModules = pageModuleRepository.GetPageModules(secondarySiteId).ToList();
// get pages (ignore personalized)
var primaryPages = pageRepository.GetPages(primarySiteId).Where(item => item.UserId == null);
var secondaryPages = pageRepository.GetPages(secondarySiteId).Where(item => item.UserId == null).ToList();
// iterate through primary pages
foreach (var primaryPage in primaryPages)
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var page = secondaryPages.FirstOrDefault(item => item.Path == primaryPage.Path);
var secondaryPage = page;
if (secondaryPage == null)
{
secondaryPage = new Page();
secondaryPage.SiteId = secondarySiteId;
}
if (page == null || primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn)
{
// set all properties
secondaryPage.Path = primaryPage.Path;
secondaryPage.Name = primaryPage.Name;
secondaryPage.ParentId = null;
if (primaryPage.ParentId != null)
{
var parentPage = pageRepository.GetPage(primaryPages.First(item => item.PageId == primaryPage.ParentId).Path, secondarySiteId);
if (parentPage != null)
{
secondaryPage.ParentId = parentPage.PageId;
}
}
secondaryPage.Title = primaryPage.Title;
secondaryPage.Order = primaryPage.Order;
secondaryPage.Url = primaryPage.Url;
secondaryPage.ThemeType = primaryPage.ThemeType;
secondaryPage.DefaultContainerType = primaryPage.DefaultContainerType;
secondaryPage.HeadContent = primaryPage.HeadContent;
secondaryPage.BodyContent = primaryPage.BodyContent;
secondaryPage.Icon = primaryPage.Icon;
secondaryPage.IsNavigation = primaryPage.IsNavigation;
secondaryPage.IsClickable = primaryPage.IsClickable;
secondaryPage.UserId = null;
secondaryPage.IsPersonalizable = primaryPage.IsPersonalizable;
secondaryPage.EffectiveDate = primaryPage.EffectiveDate;
secondaryPage.ExpiryDate = primaryPage.ExpiryDate;
secondaryPage.CreatedBy = primaryPage.CreatedBy;
secondaryPage.CreatedOn = primaryPage.CreatedOn;
secondaryPage.ModifiedBy = primaryPage.ModifiedBy;
secondaryPage.ModifiedOn = primaryPage.ModifiedOn;
secondaryPage.DeletedBy = primaryPage.DeletedBy;
secondaryPage.DeletedOn = primaryPage.DeletedOn;
secondaryPage.IsDeleted = primaryPage.IsDeleted;
secondaryPage.PermissionList = SynchronizePermissions(primaryPage.PermissionList, secondarySiteId);
if (page == null)
{
secondaryPage = pageRepository.AddPage(secondaryPage);
log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
else
{
secondaryPage = pageRepository.UpdatePage(secondaryPage);
log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
}
if (page != null)
{
secondaryPages.Remove(page);
}
// page settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId);
// modules
log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, secondaryPage, secondarySiteId);
}
else // change detection
{
if (primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}");
}
// page settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, -1);
// modules
log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, null, secondarySiteId);
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// remove pages in the secondary site which do not exist in the primary site
foreach (var secondaryPage in secondaryPages)
{
pageRepository.DeletePage(secondaryPage.PageId);
log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log)))
{
// clear cache for secondary site if any content was Synchronized
var syncManager = provider.GetRequiredService<ISyncManager>();
var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId };
syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh);
}
return log;
}
private string SynchronizeModules(IServiceProvider provider, ISettingRepository settingRepository, IPageModuleRepository pageModuleRepository, IModuleRepository moduleRepository, SiteGroupMember siteGroupMember, List<PageModule> primaryPageModules, List<PageModule> secondaryPageModules, Page primaryPage, Page secondaryPage, int secondarySiteId)
{
var log = "";
var removePageModules = secondaryPageModules.Where(item => item.PageId == secondaryPage.PageId).ToList();
// iterate through primary modules on primary page
foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId))
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var pageModule = secondaryPageModules.FirstOrDefault(item => item.PageId == secondaryPage.PageId && item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower());
var secondaryPageModule = pageModule;
if (secondaryPageModule == null)
{
secondaryPageModule = new PageModule();
secondaryPageModule.PageId = secondaryPage.PageId;
secondaryPageModule.Module = new Module();
secondaryPageModule.Module.SiteId = secondarySiteId;
secondaryPageModule.Module.ModuleDefinitionName = primaryPageModule.Module.ModuleDefinitionName;
}
if (pageModule == null || primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn || primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn)
{
// set all properties
secondaryPageModule.Title = primaryPageModule.Title;
secondaryPageModule.Pane = primaryPageModule.Pane;
secondaryPageModule.Order = primaryPageModule.Order;
secondaryPageModule.ContainerType = primaryPageModule.ContainerType;
secondaryPageModule.Header = primaryPageModule.Header;
secondaryPageModule.Footer = primaryPageModule.Footer;
secondaryPageModule.IsDeleted = primaryPageModule.IsDeleted;
secondaryPageModule.Module.PermissionList = SynchronizePermissions(primaryPageModule.Module.PermissionList, secondarySiteId);
secondaryPageModule.Module.AllPages = false;
secondaryPageModule.Module.IsDeleted = false;
if (pageModule == null)
{
// check if module exists in site (ie. a shared instance)
var module = secondaryPageModules.FirstOrDefault(item => item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower())?.Module;
if (module == null)
{
module = moduleRepository.AddModule(secondaryPageModule.Module);
log += Log(siteGroupMember, $"Module Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
if (module != null)
{
secondaryPageModule.ModuleId = module.ModuleId;
secondaryPageModule.Module = null; // remove tracking
secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule);
log += Log(siteGroupMember, $"Module Instance Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
secondaryPageModule.Module = module;
}
}
else
{
// update existing module
if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn)
{
moduleRepository.UpdateModule(secondaryPageModule.Module);
log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn)
{
secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule);
log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
}
}
if (pageModule != null)
{
removePageModules.Remove(pageModule);
}
// module settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId);
// module content
log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, secondaryPageModule, primaryPage, secondaryPage);
}
else // change detection
{
if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"Module Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}");
}
if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn)
{
log += Log(siteGroupMember, $"Module Instance Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}");
}
// module settings
log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, -1);
// module content
log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, null, primaryPage, secondaryPage);
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// remove modules on the secondary page which do not exist on the primary page
foreach (var secondaryPageModule in removePageModules)
{
pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId);
log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPageModule.Page.Path)}");
}
}
return log;
}
private string SynchronizeModuleContent(IServiceProvider provider, SiteGroupMember siteGroupMember, PageModule primaryPageModule, PageModule secondaryPageModule, Page primaryPage, Page secondaryPage)
{
var log = "";
if (primaryPageModule.Module.ModuleDefinition.ServerManagerType != "")
{
Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType);
if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null)
{
try
{
var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType);
var moduleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module, siteGroupMember.SynchronizedOn.Value);
if (!string.IsNullOrEmpty(moduleContent))
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, moduleContent);
log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}");
}
else // change detection
{
log += Log(siteGroupMember, $"Module Content Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}");
}
}
}
catch
{
// error exporting/importing
}
}
}
return log;
}
private List<Permission> SynchronizePermissions(List<Permission> permissionList, int siteId)
{
return permissionList.Select(item => new Permission
{
SiteId = siteId,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
CreatedBy = item.CreatedBy,
CreatedOn = item.CreatedOn,
ModifiedBy = item.ModifiedBy,
ModifiedOn = item.ModifiedOn
}).ToList();
}
private string SynchronizeSettings(ISettingRepository settingRepository, SiteGroupMember siteGroupMember, string entityName, int primaryEntityId, int secondaryEntityId)
{
var log = "";
var updated = false;
var secondarySettings = settingRepository.GetSettings(entityName, secondaryEntityId).ToList();
foreach (var primarySetting in settingRepository.GetSettings(entityName, primaryEntityId))
{
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
var secondarySetting = secondarySettings.FirstOrDefault(item => item.SettingName == primarySetting.SettingName);
if (secondarySetting == null)
{
secondarySetting = new Setting();
secondarySetting.EntityName = primarySetting.EntityName;
secondarySetting.EntityId = secondaryEntityId;
secondarySetting.SettingName = primarySetting.SettingName;
secondarySetting.SettingValue = primarySetting.SettingValue;
secondarySetting.IsPrivate = primarySetting.IsPrivate;
if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName))
{
settingRepository.AddSetting(secondarySetting);
updated = true;
}
}
else
{
if (secondarySetting.SettingValue != primarySetting.SettingValue || secondarySetting.IsPrivate != primarySetting.IsPrivate)
{
secondarySetting.SettingValue = primarySetting.SettingValue;
secondarySetting.IsPrivate = primarySetting.IsPrivate;
if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName))
{
settingRepository.UpdateSetting(secondarySetting);
updated = true;
}
}
secondarySettings.Remove(secondarySetting);
}
}
else // change detection
{
if (primarySetting.ModifiedOn > siteGroupMember.SynchronizedOn && !excludedSettings.Any(item => item.EntityName == primarySetting.EntityName && item.SettingName == primarySetting.SettingName))
{
updated = true;
}
}
}
if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization)
{
// any remaining secondary settings need to be deleted
foreach (var secondarySetting in secondarySettings)
{
if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName))
{
settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId);
updated = true;
}
}
}
if (updated)
{
log += Log(siteGroupMember, $"{entityName} Settings Updated");
}
return log;
}
private string SendNotifications(IServiceProvider provider, SiteGroupMember siteGroupMember, int siteId, string siteName, string changeLog)
{
var userRoleRepository = provider.GetRequiredService<IUserRoleRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>();
var log = "";
// get administrators for site
var userRoles = userRoleRepository.GetUserRoles(RoleNames.Admin, siteId);
if (userRoles != null && userRoles.Any())
{
foreach (var userRole in userRoles)
{
var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", changeLog);
notificationRepository.AddNotification(notification);
}
log += Log(siteGroupMember, $"Change Log Sent To Administrators For Secondary Site: {siteName}");
}
else
{
log += Log(siteGroupMember, $"Error Sending Change Log - Secondary Site {siteName} Does Not Have Any Administrators Defined");
}
return log;
}
private string Log(SiteGroupMember siteGroupMember, string content)
{
// not necessary to log initial synchronization
if (siteGroupMember.SynchronizedOn != DateTime.MinValue)
{
return content + "<br />";
}
else
{
return "";
}
}
private string CreateLink(string url)
{
return "<a href=\"" + url + "\" target=\"_new\">" + url + "</a>";
}
}
}

View File

@@ -4,7 +4,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
@@ -14,6 +13,7 @@ namespace Oqtane.Infrastructure
string GetDefaultCulture(); string GetDefaultCulture();
string[] GetSupportedCultures(); string[] GetSupportedCultures();
string[] GetInstalledCultures(); string[] GetInstalledCultures();
string[] GetNeutralCultures();
} }
public class LocalizationManager : ILocalizationManager public class LocalizationManager : ILocalizationManager
@@ -41,7 +41,8 @@ namespace Oqtane.Infrastructure
public string[] GetSupportedCultures() public string[] GetSupportedCultures()
{ {
return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(item => item.Name).OrderBy(c => c).ToArray(); return CultureInfo.GetCultures(CultureTypes.AllCultures)
.Select(item => item.Name).OrderBy(c => c).ToArray();
} }
public string[] GetInstalledCultures() public string[] GetInstalledCultures()
@@ -49,7 +50,14 @@ namespace Oqtane.Infrastructure
return GetSatelliteAssemblyCultures(); return GetSatelliteAssemblyCultures();
} }
// method is static as it is called during startup public string[] GetNeutralCultures()
{
return CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(item => item.IsNeutralCulture)
.Select(item => item.Name).OrderBy(c => c).ToArray();
}
// note: method is public and static as it is called during startup
public static string[] GetSatelliteAssemblyCultures() public static string[] GetSatelliteAssemblyCultures()
{ {
var cultures = new List<string>(); var cultures = new List<string>();

View File

@@ -0,0 +1,164 @@
using System;
using System.Linq;
using System.Net;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
public class GlobalReplaceTask : SiteTaskBase
{
public override string ExecuteTask(IServiceProvider provider, Site site, string parameters)
{
string log = "";
// get services
var siteRepository = provider.GetRequiredService<ISiteRepository>();
var pageRepository = provider.GetRequiredService<IPageRepository>();
var pageModuleRepository = provider.GetRequiredService<IPageModuleRepository>();
var TenantManager = provider.GetRequiredService<ITenantManager>();
var syncManager = provider.GetRequiredService<ISyncManager>();
if (!string.IsNullOrEmpty(parameters))
{
// get parameters
var globalReplace = JsonSerializer.Deserialize<GlobalReplace>(parameters);
var find = globalReplace.Find;
var replace = globalReplace.Replace;
var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
var refresh = false;
log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}<br />";
// site properties
site = siteRepository.GetSite(site.SiteId);
var changed = false;
if (site.Name != null && site.Name.Contains(find, comparisonType))
{
site.Name = site.Name.Replace(find, replace, comparisonType);
changed = true;
}
if (site.HeadContent != null && site.HeadContent.Contains(find, comparisonType))
{
site.HeadContent = site.HeadContent.Replace(find, replace, comparisonType);
changed = true;
}
if (site.BodyContent != null && site.BodyContent.Contains(find, comparisonType))
{
site.BodyContent = site.BodyContent.Replace(find, replace, comparisonType);
changed = true;
}
if (changed && globalReplace.Site)
{
siteRepository.UpdateSite(site);
log += $"Site Updated<br />";
refresh = true;
}
var pages = pageRepository.GetPages(site.SiteId).ToList();
var pageModules = pageModuleRepository.GetPageModules(site.SiteId).ToList();
// iterate pages
foreach (var page in pages)
{
// page properties
changed = false;
if (page.Name != null && page.Name.Contains(find, comparisonType))
{
page.Name = page.Name.Replace(find, replace, comparisonType);
changed = true;
}
if (page.Title != null && page.Title.Contains(find, comparisonType))
{
page.Title = page.Title.Replace(find, replace, comparisonType);
changed = true;
}
if (page.HeadContent != null && page.HeadContent.Contains(find, comparisonType))
{
page.HeadContent = page.HeadContent.Replace(find, replace, comparisonType);
changed = true;
}
if (page.BodyContent != null && page.BodyContent.Contains(find, comparisonType))
{
page.BodyContent = page.BodyContent.Replace(find, replace, comparisonType);
changed = true;
}
if (changed && globalReplace.Pages)
{
pageRepository.UpdatePage(page);
log += $"Page Updated: /{page.Path}<br />";
refresh = true;
}
foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId))
{
// module properties
changed = false;
if (pageModule.Title != null && pageModule.Title.Contains(find, comparisonType))
{
pageModule.Title = pageModule.Title.Replace(find, replace, comparisonType);
changed = true;
}
if (pageModule.Header != null && pageModule.Header.Contains(find, comparisonType))
{
pageModule.Header = pageModule.Header.Replace(find, replace, comparisonType);
changed = true;
}
if (pageModule.Footer != null && pageModule.Footer.Contains(find, comparisonType))
{
pageModule.Footer = pageModule.Footer.Replace(find, replace, comparisonType);
changed = true;
}
if (changed && globalReplace.Modules)
{
pageModuleRepository.UpdatePageModule(pageModule);
log += $"Module Updated: {pageModule.Title} Page: /{page.Path}<br />";
refresh = true;
}
// module content
if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "")
{
Type moduleType = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType);
if (moduleType != null && moduleType.GetInterface(nameof(IPortable)) != null)
{
try
{
var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType);
var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module);
if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(WebUtility.HtmlEncode(find), comparisonType) && globalReplace.Content)
{
moduleContent = moduleContent.Replace(WebUtility.HtmlEncode(find), WebUtility.HtmlEncode(replace), comparisonType);
((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version);
log += $"Module Content Updated: {pageModule.Title} Page: /{page.Path}<br />";
}
}
catch (Exception ex)
{
log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}<br />";
}
}
}
}
}
if (refresh)
{
// clear cache
syncManager.AddSyncEvent(TenantManager.GetAlias(), EntityNames.Site, site.SiteId, SyncEventActions.Refresh);
}
}
else
{
log += $"No Criteria Provided<br />";
}
return log;
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Managers;
using Oqtane.Models;
using Oqtane.Repository;
namespace Oqtane.Infrastructure
{
public class ImportUsersTask : SiteTaskBase
{
public override async Task<string> ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters)
{
string log = "";
if (!string.IsNullOrEmpty(parameters) && parameters.Contains(":"))
{
var fileId = int.Parse(parameters.Split(':')[0]);
var notify = bool.Parse(parameters.Split(':')[1]);
var fileRepository = provider.GetRequiredService<IFileRepository>();
var userManager = provider.GetRequiredService<IUserManager>();
var file = fileRepository.GetFile(fileId);
if (file != null)
{
var filePath = fileRepository.GetFilePath(file);
log += $"Importing Users From {filePath}<br />";
var result = await userManager.ImportUsers(site.SiteId, filePath, notify);
if (result["Success"] == "True")
{
log += $"{result["Users"]} Users Imported<br />";
}
else
{
log += $"User Import Failed<br />";
}
}
else
{
log += $"Import Users FileId {fileId} Does Not Exist<br />";
}
}
return log;
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteTaskBase : ISiteTask
{
public virtual string ExecuteTask(IServiceProvider provider, Site site, string parameters)
{
return "";
}
public virtual Task<string> ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters)
{
return Task.FromResult(string.Empty);
}
}
}

View File

@@ -607,6 +607,34 @@ namespace Oqtane.Infrastructure.SiteTemplates
} }
} }
}); });
pageTemplates.Add(new PageTemplate
{
Name = "Global Replace",
Parent = "Admin",
Order = 23,
Path = "admin/replace",
Icon = Icons.LoopSquare,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
// host pages (order starts at 51) // host pages (order starts at 51)
pageTemplates.Add(new PageTemplate pageTemplates.Add(new PageTemplate

View File

@@ -49,7 +49,10 @@ namespace Oqtane.Infrastructure.SiteTemplates
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
PageTemplateModules = new List<PageTemplateModule> { PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Welcome To Oqtane...", Pane = PaneNames.Default, new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
Title = "Welcome To Oqtane...",
Pane = PaneNames.Default,
Order = 1,
PermissionList = new List<Permission> { PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
@@ -60,7 +63,10 @@ namespace Oqtane.Infrastructure.SiteTemplates
"<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is a modern front-end web framework based on HTML, CSS, and C# that helps you build web applications faster. Blazor provides a component-based architecture with server-side rendering and full client-side interactivity in a single solution, where you can switch between server-side and client-side rendering modes and even mix them in the same web page. For desktop or mobile scenarios, Blazor Hybrid is part of .NET MAUI and uses a Web View to render components natively on all modern devices.</p>" + "<p><a href=\"https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor\" target=\"_new\">Blazor</a> is a modern front-end web framework based on HTML, CSS, and C# that helps you build web applications faster. Blazor provides a component-based architecture with server-side rendering and full client-side interactivity in a single solution, where you can switch between server-side and client-side rendering modes and even mix them in the same web page. For desktop or mobile scenarios, Blazor Hybrid is part of .NET MAUI and uses a Web View to render components natively on all modern devices.</p>" +
"<p>Blazor is a feature of <a href=\"https://dotnet.microsoft.com/apps/aspnet\" target=\"_new\">ASP.NET</a>, the popular cross platform development framework from Microsoft that provides powerful tools and libraries for building modern software applications.</p>" "<p>Blazor is a feature of <a href=\"https://dotnet.microsoft.com/apps/aspnet\" target=\"_new\">ASP.NET</a>, the popular cross platform development framework from Microsoft that provides powerful tools and libraries for building modern software applications.</p>"
}, },
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = PaneNames.Default, new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
Title = "MIT License",
Pane = PaneNames.Default,
Order = 3,
PermissionList = new List<Permission> { PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
@@ -71,7 +77,10 @@ namespace Oqtane.Infrastructure.SiteTemplates
"<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" + "<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" +
"<p>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.</p>" "<p>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.</p>"
}, },
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
Title = "Secure Content",
Pane = PaneNames.Default,
Order = 5,
PermissionList = new List<Permission> { PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
@@ -96,7 +105,10 @@ namespace Oqtane.Infrastructure.SiteTemplates
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
PageTemplateModules = new List<PageTemplateModule> { PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
Title = "Secure Content",
Pane = PaneNames.Default,
Order = 1,
PermissionList = new List<Permission> { PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
@@ -121,7 +133,10 @@ namespace Oqtane.Infrastructure.SiteTemplates
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
PageTemplateModules = new List<PageTemplateModule> { PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "My Page", Pane = PaneNames.Default, new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client",
Title = "My Page",
Pane = PaneNames.Default,
Order = 1,
PermissionList = new List<Permission> { PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),

View File

@@ -93,6 +93,9 @@ namespace Oqtane.Infrastructure
case "10.0.4": case "10.0.4":
Upgrade_10_0_4(tenant, scope); Upgrade_10_0_4(tenant, scope);
break; break;
case "10.1.0":
Upgrade_10_1_0(tenant, scope);
break;
} }
} }
} }
@@ -602,6 +605,45 @@ namespace Oqtane.Infrastructure
RemoveAssemblies(tenant, assemblies, "10.0.4"); RemoveAssemblies(tenant, assemblies, "10.0.4");
} }
private void Upgrade_10_1_0(Tenant tenant, IServiceScope scope)
{
var pageTemplates = new List<PageTemplate>
{
new PageTemplate
{
Update = false,
Name = "Global Replace",
Parent = "Admin",
Order = 23,
Path = "admin/replace",
Icon = Icons.LoopSquare,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
}
};
AddPagesToSites(scope, tenant, pageTemplates);
}
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates) private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
{ {
var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>(); var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>();

View File

@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class SiteGroupEntityBuilder : AuditableBaseEntityBuilder<SiteGroupEntityBuilder>
{
private const string _entityTableName = "SiteGroup";
private readonly PrimaryKey<SiteGroupEntityBuilder> _primaryKey = new("PK_SiteGroup", x => x.SiteGroupId);
public SiteGroupEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override SiteGroupEntityBuilder BuildTable(ColumnsBuilder table)
{
SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId");
Name = AddStringColumn(table, "Name", 200);
Type = AddStringColumn(table, "Type", 50);
PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId");
Synchronize = AddBooleanColumn(table, "Synchronize");
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> SiteGroupId { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
public OperationBuilder<AddColumnOperation> Type { get; set; }
public OperationBuilder<AddColumnOperation> PrimarySiteId { get; set; }
public OperationBuilder<AddColumnOperation> Synchronize { get; set; }
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class SiteGroupMemberEntityBuilder : AuditableBaseEntityBuilder<SiteGroupMemberEntityBuilder>
{
private const string _entityTableName = "SiteGroupMember";
private readonly PrimaryKey<SiteGroupMemberEntityBuilder> _primaryKey = new("PK_SiteGroupMember", x => x.SiteGroupMemberId);
private readonly ForeignKey<SiteGroupMemberEntityBuilder> _groupForeignKey = new("FK_SiteGroupMember_SiteGroup", x => x.SiteGroupId, "SiteGroup", "SiteGroupId", ReferentialAction.Cascade);
private readonly ForeignKey<SiteGroupMemberEntityBuilder> _siteForeignKey = new("FK_SiteGroupMember_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade);
public SiteGroupMemberEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_groupForeignKey);
ForeignKeys.Add(_siteForeignKey);
}
protected override SiteGroupMemberEntityBuilder BuildTable(ColumnsBuilder table)
{
SiteGroupMemberId = AddAutoIncrementColumn(table, "SiteGroupMemberId");
SiteGroupId = AddIntegerColumn(table, "SiteGroupId");
SiteId = AddIntegerColumn(table, "SiteId");
SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true);
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> SiteGroupMemberId { get; set; }
public OperationBuilder<AddColumnOperation> SiteGroupId { get; set; }
public OperationBuilder<AddColumnOperation> SiteId { get; set; }
public OperationBuilder<AddColumnOperation> SynchronizedOn { get; set; }
}
}

View File

@@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class SiteTaskEntityBuilder : AuditableBaseEntityBuilder<SiteTaskEntityBuilder>
{
private const string _entityTableName = "SiteTask";
private readonly PrimaryKey<SiteTaskEntityBuilder> _primaryKey = new("PK_SiteTask", x => x.SiteTaskId);
private readonly ForeignKey<SiteTaskEntityBuilder> _siteForeignKey = new("FK_SiteTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade);
public SiteTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_siteForeignKey);
}
protected override SiteTaskEntityBuilder BuildTable(ColumnsBuilder table)
{
SiteTaskId = AddAutoIncrementColumn(table,"SiteTaskId");
SiteId = AddIntegerColumn(table,"SiteId");
Name = AddStringColumn(table, "Name", 200);
Type = AddStringColumn(table, "Type", 200);
Parameters = AddMaxStringColumn(table, "Parameters", true);
IsCompleted = AddBooleanColumn(table, "IsCompleted", true);
Status = AddMaxStringColumn(table, "Status", true);
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> SiteTaskId { get; private set; }
public OperationBuilder<AddColumnOperation> SiteId { get; private set; }
public OperationBuilder<AddColumnOperation> Name { get; private set; }
public OperationBuilder<AddColumnOperation> Type { get; private set; }
public OperationBuilder<AddColumnOperation> Parameters { get; private set; }
public OperationBuilder<AddColumnOperation> IsCompleted { get; private set; }
public OperationBuilder<AddColumnOperation> Status { get; private set; }
}
}

View File

@@ -0,0 +1,32 @@
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.10.01.00.01")]
public class AddSiteGroups : MultiDatabaseMigration
{
public AddSiteGroups(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteGroupEntityBuilder = new SiteGroupEntityBuilder(migrationBuilder, ActiveDatabase);
siteGroupEntityBuilder.Create();
var siteGroupMemberEntityBuilder = new SiteGroupMemberEntityBuilder(migrationBuilder, ActiveDatabase);
siteGroupMemberEntityBuilder.Create();
siteGroupMemberEntityBuilder.AddIndex("IX_SiteGroupMember", new[] { "SiteId", "SiteGroupId" }, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,33 @@
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.10.01.00.02")]
public class AddCultureCode : MultiDatabaseMigration
{
public AddCultureCode(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.AddStringColumn("CultureCode", 10, true);
siteEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'");
var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase);
userEntityBuilder.AddStringColumn("CultureCode", 10, true);
userEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,28 @@
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.10.01.00.03")]
public class RemoveHomePageId : MultiDatabaseMigration
{
public RemoveHomePageId(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropColumn("HomePageId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,28 @@
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.10.01.00.04")]
public class AddSiteTasks : MultiDatabaseMigration
{
public AddSiteTasks(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteTaskEntityBuilder = new SiteTaskEntityBuilder(migrationBuilder, ActiveDatabase);
siteTaskEntityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

Some files were not shown because too many files have changed in this diff Show More