Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into dev
Some checks failed
build-oqtane / Build the oqtane (push) Has been cancelled

This commit is contained in:
2026-04-11 18:05:16 +02:00
174 changed files with 5542 additions and 1584 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.3</Version> <Version>10.1.2</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.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2025 .NET Foundation Copyright (c) 2018-2026 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,10 +12,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.3" /> <PackageReference Include="Oqtane.Client" Version="10.1.2" />
</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.3</version> <version>10.1.2</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.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.3" /> <PackageReference Include="Oqtane.Server" Version="10.1.2" />
</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

@@ -1,5 +1,6 @@
@namespace [Owner].Module.[Module] @namespace [Owner].Module.[Module]
@inherits ModuleBase @inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IStringLocalizer<Settings> Localizer @inject IStringLocalizer<Settings> Localizer

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.3" /> <PackageReference Include="Oqtane.Shared" Version="10.1.2" />
</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 information should be updated (ie. name, head content, body content, settings)" ResourceKey="Site">Site Info? </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 information should be updated (ie. name, title, head content, body content, settings)" ResourceKey="Pages">Page Info? </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 information should be updated (ie. title, header, footer, settings)" ResourceKey="Modules">Module Info? </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

@@ -5,100 +5,109 @@
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (_initialized)
<div class="container"> {
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label> <div class="container">
<div class="col-sm-9"> @if (!_editable)
<input id="name" class="form-control" @bind="@_name" maxlength="200" required /> {
<ModuleMessage Message="@Localizer["JobNotEditable"]" Type="MessageType.Warning" />
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" required disabled />
</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="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="name" class="form-control" @bind="@_name" maxlength="200" required disabled="@(!_editable)" />
<input id="type" class="form-control" @bind="@_jobType" readonly /> </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="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label> <div class="col-sm-9">
<div class="col-sm-9"> <select id="enabled" class="form-select" @bind="@_isEnabled" required disabled="@(!_editable)">
<select id="enabled" class="form-select" @bind="@_isEnabled" required> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="False">@SharedLocalizer["No"]</option>
<option value="False">@SharedLocalizer["No"]</option> </select>
</select> </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="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required disabled="@(!_editable)" />
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required /> <select id="runs-every" class="form-select" @bind="@_frequency" required disabled="@(!_editable)">
<select id="runs-every" class="form-select" @bind="@_frequency" required> <option value="m">@Localizer["Minute(s)"]</option>
<option value="m">@Localizer["Minute(s)"]</option> <option value="H">@Localizer["Hour(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option> <option value="d">@Localizer["Day(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option> <option value="w">@Localizer["Week(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option> <option value="M">@Localizer["Month(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option> <option value="O">@Localizer["Once"]</option>
<option value="O">@Localizer["Once"]</option> </select>
</select> </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="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required disabled="@(!_editable)" />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required /> </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="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="starting" type="date" class="form-control" @bind="@_startDate" disabled="@(!_editable)" />
<input id="starting" type="date" class="form-control" @bind="@_startDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" disabled="@(!_editable)" />
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" /> </div>
</div> </div>
</div> </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="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="ending" type="date" class="form-control" @bind="@_endDate" disabled="@(!_editable)" />
<input id="ending" type="date" class="form-control" @bind="@_endDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" disabled="@(!_editable)" />
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" /> </div>
</div> </div>
</div> </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="next" HelpText="The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job." ResourceKey="NextExecution">Next Execution: </Label>
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="next" class="form-control" @bind="@_nextDate" disabled />
<div class="row">
<div class="col">
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <br />
<br /> @if (_editable)
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button> {
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<br /> }
<br /> else
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo> {
</form> <button type="button" class="btn btn-danger" @onclick="DisableJob">@Localizer["Disable"]</button>
}
<NavLink class="btn btn-secondary ms-1" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private bool _initialized = false;
private bool _editable = true;
private int _jobId; private int _jobId;
private string _name = string.Empty; private string _name = string.Empty;
private string _jobType = string.Empty; private string _jobType = string.Empty;
@@ -113,26 +122,32 @@
private DateTime? _nextDate = null; private DateTime? _nextDate = null;
private DateTime? _nextTime = null; private DateTime? _nextTime = null;
private string createdby; private string createdby;
private DateTime createdon; private DateTime createdon;
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try await LoadJob();
{ }
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId); protected async Task LoadJob()
if (job != null) {
{ try
_name = job.Name; {
_jobType = job.JobType; _jobId = Int32.Parse(PageState.QueryString["id"]);
_isEnabled = job.IsEnabled.ToString(); Job job = await JobService.GetJobAsync(_jobId);
_interval = job.Interval.ToString(); if (job != null)
_frequency = job.Frequency; {
_startDate = UtcToLocal(job.StartDate); _editable = !job.IsEnabled && !job.IsExecuting;
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate); _startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate); _endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate); _endTime = UtcToLocal(job.EndDate);
@@ -140,70 +155,107 @@
_nextDate = UtcToLocal(job.NextExecution); _nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution); _nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn; modifiedon = job.ModifiedOn;
} }
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
private async Task SaveJob() _initialized = true;
{
if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate))
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
return;
} }
validated = true; catch (Exception ex)
var interop = new Interop(JSRuntime); {
if (await interop.FormValid(form)) await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
{ AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
var job = await JobService.GetJobAsync(_jobId); }
job.Name = _name; }
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue private async Task SaveJob()
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay)) {
: null; try
{
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.IsEnabled = bool.Parse(_isEnabled);
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue job.Frequency = _frequency;
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay)) if (job.Frequency == "O") // once
: null; {
job.RetentionHistory = int.Parse(_retentionHistory); job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
try job.StartDate = _startDate.HasValue && _startTime.HasValue
{ ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
job = await JobService.UpdateJobAsync(job); : null;
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl()); job.EndDate = _endDate.HasValue && _endTime.HasValue
} ? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
catch (Exception ex) : null;
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message); job.RetentionHistory = int.Parse(_retentionHistory);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
} if (!job.IsEnabled || Utilities.ValidateEffectiveExpiryDates(job.StartDate, job.EndDate))
} {
else if (!job.IsEnabled || (job.StartDate >= DateTime.UtcNow || job.StartDate == null))
{ {
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); job.NextExecution = null;
} job = await JobService.UpdateJobAsync(job);
} await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.StartDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
private async Task DisableJob()
{
try
{
var job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
if (job.IsExecuting)
{
AddModuleMessage(Localizer["Message.ExecutingError"], MessageType.Warning);
}
else
{
job.IsEnabled = false;
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
} }

View File

@@ -144,7 +144,6 @@ else
private string _code = string.Empty; private string _code = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override bool? Prerender => true;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
@@ -222,6 +221,14 @@ else
{ {
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning); AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
} }
else
{
if (_allowexternallogin && !_allowsitelogin)
{
// external login
ExternalLogin();
}
}
} }
} }
catch (Exception ex) catch (Exception ex)

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

@@ -130,7 +130,7 @@ else
private async Task LoadModuleDefinitions() private async Task LoadModuleDefinitions()
{ {
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_packages = await PackageService.GetPackageUpdatesAsync("module"); _packages = await PackageService.GetPackageUpdatesAsync("module");
} }

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,40 @@
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 approach - module settings updated by convention (ie. by calling a public method named "UpdateSettings" in settings component)
// this method should be removed however the ISettingsControl declaration was not added to the default module template until version 10.1.2
_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 +331,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,11 +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 IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService @inject IOutputCacheService CacheService
@inject ISiteGroupService SiteGroupService
@inject ISiteGroupMemberService SiteGroupMemberService
@if (_initialized) @if (_initialized)
{ {
@@ -28,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>
@@ -54,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">
@@ -207,13 +208,6 @@
</div> </div>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label> <Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -264,15 +258,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
} }
else else
{ {
@@ -348,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">
@@ -424,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>
@@ -453,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">
@@ -493,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 = "";
@@ -534,7 +643,6 @@
private string _togglesmtpclientsecret = string.Empty; private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty; private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
@@ -543,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;
@@ -580,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;
@@ -653,7 +771,6 @@
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"]; _togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty); _smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
} }
@@ -669,7 +786,7 @@
} }
// aliases // aliases
await GetAliases(); await LoadAliases();
// hosting model // hosting model
_rendermode = site.RenderMode; _rendermode = site.RenderMode;
@@ -693,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;
@@ -760,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
@@ -846,8 +969,18 @@
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true); settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true); settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
if (_smtpenabled == "True")
{
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
} }
//cookie consent //cookie consent
@@ -957,7 +1090,10 @@
{ {
try try
{ {
_smtpenabled = "True";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
@@ -972,6 +1108,14 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info); AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
await ScrollToPageTop(); await ScrollToPageTop();
@@ -988,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 == -1)
{ {
_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();
@@ -1086,4 +1235,205 @@
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)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupMemberId);
}
}
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

@@ -83,14 +83,14 @@ else
{ {
@if (_connection != "-") @if (_connection != "-")
{ {
@if (!string.IsNullOrEmpty(_tenant)) <div class="row mb-1 align-items-center">
{ <Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label> <input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
</div> </div>
</div>
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label> <Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -150,130 +150,149 @@ else
} }
@code { @code {
private string _connection = "-"; private string _connection = "-";
private Dictionary<string, object> _connections; private Dictionary<string, object> _connections;
private List<Tenant> _tenants; private List<Tenant> _tenants;
private List<Database> _databases; private List<Database> _databases;
private string _name = string.Empty; private string _name = string.Empty;
private string _databasetype = string.Empty; private string _databasetype = string.Empty;
private Type _databaseConfigType; private Type _databaseConfigType;
private object _databaseConfig; private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; } private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false; private bool _showConnectionString = false;
private string _tenant = string.Empty; private string _tenant = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
private string _connectionstringtype = "password"; private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty; private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty; private string _sql = string.Empty;
private List<Dictionary<string, string>> _results; private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_connections = await SystemService.GetSystemInfoAsync("connectionstrings"); _connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync(); _tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"]; _connectionstringtoggle = SharedLocalizer["ShowPassword"];
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message); await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private async void ConnectionChanged(ChangeEventArgs e) private async void ConnectionChanged(ChangeEventArgs e)
{ {
try try
{ {
_connection = (string)e.Value; _connection = (string)e.Value;
if (_connection != "-" && _connection != "+") if (_connection != "-" && _connection != "+")
{ {
_connectionstring = _connections[_connection].ToString(); _connectionstring = _connections[_connection].ToString();
_tenant = ""; _tenant = "";
_databasetype = ""; _databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection); var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
// hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name; _databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
} }
} else
else {
{ if (_connection.Contains(" ("))
if (_databases.Exists(item => item.IsDefault)) {
{ _databasetype = _connection.Substring(_connection.LastIndexOf(" (") + 2).Replace(")", "");
_databasetype = _databases.Find(item => item.IsDefault).Name; }
} else
else {
{ if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
}
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName; _databasetype = Constants.DefaultDBName;
} }
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message); await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private void DatabaseTypeChanged(ChangeEventArgs eventArgs) private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databasetype = (string)eventArgs.Value; _databasetype = (string)eventArgs.Value;
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error); AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
} }
} }
private void LoadDatabaseConfigComponent() private void LoadDatabaseConfigComponent()
{ {
var database = _databases.SingleOrDefault(d => d.Name == _databasetype); var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null) if (database != null)
{ {
_databaseConfigType = Type.GetType(database.ControlType); _databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder => DatabaseConfigComponent = builder =>
{ {
builder.OpenComponent(0, _databaseConfigType); builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
private void ShowConnectionString() private void ShowConnectionString()
{ {
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
_connectionstring = databaseConfigControl.GetConnectionString(); _connectionstring = databaseConfigControl.GetConnectionString();
} }
_showConnectionString = !_showConnectionString; _showConnectionString = !_showConnectionString;
} }
private async Task Add() private async Task Add()
{ {
var connectionstring = _connectionstring; var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl) if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
connectionstring = databaseConfigControl.GetConnectionString(); connectionstring = databaseConfigControl.GetConnectionString();
} }
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring)) if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{ {
var settings = new Dictionary<string, object>(); _name = _name + " (" + _databasetype +")";
var settings = new Dictionary<string, object>();
settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring); settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring);
await SystemService.UpdateSystemInfoAsync(settings); await SystemService.UpdateSystemInfoAsync(settings);
_connections = await SystemService.GetSystemInfoAsync("connectionstrings"); _connections = await SystemService.GetSystemInfoAsync("connectionstrings");

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

@@ -21,7 +21,7 @@
@if (_style == MessageStyle.Toast) @if (_style == MessageStyle.Toast)
{ {
<div class="app-modulemessage-toast bottom-0 end-0" @key="DateTime.UtcNow"> <div class="app-modulemessage-toast bottom-0 end-0">
<div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert"> <div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert">
@((MarkupString)Message) @((MarkupString)Message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@@ -86,11 +86,19 @@
/// <summary> /// <summary>
/// Determines if a tab should be visible based on user permissions. /// Determines if a tab should be visible based on user permissions.
/// Authorization hierarchy: /// Authorization follows this hierarchy:
/// 1. Host and Admin roles ALWAYS have access (bypass all checks) /// 1. Host tabs (Security == Host): Only users with Host role can access (Admins excluded)
/// 2. Check standard SecurityAccessLevel (View, Edit, etc.) /// 2. Admin users: Bypass all other checks (except Host restrictions)
/// 3. If RoleName specified AND user is not Admin/Host, check RoleName /// 3. SecurityAccessLevel check (null/Anonymous/View/Edit/Host):
/// 4. If PermissionName specified AND user is not Admin/Host, check PermissionName /// - null: No security level restriction (proceeds to step 4)
/// - Anonymous: No authentication required
/// - View/Edit: Requires corresponding module permission
/// - Host: Only Host role can access
/// 4. Additional RoleName requirement (if specified)
/// 5. Additional PermissionName requirement (if specified)
///
/// Important: When Security is null, RoleName and PermissionName checks STILL apply
/// (Security = null doesn't mean unrestricted, it means "no security level required")
/// </summary> /// </summary>
/// <param name="tabPanel">The tab panel to check authorization for</param> /// <param name="tabPanel">The tab panel to check authorization for</param>
/// <returns>True if user is authorized to see this tab, false otherwise</returns> /// <returns>True if user is authorized to see this tab, false otherwise</returns>
@@ -99,24 +107,40 @@
// Step 1: Check for Host-only restriction // Step 1: Check for Host-only restriction
if (tabPanel.Security == SecurityAccessLevel.Host) if (tabPanel.Security == SecurityAccessLevel.Host)
{ {
// Only Host users can access Host-level security tabs (Admin users are excluded)
return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
} }
// Step 2: Admin bypass all other restrictions // Step 2: Admin bypass all restrictions except Host
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
return true; return true;
} }
var authorized = false; // Step 3: If Security is null, check only RoleName and PermissionName
if (tabPanel.Security == null)
{
// Start with authorized = true for null security
bool isAuthorized = true;
// Step 3: Check standard SecurityAccessLevel // Only apply RoleName check if provided
if (!string.IsNullOrEmpty(tabPanel.RoleName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Only apply PermissionName check if provided
if (isAuthorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);
}
return isAuthorized;
}
// Handle other SecurityAccessLevel values
bool authorized = false; // Use different variable name or move declaration
switch (tabPanel.Security) switch (tabPanel.Security)
{ {
case null:
authorized = true;
break;
case SecurityAccessLevel.Anonymous: case SecurityAccessLevel.Anonymous:
authorized = true; authorized = true;
break; break;
@@ -131,13 +155,13 @@
break; break;
} }
// Step 4: Check RoleName if provided (additional requirement) // Step 4: Additional RoleName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName)) if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName))
{ {
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName); authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
} }
// Step 5: Check PermissionName if provided (additional requirement) // Step 5: Additional PermissionName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName)) if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{ {
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList); authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);

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.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Radzen.Blazor" Version="8.4.0" /> <PackageReference Include="Radzen.Blazor" Version="10.0.6" />
</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 Info?</value>
</data>
<data name="Pages.HelpText" xml:space="preserve">
<value>Specify if page information should be updated (ie. name, title, head content, body content settings)</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Site Info?</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>Specify if site information should be updated (ie. name, head content, body content, settings)</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 Info?</value>
</data>
<data name="Modules.HelpText" xml:space="preserve">
<value>Specify if module information should be updated (ie. title, header, footer settings)</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

@@ -139,7 +139,7 @@
<value>Error Updating Job</value> <value>Error Updating Job</value>
</data> </data>
<data name="Message.Required.JobInfo" xml:space="preserve"> <data name="Message.Required.JobInfo" xml:space="preserve">
<value>You Must Provide The Job Name, Type, Frequency, and Retention</value> <value>You Must Provide The Job Name, Frequency, and Retention</value>
</data> </data>
<data name="Name.HelpText" xml:space="preserve"> <data name="Name.HelpText" xml:space="preserve">
<value>Enter the job name</value> <value>Enter the job name</value>
@@ -154,7 +154,7 @@
<value>Select how often you want the job to run</value> <value>Select how often you want the job to run</value>
</data> </data>
<data name="Starting.HelpText" xml:space="preserve"> <data name="Starting.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should start executing</value> <value>Optionally enter the date and time when this job should start executing. If no date or time is specified, the job will execute immediately.</value>
</data> </data>
<data name="Ending.HelpText" xml:space="preserve"> <data name="Ending.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should stop executing</value> <value>Optionally enter the date and time when this job should stop executing</value>
@@ -163,7 +163,7 @@
<value>Number of log entries to retain for this job</value> <value>Number of log entries to retain for this job</value>
</data> </data>
<data name="NextExecution.HelpText" xml:space="preserve"> <data name="NextExecution.HelpText" xml:space="preserve">
<value>Optionally modify the date and time when this job should execute next</value> <value>The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job.</value>
</data> </data>
<data name="Type.Text" xml:space="preserve"> <data name="Type.Text" xml:space="preserve">
<value>Type: </value> <value>Type: </value>
@@ -193,6 +193,15 @@
<value>Execute Once</value> <value>Execute Once</value>
</data> </data>
<data name="Message.StartEndDateError" xml:space="preserve"> <data name="Message.StartEndDateError" xml:space="preserve">
<value>Start Date cannot be after End Date.</value> <value>The Start Date Cannot Be Later Than The End Date</value>
</data>
<data name="Message.StartDateError" xml:space="preserve">
<value>The Start Date Cannot Be Prior To The Current Date</value>
</data>
<data name="Message.ExecutingError" xml:space="preserve">
<value>The Job Is Currently Executing. You Must Wait Until The Job Has Completed.</value>
</data>
<data name="JobNotEditable" xml:space="preserve">
<value>The Job Cannot Be Modified As It Is Currently Enabled. You Must Disable The Job To Change Its Settings.</value>
</data> </data>
</root> </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

@@ -132,9 +132,6 @@
<data name="DefaultAdminContainer" xml:space="preserve"> <data name="DefaultAdminContainer" xml:space="preserve">
<value>Default Admin Container</value> <value>Default Admin Container</value>
</data> </data>
<data name="Smtp.Required.EnableNotificationJob" xml:space="preserve">
<value>** Please Note That SMTP Requires The Notification Job To Be Enabled In Scheduled Jobs</value>
</data>
<data name="Smtp.TestConfig" xml:space="preserve"> <data name="Smtp.TestConfig" xml:space="preserve">
<value>Test SMTP Configuration</value> <value>Test SMTP Configuration</value>
</data> </data>
@@ -322,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>
@@ -342,12 +339,6 @@
<data name="HomePage.Text" xml:space="preserve"> <data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value> <value>Home Page:</value>
</data> </data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve"> <data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value> <value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data> </data>
@@ -409,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>
@@ -504,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

@@ -148,7 +148,7 @@
<value>You Cannot Perform A System Update In A Development Environment</value> <value>You Cannot Perform A System Update In A Development Environment</value>
</data> </data>
<data name="Disclaimer.Text" xml:space="preserve"> <data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process.</value> <value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Enterprise Installations And Microsoft Azure Installations You Will Want To Use A Manual Upgrade Process.</value>
</data> </data>
<data name="Backup.Text" xml:space="preserve"> <data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value> <value>Backup Files?</value>

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 = "";
@@ -285,15 +303,16 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = PageState.Site.DefaultContainerType; _containerType = PageState.Site.DefaultContainerType;
_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.Split(',', StringSplitOptions.RemoveEmptyEntries).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);
} }
} }
private void CategoryChanged(ChangeEventArgs e) private void CategoryChanged(ChangeEventArgs e)
{ {
_category = (string)e.Value; _category = (string)e.Value;
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_moduleDefinitionName = "-"; _moduleDefinitionName = "-";
_message = ""; _message = "";
} }
@@ -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))
@@ -490,6 +523,11 @@
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, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break; 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;
} }
} }
@@ -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

@@ -83,7 +83,10 @@
get => ""; get => "";
set set
{ {
_showBanner = bool.Parse(value); if (!bool.TryParse(value, out _showBanner))
{
_showBanner = false;
}
_togglePostback = true; _togglePostback = 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 }); // determine if 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

@@ -16,7 +16,7 @@
{ {
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top") @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top")
{ {
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" /> <ModuleMessage @key="_messageVersionTop" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
} }
@DynamicComponent @DynamicComponent
@if (_progressIndicator) @if (_progressIndicator)
@@ -25,7 +25,7 @@
} }
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom") @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom")
{ {
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" /> <ModuleMessage @key="_messageVersionBottom" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
} }
} }
} }
@@ -52,6 +52,8 @@
private MessageStyle _messageStyle; private MessageStyle _messageStyle;
private bool _progressIndicator = false; private bool _progressIndicator = false;
private string _error; private string _error;
private string _messageVersionTop = Guid.NewGuid().ToString();
private string _messageVersionBottom = Guid.NewGuid().ToString();
[Parameter] [Parameter]
public SiteState SiteState { get; set; } public SiteState SiteState { get; set; }
@@ -143,6 +145,18 @@
_messageStyle = style; _messageStyle = style;
_progressIndicator = false; _progressIndicator = false;
if (style == MessageStyle.Toast && !string.IsNullOrEmpty(_messageContent))
{
if (_messagePosition == "top")
{
_messageVersionTop = Guid.NewGuid().ToString();
}
else if (_messagePosition == "bottom")
{
_messageVersionBottom = Guid.NewGuid().ToString();
}
}
StateHasChanged(); StateHasChanged();
} }
} }

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.3</ApplicationDisplayVersion> <ApplicationDisplayVersion>10.1.2</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.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" /> <PackageReference Include="System.Net.Http.Json" Version="10.0.5" />
<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.3</version> <version>10.1.2</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.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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.0" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Localization" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Http" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.4.0" exclude="Build,Analyzers" /> <dependency id="Radzen.Blazor" version="10.0.6" 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.3</version> <version>10.1.2</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.3/Oqtane.Framework.10.0.3.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.1.2/Oqtane.Framework.10.1.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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.3</version> <version>10.1.2</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.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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.1" exclude="Build,Analyzers" /> <dependency id="Oqtane.Client" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.5" 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.0.1" exclude="Build,Analyzers" /> <dependency id="Swashbuckle.AspNetCore" version="10.1.7" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" /> <dependency id="MailKit" version="4.15.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="Pomelo.EntityFrameworkCore.MySql" version="9.0.0" exclude="Build,Analyzers" /> <dependency id="MySql.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.0-rc.2" 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.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Data.Sqlite.Core" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.5" 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.3</version> <version>10.1.2</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,15 +12,15 @@
<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.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.3" exclude="Build,Analyzers" /> <dependency id="NodaTime" version="3.3.1" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>10.0.3</version> <version>10.1.2</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.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</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.3.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.2.Install.zip" -Force

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.3.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.2.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);
_language = ManageLocalization(alias, 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
{ {
@@ -294,6 +266,28 @@
private void HandlePageNotFound(Site site, Page page, Route route) private void HandlePageNotFound(Site site, Page page, Route route)
{ {
// filter
string useragent = (Context.Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.UserAgent] : "(none)";
var settings = Context.GetSiteSettings();
var filter = settings.GetValue("VisitorFilter", Constants.DefaultVisitorFilter);
foreach (string term in filter.ToLower().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
if (_remoteIPAddress.ToLower().Contains(term) || useragent.ToLower().Contains(term))
{
// handle not found request in static mode
if (_renderMode == RenderModes.Static)
{
NavigationManager.NotFound();
}
else
{
// redirect to 404 page
NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
}
return;
}
}
// referrer will only be set if the link originated externally // referrer will only be set if the link originated externally
string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : ""; string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : "";
@@ -550,8 +544,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,32 +565,27 @@
} }
} }
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>";
}
} }
} }
private void SetLocalizationCookie(string cookieValue)
{
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
};
Context.Response.Cookies.Append(
Shared.CookieRequestCultureProvider.DefaultCookieName,
cookieValue,
cookieOptions
);
}
private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action) private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action)
{ {
var resources = new List<Resource>(); var resources = new List<Resource>();
@@ -751,11 +738,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 +790,103 @@
} }
} }
} }
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 string ManageLocalization(Alias alias, Site site)
{
// get site culture (content localization)
var culture = site.CultureCode;
if (string.IsNullOrEmpty(culture))
{
// use fallback language
culture = LocalizationManager.GetDefaultCulture();
}
// determine if site is using content localization
var contentLocalization = site.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName));
// get user culture (ui localization)
var uiCulture = site.User?.CultureCode;
// if user has not specified a UI culture
if (string.IsNullOrEmpty(uiCulture))
{
// if not using content localization
if (!contentLocalization)
{
// use default language if specified otherwise use first language in collection
uiCulture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
}
else
{
// use site culture
uiCulture = culture;
}
}
else
{
// if not using content localization and UI culture is not supported by site
if (!contentLocalization && !site.Languages.Any(item => item.Code == uiCulture))
{
// use site culture
uiCulture = culture;
}
}
// get culture cookie
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (cultureCookie == null)
{
// 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
{
// validate the culture cookie
var requestCulture = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie);
if (requestCulture?.Culture.Name != culture || requestCulture?.UICulture.Name != uiCulture)
{
// convert to culture cookie format (ie. "c=en|uic=en") and save cookie
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture));
SetLocalizationCookie(cultureCookie);
}
}
return culture; // html element language attribute for page
}
private void SetLocalizationCookie(string cookieValue)
{
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
};
Context.Response.Cookies.Append(
Shared.CookieRequestCultureProvider.DefaultCookieName,
cookieValue,
cookieOptions
);
}
} }

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

@@ -264,7 +264,7 @@ namespace Oqtane.Controllers
_userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename))
{ {
// get content // get content
var content = _modules.ExportModule(moduleid); var content = _modules.ExportModule(module, "Export Module");
// get folder // get folder
var folder = _folders.GetFolder(folderid, false); var folder = _folders.GetFolder(folderid, false);
@@ -317,7 +317,7 @@ namespace Oqtane.Controllers
var module = _modules.GetModule(moduleid); var module = _modules.GetModule(moduleid);
if (ModelState.IsValid && module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit)) if (ModelState.IsValid && module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit))
{ {
success = _modules.ImportModule(moduleid, content); success = _modules.ImportModule(module, content, "Import Module");
if (success) if (success)
{ {
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Content Imported {ModuleId}", moduleid); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Content Imported {ModuleId}", moduleid);

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,14 @@
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 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
{ {
@@ -240,10 +238,11 @@ namespace Oqtane.Controllers
}; };
module = _modules.AddModule(module); module = _modules.AddModule(module);
string content = _modules.ExportModule(pm.ModuleId); // deep copy module content (includes settings)
string content = _modules.ExportModule(pm.Module, "Copy Page");
if (content != "") if (content != "")
{ {
_modules.ImportModule(module.ModuleId, content); _modules.ImportModule(module, content, "Copy Page");
} }
PageModule pagemodule = new PageModule(); PageModule pagemodule = new PageModule();
@@ -498,6 +497,107 @@ 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 page settings
var settings = _settings.GetSettings(EntityNames.Page, fromPage.PageId).ToList();
foreach (var setting in settings)
{
_settings.AddSetting(new Setting
{
EntityName = setting.EntityName,
EntityId = toPage.PageId,
SettingName = setting.SettingName,
SettingValue = setting.SettingValue,
IsPrivate = setting.IsPrivate
});
}
// 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);
// deep copy module content (includes settings)
string content = _modules.ExportModule(pm.Module, "Copy Page");
if (content != "")
{
_modules.ImportModule(module, content, "Copy Page");
}
}
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

@@ -128,7 +128,7 @@ namespace Oqtane.Controllers
} }
break; break;
default: default:
_configManager.AddOrUpdateSetting(key, value, false); _configManager.AddOrUpdateSetting(key, value, true);
break; break;
} }
} }

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

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases; using Oqtane.Databases;
namespace Oqtane.Database.MySQL namespace Oqtane.Database.MySQL
@@ -25,7 +26,7 @@ namespace Oqtane.Database.MySQL
public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name) public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name)
{ {
return table.Column<int>(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); return table.Column<int>(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn);
} }
public override string ConcatenateSql(params string[] values) public override string ConcatenateSql(params string[] values)
@@ -96,7 +97,7 @@ namespace Oqtane.Database.MySQL
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{ {
return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); return optionsBuilder.UseMySQL(connectionString);
} }
private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query) private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query)

View File

@@ -67,8 +67,8 @@ namespace Oqtane.Extensions
app.UseOutputCache(); app.UseOutputCache();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseAntiforgery();
app.UseNotFoundResponse(); app.UseNotFoundResponse();
app.UseAntiforgery();
// execute any IServerStartup logic // execute any IServerStartup logic
app.ConfigureOqtaneAssemblies(environment); app.ConfigureOqtaneAssemblies(environment);
@@ -153,28 +153,33 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app) public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app)
{ {
// set the route to be rendered on NavigationManager.NotFound()
const string notFoundRoute = "/404"; const string notFoundRoute = "/404";
app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true); app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true);
// middleware to determine if status code pages should be skipped
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
var path = context.Request.Path.Value ?? string.Empty; var path = context.Request.Path.Value ?? string.Empty;
if (string.IsNullOrEmpty(path) || ShouldSkipStatusCodeReExecution(path)) if (ShouldSkipStatusCodeReExecution(path))
{ {
var feature = context.Features.Get<IStatusCodePagesFeature>(); var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>();
feature?.Enabled = false; if (statusCodePagesFeature != null)
{
statusCodePagesFeature.Enabled = false;
}
} }
await next(); await next();
}); });
// middleware to rewrite the path for 404 status code responses on sites using subfolders
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
var feature = context.Features.Get<IStatusCodeReExecuteFeature>(); var statusCodeReExecuteFeature = context.Features.Get<IStatusCodeReExecuteFeature>();
var handled = false; if (statusCodeReExecuteFeature != null
if (feature != null
&& context.Response.StatusCode == (int)HttpStatusCode.NotFound && context.Response.StatusCode == (int)HttpStatusCode.NotFound
&& notFoundRoute.Equals(context.Request.Path.Value, StringComparison.OrdinalIgnoreCase)) && string.Equals(context.Request.Path.Value, notFoundRoute, StringComparison.OrdinalIgnoreCase))
{ {
var alias = context.GetAlias(); var alias = context.GetAlias();
if (!string.IsNullOrEmpty(alias?.Path)) if (!string.IsNullOrEmpty(alias?.Path))
@@ -183,20 +188,17 @@ namespace Oqtane.Extensions
context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}"); context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}");
try try
{ {
handled = true;
await next(); await next();
} }
finally finally
{ {
context.Request.Path = originalPath; context.Request.Path = originalPath;
} }
return;
} }
} }
if (!handled) await next();
{
await next();
}
}); });
return app; return app;
@@ -204,12 +206,16 @@ namespace Oqtane.Extensions
static bool ShouldSkipStatusCodeReExecution(string path) static bool ShouldSkipStatusCodeReExecution(string path)
{ {
return Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) || HasStaticFileExtension(path); // skip requests for framework resources, reserved routes, and static files
return path.StartsWith("/_framework/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/_content/", StringComparison.OrdinalIgnoreCase) ||
Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) ||
HasStaticFileExtension(path);
} }
static bool HasStaticFileExtension(string path) static bool HasStaticFileExtension(string path)
{ {
return !string.IsNullOrEmpty(Path.GetExtension(path)); return !string.IsNullOrEmpty(Path.GetExtension(path)) && Path.GetExtension(path).Length != 1;
} }
} }
} }

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

@@ -84,16 +84,13 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "") if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{ {
options.Events = new OpenIdConnectEvents options.Events.OnRedirectToIdentityProvider = context =>
{ {
OnRedirectToIdentityProvider = context => foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{ {
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(",")) context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
{
context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
}
return Task.FromResult(0);
} }
return Task.FromResult(0);
}; };
} }
} }
@@ -132,18 +129,15 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "") if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{ {
options.Events = new OAuthEvents options.Events.OnRedirectToAuthorizationEndpoint = context =>
{ {
OnRedirectToAuthorizationEndpoint = context => var url = context.RedirectUri;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{ {
var url = context.RedirectUri; url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
}
context.Response.Redirect(url);
return Task.FromResult(0);
} }
context.Response.Redirect(url);
return Task.FromResult(0);
}; };
} }
} }

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

@@ -198,41 +198,27 @@ namespace Oqtane.Infrastructure
{ {
case "m": // minutes case "m": // minutes
nextExecution = nextExecution.AddMinutes(job.Interval); nextExecution = nextExecution.AddMinutes(job.Interval);
if (nextExecution < DateTime.UtcNow) nextExecution = DateTime.UtcNow;
break; break;
case "H": // hours case "H": // hours
nextExecution = nextExecution.AddHours(job.Interval); nextExecution = nextExecution.AddHours(job.Interval);
if (nextExecution < DateTime.UtcNow) nextExecution = DateTime.UtcNow;
break; break;
case "d": // days case "d": // days
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddDays(job.Interval); nextExecution = nextExecution.AddDays(job.Interval);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "w": // weeks case "w": // weeks
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddDays(job.Interval * 7); nextExecution = nextExecution.AddDays(job.Interval * 7);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "M": // months case "M": // months
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddMonths(job.Interval); nextExecution = nextExecution.AddMonths(job.Interval);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "O": // one time case "O": // one time
break; break;
} }
if (nextExecution < DateTime.UtcNow)
{
nextExecution = DateTime.UtcNow;
}
return nextExecution; return nextExecution;
} }

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
@@ -160,7 +172,7 @@ namespace Oqtane.Infrastructure
var toEmail = notification.ToEmail ?? ""; var toEmail = notification.ToEmail ?? "";
var toName = notification.ToDisplayName ?? ""; var toName = notification.ToDisplayName ?? "";
// get sender and receiver information from user information if available // get sender from user information if "from" email or name is not specified and user id is available
if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null) if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null)
{ {
var user = userRepository.GetUser(notification.FromUserId.Value); var user = userRepository.GetUser(notification.FromUserId.Value);
@@ -170,6 +182,9 @@ namespace Oqtane.Infrastructure
fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName; fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName;
} }
} }
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
// get recipient from user information if "to" email or name is not specified and user id is available
if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null) if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null)
{ {
var user = userRepository.GetUser(notification.ToUserId.Value); var user = userRepository.GetUser(notification.ToUserId.Value);
@@ -181,30 +196,34 @@ namespace Oqtane.Infrastructure
} }
// create mailbox addresses // create mailbox addresses
MailboxAddress to = null;
MailboxAddress from = null; MailboxAddress from = null;
MailboxAddress to = null;
MailboxAddress replyTo = null;
var mailboxAddressValidationError = ""; var mailboxAddressValidationError = "";
// sender // always send from SMTP Sender
if ((settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True") && string.IsNullOrEmpty(fromEmail)) if (MailboxAddress.TryParse(settingRepository.GetSettingValue(settings, "SMTPSender", ""), out from))
{ {
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", ""); from.Name = fromName;
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
}
if (MailboxAddress.TryParse(fromEmail, out from))
{
from.Name = fromName;
} }
else else
{ {
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{settingRepository.GetSettingValue(settings, "SMTPSender", "")}&gt;";
}
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;"; // reply to
if (!string.IsNullOrEmpty(fromEmail) && fromEmail != from.Address)
{
if (MailboxAddress.TryParse(fromEmail, out replyTo))
{
replyTo.Name = fromName;
}
} }
// recipient // recipient
if (MailboxAddress.TryParse(toEmail, out to)) if (MailboxAddress.TryParse(toEmail, out to))
{ {
to.Name = toName; to.Name = toName;
} }
else else
{ {
@@ -218,6 +237,10 @@ namespace Oqtane.Infrastructure
MimeMessage mailMessage = new MimeMessage(); MimeMessage mailMessage = new MimeMessage();
mailMessage.From.Add(from); mailMessage.From.Add(from);
mailMessage.To.Add(to); mailMessage.To.Add(to);
if (replyTo != null)
{
mailMessage.ReplyTo.Add(replyTo);
}
// subject // subject
mailMessage.Subject = notification.Subject; mailMessage.Subject = notification.Subject;
@@ -267,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

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