Merge pull request #4468 from oqtane/dev

5.2.0 release
This commit is contained in:
Shaun Walker
2024-07-25 11:48:50 -04:00
committed by GitHub
150 changed files with 5466 additions and 1793 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Oqtane.Interfaces;
using Oqtane.Providers; using Oqtane.Providers;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
@ -51,6 +52,10 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IVisitorService, VisitorService>(); services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.TextAreaTextEditor>();
return services; return services;
} }
} }

View File

@ -0,0 +1,19 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Files
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "File Management",
Description = "File Management",
Version = Constants.Version,
Categories = "Admin",
ServerManagerType = "Oqtane.Modules.Admin.Files.Manager.FileManager, Oqtane.Server"
};
}
}

View File

@ -10,9 +10,7 @@
} }
else else
{ {
<ActionLink Action="Log" Class="btn btn-secondary" Text="View Logs" ResourceKey="ViewJobs" /> <button type="button" class="btn btn-secondary" @onclick="Refresh">@Localizer["Refresh.Text"]</button>
<button type="button" class="btn btn-secondary" @onclick="(async () => await Refresh())">@Localizer["Refresh.Text"]</button>
<br />
<br /> <br />
<Pager Items="@_jobs" SearchProperties="Name"> <Pager Items="@_jobs" SearchProperties="Name">
@ -44,6 +42,9 @@ else
</td> </td>
</Row> </Row>
</Pager> </Pager>
<br />
<ActionLink Action="Log" Class="btn btn-secondary" Text="View All Logs" ResourceKey="ViewLogs" />
} }
@code { @code {
@ -53,13 +54,18 @@ else
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_jobs = await JobService.GetJobsAsync(); await GetJobs();
if (_jobs.Count == 0) if (_jobs.Count == 0)
{ {
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
} }
} }
private async Task GetJobs()
{
_jobs = await JobService.GetJobsAsync();
}
private string DisplayStatus(bool isEnabled, bool isExecuting) private string DisplayStatus(bool isEnabled, bool isExecuting)
{ {
var status = string.Empty; var status = string.Empty;
@ -146,7 +152,7 @@ else
private async Task Refresh() private async Task Refresh()
{ {
_jobs = await JobService.GetJobsAsync(); await GetJobs();
StateHasChanged(); StateHasChanged();
} }
} }

View File

@ -10,6 +10,9 @@
} }
else else
{ {
<button type="button" class="btn btn-secondary" @onclick="Refresh">@Localizer["Refresh"]</button>
<br /><br />
<Pager Items="@_jobLogs"> <Pager Items="@_jobLogs">
<Header> <Header>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
@ -35,6 +38,11 @@ else
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{
await GetJobLogs();
}
private async Task GetJobLogs()
{ {
_jobLogs = await JobLogService.GetJobLogsAsync(); _jobLogs = await JobLogService.GetJobLogsAsync();
@ -67,4 +75,10 @@ else
return status; return status;
} }
private async Task Refresh()
{
await GetJobLogs();
StateHasChanged();
}
} }

View File

@ -93,6 +93,7 @@
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>()
{ {

View File

@ -125,7 +125,7 @@
<TabPanel Name="Upload" ResourceKey="Upload" Heading="Upload"> <TabPanel Name="Upload" ResourceKey="Upload" Heading="Upload">
<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" HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label> <Label Class="col-sm-3" HelpText="Upload one or more module packages." ResourceKey="Module">Module: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" />
</div> </div>

View File

@ -10,6 +10,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IPageModuleService PageModuleService @inject IPageModuleService PageModuleService
@inject IModuleService ModuleService @inject IModuleService ModuleService
@inject IPageService PageService
@if (_initialized) @if (_initialized)
{ {
@ -307,13 +308,15 @@
} }
// get distinct pages where module exists // get distinct pages where module exists
var distinctPageIds = PageState.Modules var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
var distinctPageIds = modules
.Where(md => md.ModuleDefinition?.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false) .Where(md => md.ModuleDefinition?.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false)
.Select(md => md.PageId) .Select(md => md.PageId)
.Distinct(); .Distinct();
// Filter and retrieve the corresponding pages // retrieve the pages which contain the module
_pagesWithModules = PageState.Pages var pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pagesWithModules = pages
.Where(pg => distinctPageIds.Contains(pg.PageId) && pg.IsDeleted == false) .Where(pg => distinctPageIds.Contains(pg.PageId) && pg.IsDeleted == false)
.ToList(); .ToList();

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions @namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IModuleDefinitionService ModuleDefinitionService @inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService @inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@ -70,7 +71,7 @@ else
} }
</td> </td>
<td> <td>
@if (context.AssemblyName == Constants.ClientId || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null) @if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{ {
<span>@SharedLocalizer["Yes"]</span> <span>@SharedLocalizer["Yes"]</span>
} }
@ -99,6 +100,7 @@ else
} }
@code { @code {
private List<Module> _modules;
private List<ModuleDefinition> _allModuleDefinitions; private List<ModuleDefinition> _allModuleDefinitions;
private List<ModuleDefinition> _moduleDefinitions; private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages; private List<Package> _packages;
@ -111,6 +113,7 @@ else
{ {
try try
{ {
_modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList();
await LoadModuleDefinitions(); await LoadModuleDefinitions();

View File

@ -3,6 +3,7 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject IPageService PageService
@inject IModuleService ModuleService @inject IModuleService ModuleService
@inject IPageModuleService PageModuleService @inject IPageModuleService PageModuleService
@inject IStringLocalizer<Settings> Localizer @inject IStringLocalizer<Settings> Localizer
@ -79,14 +80,16 @@
} }
else else
{ {
foreach (Page p in PageState.Pages) if (_pages != null)
{
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>
@ -154,10 +157,12 @@
private DateTime modifiedon; private DateTime modifiedon;
private DateTime? _effectivedate = null; private DateTime? _effectivedate = null;
private DateTime? _expirydate = null; private DateTime? _expirydate = null;
private List<Page> _pages;
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
SetModuleTitle(Localizer["ModuleSettings.Title"]); SetModuleTitle(Localizer["ModuleSettings.Title"]);
_module = ModuleState.ModuleDefinition.Name; _module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title; _title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; _moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
@ -173,7 +178,7 @@
modifiedon = ModuleState.ModifiedOn; modifiedon = ModuleState.ModifiedOn;
_effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate); _effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate); _expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (ModuleState.ModuleDefinition != null) if (ModuleState.ModuleDefinition != null)
{ {

View File

@ -26,7 +26,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required> <select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option> <option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in PageState.Pages) @foreach (Page page in _pages)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList))
{ {
@ -213,6 +213,7 @@
private bool validated = false; private bool validated = false;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private int _pageId; private int _pageId;
private string _name; private string _name;
private string _parentid = "-1"; private string _parentid = "-1";
@ -243,6 +244,8 @@
{ {
try try
{ {
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (PageState.QueryString.ContainsKey("id")) if (PageState.QueryString.ContainsKey("id"))
{ {
_pageId = Int32.Parse(PageState.QueryString["id"]); _pageId = Int32.Parse(PageState.QueryString["id"]);
@ -263,7 +266,7 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
@ -293,7 +296,7 @@
{ {
_parentid = (string)e.Value; _parentid = (string)e.Value;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
@ -371,7 +374,7 @@
} }
else else
{ {
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId); Page parent = _pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty) if (parent.Path == string.Empty)
{ {
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path); page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
@ -382,7 +385,6 @@
} }
} }
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == page.Path)) if (_pages.Any(item => item.Path == page.Path))
{ {
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
@ -402,11 +404,11 @@
page.Order = 0; page.Order = 0;
break; break;
case "<": case "<":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); child = _pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order - 1; page.Order = child.Order - 1;
break; break;
case ">": case ">":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); child = _pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order + 1; page.Order = child.Order + 1;
break; break;
case ">>": case ">>":
@ -449,7 +451,7 @@
await logger.LogInformation("Page Added {Page}", page); await logger.LogInformation("Page Added {Page}", page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{ {
NavigationManager.NavigateTo(page.Path, true); // redirect to page added and reload NavigationManager.NavigateTo(NavigateUrl(page.Path), true); // redirect to page added and reload
} }
else else
{ {

View File

@ -31,7 +31,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required> <select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option> <option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in PageState.Pages) @foreach (Page page in _pages)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId)
{ {
@ -302,6 +302,7 @@
private bool validated = false; private bool validated = false;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private int _pageId; private int _pageId;
private string _name; private string _name;
private string _currentparentid; private string _currentparentid;
@ -345,6 +346,7 @@
{ {
try try
{ {
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pageId = Int32.Parse(PageState.QueryString["id"]); _pageId = Int32.Parse(PageState.QueryString["id"]);
_page = await PageService.GetPageAsync(_pageId); _page = await PageService.GetPageAsync(_pageId);
_icons = await SystemService.GetIconsAsync(); _icons = await SystemService.GetIconsAsync();
@ -360,10 +362,10 @@
else else
{ {
_parentid = _page.ParentId.ToString(); _parentid = _page.ParentId.ToString();
_parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId); _parent = _pages.FirstOrDefault(item => item.PageId == _page.ParentId);
} }
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in PageState.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 && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
@ -415,7 +417,7 @@
_permissions = _page.PermissionList; _permissions = _page.PermissionList;
// page modules // page modules
_pageModules = PageState.Modules.Where(m => m.PageId == _page.PageId).ToList(); _pageModules = PageState.Modules;
// audit // audit
_createdby = _page.CreatedBy; _createdby = _page.CreatedBy;
@ -447,7 +449,7 @@
{ {
_parentid = (string)e.Value; _parentid = (string)e.Value;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{ {
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
@ -549,7 +551,7 @@
} }
else else
{ {
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId); Page parent = _pages.FirstOrDefault(item => item.PageId == _page.ParentId);
if (parent.Path == string.Empty) if (parent.Path == string.Empty)
{ {
_page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path); _page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
@ -560,7 +562,6 @@
} }
} }
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == _page.Path && item.PageId != _page.PageId)) if (_pages.Any(item => item.Path == _page.Path && item.PageId != _page.PageId))
{ {
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
@ -582,11 +583,11 @@
_page.Order = 0; _page.Order = 0;
break; break;
case "<": case "<":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid); child = _pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) _page.Order = child.Order - 1; if (child != null) _page.Order = child.Order - 1;
break; break;
case ">": case ">":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid); child = _pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) _page.Order = child.Order + 1; if (child != null) _page.Order = child.Order + 1;
break; break;
case ">>": case ">>":

View File

@ -5,11 +5,11 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.Pages != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) @if (_pages != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
<ActionLink Action="Add" Text="Add Page" ResourceKey="AddPage" /> <ActionLink Action="Add" Text="Add Page" ResourceKey="AddPage" />
<Pager Items="@PageState.Pages.Where(item => !item.IsDeleted)" SearchProperties="Name"> <Pager Items="@_pages.Where(item => !item.IsDeleted)" SearchProperties="Name">
<Header> <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>
@ -28,6 +28,21 @@
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private List<Page> _pages;
protected override async Task OnInitializedAsync()
{
try
{
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Pages {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error);
}
}
private async Task DeletePage(Page page) private async Task DeletePage(Page page)
{ {
try try

View File

@ -0,0 +1,102 @@
@namespace Oqtane.Modules.Admin.Search
@inherits ModuleBase
@inject ISettingService SettingService
@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="searchprovider" HelpText="Specify the search provider for this site" ResourceKey="SearchProvider">Search Provider: </Label>
<div class="col-sm-9">
<input id="searchprovider" class="form-control" @bind="@_searchProvider" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Specify if search indexing is enabled" ResourceKey="Enabled">Indexing Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_enabled">
<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="lastindexedon" HelpText="The date/time which the site was last indexed on" ResourceKey="LastIndexedOn">Last Indexed: </Label>
<div class="col-sm-9">
<input id="lastindexedon" class="form-control" @bind="@_lastIndexedOn" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ignorepages" HelpText="Comma delimited list of pages which should be ignored (based on their path)" ResourceKey="IgnorePages">Ignore Pages: </Label>
<div class="col-sm-9">
<textarea id="ignorepages" class="form-control" @bind="@_ignorePages" rows="3"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ignoreentities" HelpText="Comma delimited list of entities which should be ignored" ResourceKey="IgnoreEntities">Ignore Entities: </Label>
<div class="col-sm-9">
<textarea id="ignoreentities" class="form-control" @bind="@_ignoreEntities" rows="3"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumwordlength" HelpText="Minimum length of a word to be indexed" ResourceKey="MinimumWordLength">Word Length: </Label>
<div class="col-sm-9">
<input id="minimumwordlength" class="form-control" type="number" min="0" step="1" @bind="@_minimumWordLength" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ignorewords" HelpText="Comma delimited list of words which should be ignored" ResourceKey="IgnoreWords">Ignore Words: </Label>
<div class="col-sm-9">
<textarea id="ignorewords" class="form-control" @bind="@_ignoreWords" rows="3"></textarea>
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<br /><br />
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private string _searchProvider;
private string _enabled;
private string _lastIndexedOn;
private string _ignorePages;
private string _ignoreEntities;
private string _minimumWordLength;
private string _ignoreWords;
protected override async Task OnInitializedAsync()
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_searchProvider = SettingService.GetSetting(settings, "Search_SearchProvider", Constants.DefaultSearchProviderName);
_enabled = SettingService.GetSetting(settings, "Search_Enabled", "True");
_lastIndexedOn = SettingService.GetSetting(settings, "Search_LastIndexedOn", "");
_ignorePages = SettingService.GetSetting(settings, "Search_IgnorePages", "");
_ignoreEntities = SettingService.GetSetting(settings, "Search_IgnoreEntities", "");
_minimumWordLength = SettingService.GetSetting(settings, "Search_MininumWordLength", "3");
_ignoreWords = SettingService.GetSetting(settings, "Search_IgnoreWords", "");
}
private async Task Save()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "Search_SearchProvider", _searchProvider);
settings = SettingService.SetSetting(settings, "Search_Enabled", _enabled, true);
settings = SettingService.SetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn, true);
settings = SettingService.SetSetting(settings, "Search_IgnorePages", _ignorePages, true);
settings = SettingService.SetSetting(settings, "Search_IgnoreEntities", _ignoreEntities, true);
settings = SettingService.SetSetting(settings, "Search_MininumWordLength", _minimumWordLength, true);
settings = SettingService.SetSetting(settings, "Search_IgnoreWords", _ignoreWords, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Save"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Search Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}

View File

@ -0,0 +1,141 @@
@using Oqtane.Services
@using System.Net
@namespace Oqtane.Modules.Admin.SearchResults
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISearchResultsService SearchResultsService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="search-result-container">
<div class="row">
<div class="col">
<form method="post" @formname="SearchResultsForm" @onsubmit="Search" data-enhance>
<div class="input-group mb-3">
<span class="input-group-text">@Localizer["SearchLabel"]</span>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" class="form-control shadow-none" maxlength="50"
aria-label="Keywords"
placeholder="@Localizer["SearchPlaceholder"]"
@bind="@_keywords">
<button class="btn btn-primary" type="submit">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Reset"]</a>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 mb-3">
@if (_loading)
{
<div class="app-progress-indicator"></div>
}
else
{
@if (_searchResults != null && _searchResults.Results != null)
{
if (_searchResults.Results.Any())
{
<Pager Items="@_searchResults?.Results"
Format="Grid"
Columns="1"
Toolbar="Bottom"
Parameters="@($"q={_keywords}")">
<Row>
<div class="search-item mb-2">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4>
<p class="mb-0 text-muted">@((MarkupString)context.Snippet)</p>
</div>
</Row>
</Pager>
}
else
{
<div class="alert alert-info show mt-3" role="alert">
@Localizer["NoResult"]
</div>
}
}
<div class="clearfix"></div>
}
</div>
</div>
</div>
@code {
public override string RenderMode => RenderModes.Static;
private string _includeEntities;
private string _excludeEntities;
private string _fromDate;
private string _toDate;
private string _pageSize;
private string _sortField;
private string _sortOrder;
private string _bodyLength;
private string _keywords;
private SearchResults _searchResults;
private bool _loading;
[SupplyParameterFromForm(FormName = "SearchResultsForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override async Task OnInitializedAsync()
{
_includeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_IncludeEntities", "");
_excludeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ExcludeEntities", "");
_fromDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_FromDate", DateTime.MinValue.ToString());
_toDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ToDate", DateTime.MaxValue.ToString());
_pageSize = SettingService.GetSetting(ModuleState.Settings, "SearchResults_PageSize", int.MaxValue.ToString());
_sortField = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortField", "Relevance");
_sortOrder = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortOrder", "Descending");
_bodyLength = SettingService.GetSetting(ModuleState.Settings, "SearchResults_BodyLength", "255");
if (_keywords == null && PageState.QueryString.ContainsKey("q"))
{
_keywords = WebUtility.UrlDecode(PageState.QueryString["q"]);
await PerformSearch();
}
}
private void Search()
{
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, $"page=1&q={_keywords}"));
}
private async Task PerformSearch()
{
_loading = true;
StateHasChanged();
if (!string.IsNullOrEmpty(_keywords))
{
var searchQuery = new SearchQuery
{
SiteId = PageState.Site.SiteId,
Alias = PageState.Alias,
Keywords = _keywords,
IncludeEntities = _includeEntities,
ExcludeEntities = _excludeEntities,
FromDate = (!string.IsNullOrEmpty(_fromDate)) ? DateTime.Parse(_fromDate) : DateTime.MinValue,
ToDate = (!string.IsNullOrEmpty(_toDate)) ? DateTime.Parse(_toDate) : DateTime.MaxValue,
PageSize = (!string.IsNullOrEmpty(_pageSize)) ? int.Parse(_pageSize) : int.MaxValue,
PageIndex = 0,
SortField = (!string.IsNullOrEmpty(_sortField)) ? (SearchSortField)Enum.Parse(typeof(SearchSortField), _sortField) : SearchSortField.Relevance,
SortOrder = (!string.IsNullOrEmpty(_sortOrder)) ? (SearchSortOrder)Enum.Parse(typeof(SearchSortOrder), _sortOrder) : SearchSortOrder.Descending,
BodyLength = (!string.IsNullOrEmpty(_bodyLength)) ? int.Parse(_bodyLength) : 255
};
_searchResults = await SearchResultsService.GetSearchResultsAsync(searchQuery);
}
else
{
AddModuleMessage(Localizer["NoCriteria"], MessageType.Info, "bottom");
}
_loading = false;
StateHasChanged();
}
}

View File

@ -0,0 +1,19 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.SearchResults
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Search Results",
Description = "Search Results",
Categories = "Admin",
Version = Constants.Version,
SettingsType = "Oqtane.Modules.Admin.SearchResults.Settings, Oqtane.Client"
};
}
}

View File

@ -0,0 +1,123 @@
@namespace Oqtane.Modules.Admin.SearchResults
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="includeentities" ResourceKey="IncludeEntities" ResourceType="@resourceType" HelpText="Comma delimited list of entities to include in the search results. By default all entities will be included.">Include Entities: </Label>
<div class="col-sm-9">
<input id="includeentities" type="text" class="form-control" @bind="@_includeEntities" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="excludeentities" ResourceKey="ExcludeEntities" ResourceType="@resourceType" HelpText="Comma delimited list of entities to exclude from search results. By default no entities will be excluded.">Exclude Entities: </Label>
<div class="col-sm-9">
<input id="excludeentities" class="form-control" @bind="@_excludeEntities" required></input>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="daterange" ResourceKey="DateRange" ResourceType="@resourceType" HelpText="Enter the date range for search results. The default includes all content.">Date Range: </Label>
<div class="col-sm-9">
<div class="input-group">
<input type="date" class="form-control" @bind="@_fromDate" />
<span class="input-group-text">@Localizer["To"]</span>
<input type="date" class="form-control" @bind="@_toDate" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pagesize" ResourceKey="PageSize" ResourceType="@resourceType" HelpText="The maximum number of search results to retrieve. The default is unlimited.">Page Size: </Label>
<div class="col-sm-9">
<input id="pagesize" type="text" class="form-control" @bind="@_pageSize" />
</div>
</div>
<hr />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sortfield" ResourceKey="SortField" ResourceType="@resourceType" HelpText="Specify the default sort field">Sort By: </Label>
<div class="col-sm-9">
<select id="softfield" class="form-select" @bind="@_sortField">
<option value="Relevance">@Localizer["Relevance"]</option>
<option value="Title">@Localizer["Title"]</option>
<option value="LastModified">@Localizer["LastModified"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sortorder" ResourceKey="SortOrder" ResourceType="@resourceType" HelpText="Specify the default sort order">Sort Order: </Label>
<div class="col-sm-9">
<select id="softorder" class="form-select" @bind="@_sortOrder">
<option value="Ascending">@Localizer["Ascending"]</option>
<option value="Descending">@Localizer["Descending"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodylength" ResourceKey="BodyLength" ResourceType="@resourceType" HelpText="The number of characters displayed for each search result summary. The default is 255 characters.">Body Size: </Label>
<div class="col-sm-9">
<input id="bodylength" type="text" class="form-control" @bind="@_bodyLength" />
</div>
</div>
</div>
</form>
@code {
private string resourceType = "Oqtane.Modules.Admin.SearchResults.Settings, Oqtane.Client"; // for localization
private ElementReference form;
private bool validated = false;
private string _includeEntities;
private string _excludeEntities;
private DateTime? _fromDate = null;
private DateTime? _toDate = null;
private string _pageSize;
private string _sortField;
private string _sortOrder;
private string _bodyLength;
protected override void OnInitialized()
{
try
{
_includeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_IncludeEntities", "");
_excludeEntities = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ExcludeEntities", "");
var fromDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_FromDate", "");
_fromDate = (string.IsNullOrEmpty(fromDate)) ? null : DateTime.Parse(fromDate);
var toDate = SettingService.GetSetting(ModuleState.Settings, "SearchResults_ToDate", "");
_toDate = (string.IsNullOrEmpty(toDate)) ? null : DateTime.Parse(toDate);
_pageSize = SettingService.GetSetting(ModuleState.Settings, "SearchResults_PageSize", "");
_sortField = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortField", "Relevance");
_sortOrder = SettingService.GetSetting(ModuleState.Settings, "SearchResults_SortOrder", "Descending");
_bodyLength = SettingService.GetSetting(ModuleState.Settings, "SearchResults_BodyLength", "255");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "SearchResults_IncludeEntities", _includeEntities);
settings = SettingService.SetSetting(settings, "SearchResults_ExcludeEntities", _excludeEntities);
settings = SettingService.SetSetting(settings, "SearchResults_From", _fromDate.ToString());
settings = SettingService.SetSetting(settings, "SearchResults_To", _toDate.ToString());
settings = SettingService.SetSetting(settings, "SearchResults_PageSize", _pageSize);
settings = SettingService.SetSetting(settings, "SearchResults_SortField", _sortField);
settings = SettingService.SetSetting(settings, "SearchResults_SortOrder", _sortOrder);
settings = SettingService.SetSetting(settings, "SearchResults_BodyLength", _bodyLength);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -1,13 +1,16 @@
@namespace Oqtane.Modules.Admin.Site @namespace Oqtane.Modules.Admin.Site
@inherits ModuleBase @inherits ModuleBase
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.Extensions.DependencyInjection
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISiteService SiteService @inject ISiteService SiteService
@inject IPageService PageService
@inject ITenantService TenantService @inject ITenantService TenantService
@inject IDatabaseService DatabaseService @inject IDatabaseService DatabaseService
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -27,7 +30,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required> <select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option> <option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (Page page in PageState.Pages) @foreach (Page page in _pages)
{ {
if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone)) if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{ {
@ -74,7 +77,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label> <Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_ImageFiles" @ref="_logofilemanager" /> <FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -125,18 +128,32 @@
</div> </div>
</div> </div>
</Section> </Section>
<Section Name="FileExtensions" Heading="File Extensions" ResourceKey="FileExtensions"> <Section Name="Functionality" Heading="Functionality" ResourceKey="Functionality">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="textEditor" HelpText="Select the text editor for the site" ResourceKey="TextEditor">Text Editor: </Label>
<div class="col-sm-9">
<select id="textEditor" class="form-select" @bind="@_textEditor" required>
@if (_textEditors != null)
{
@foreach (var textEditor in _textEditors)
{
<option value="@textEditor.Value">@textEditor.Key</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="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label> <Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_ImageFiles" /> <input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</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="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label> <Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_UploadableFiles" /> <input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div> </div>
</div> </div>
</div> </div>
@ -394,12 +411,15 @@
private bool _initialized = false; private bool _initialized = false;
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private string _name = string.Empty; private string _name = string.Empty;
private string _homepageid = "-"; private string _homepageid = "-";
private string _isdeleted; private string _isdeleted;
private string _sitemap = ""; private string _sitemap = "";
private string _siteguid = ""; private string _siteguid = "";
private string _version = ""; private string _version = "";
private int _logofileid = -1; private int _logofileid = -1;
private FileManager _logofilemanager; private FileManager _logofilemanager;
private int _faviconfileid = -1; private int _faviconfileid = -1;
@ -407,8 +427,15 @@
private string _themetype = ""; private string _themetype = "";
private string _containertype = ""; private string _containertype = "";
private string _admincontainertype = ""; private string _admincontainertype = "";
private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = "";
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "False";
@ -419,25 +446,28 @@
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False"; private string _smtprelay = "False";
private string _smtpenabled = "True"; private string _smtpenabled = "True";
private string _ImageFiles = string.Empty;
private string _UploadableFiles = string.Empty;
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
private int _pwaappiconfileid = -1; private int _pwaappiconfileid = -1;
private FileManager _pwaappiconfilemanager; private FileManager _pwaappiconfilemanager;
private int _pwasplashiconfileid = -1; private int _pwasplashiconfileid = -1;
private FileManager _pwasplashiconfilemanager; private FileManager _pwasplashiconfilemanager;
private List<Alias> _aliases; private List<Alias> _aliases;
private int _aliasid = -1; private int _aliasid = -1;
private string _aliasname; private string _aliasname;
private string _defaultalias; private string _defaultalias;
private string _rendermode = RenderModes.Interactive; private string _rendermode = RenderModes.Interactive;
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 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;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
@ -454,6 +484,10 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name; _name = site.Name;
if (site.HomePageId != null) if (site.HomePageId != null)
{ {
@ -480,23 +514,23 @@
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
// functionality
var textEditors = ServiceProvider.GetServices<ITextEditor>();
foreach (var textEditor in textEditors)
{
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
}
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
_bodycontent = site.BodyContent; _bodycontent = site.BodyContent;
// PWA
_pwaisenabled = site.PwaIsEnabled.ToString();
if (site.PwaAppIconFileId != null)
{
_pwaappiconfileid = site.PwaAppIconFileId.Value;
}
if (site.PwaSplashIconFileId != null)
{
_pwasplashiconfileid = site.PwaSplashIconFileId.Value;
}
// SMTP // SMTP
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty); _smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty); _smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
@ -508,11 +542,16 @@
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True"); _smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// file extensions // PWA
_ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _pwaisenabled = site.PwaIsEnabled.ToString();
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; if (site.PwaAppIconFileId != null)
_UploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles); {
_UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles; _pwaappiconfileid = site.PwaAppIconFileId.Value;
}
if (site.PwaSplashIconFileId != null)
{
_pwasplashiconfileid = site.PwaSplashIconFileId.Value;
}
// aliases // aliases
await GetAliases(); await GetAliases();
@ -688,9 +727,10 @@
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true); settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
// file extensions // functionality
settings = SettingService.SetSetting(settings, "ImageFiles", (_ImageFiles != Constants.ImageFiles) ? _ImageFiles.Replace(" ", "") : "", false); settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_UploadableFiles != Constants.UploadableFiles) ? _UploadableFiles.Replace(" ", "") : "", false); settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);

View File

@ -125,7 +125,7 @@
<TabPanel Name="Upload" ResourceKey="Upload"> <TabPanel Name="Upload" ResourceKey="Upload">
<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" HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation." ResourceKey="Theme">Theme: </Label> <Label Class="col-sm-3" HelpText="Upload one or more theme packages." ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" />
</div> </div>

View File

@ -162,7 +162,6 @@ else
{ {
Text = Action; Text = Action;
} }
_openText = Text;
if (string.IsNullOrEmpty(Class)) if (string.IsNullOrEmpty(Class))
{ {
@ -181,7 +180,10 @@ else
_openText = string.Empty; _openText = string.Empty;
} }
if (!IconName.Contains(" ")) // Check if IconName starts with "oi oi-"
bool startsWithOiOi = IconName.StartsWith("oi oi-");
if (!startsWithOiOi && !IconName.Contains(" "))
{ {
IconName = "oi oi-" + IconName; IconName = "oi oi-" + IconName;
} }
@ -193,6 +195,7 @@ else
Header = Localize(nameof(Header), Header); Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message); Message = Localize(nameof(Message), Message);
_openText = Text;
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList; _permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized(); _authorized = IsAuthorized();

View File

@ -145,7 +145,10 @@
if (!string.IsNullOrEmpty(IconName)) if (!string.IsNullOrEmpty(IconName))
{ {
if (!IconName.Contains(" ")) // Check if IconName starts with "oi oi-"
bool startsWithOiOi = IconName.StartsWith("oi oi-");
if (!startsWithOiOi && !IconName.Contains(" "))
{ {
IconName = "oi oi-" + IconName; IconName = "oi oi-" + IconName;
} }

View File

@ -37,8 +37,6 @@
} }
protected void OnChange(ChangeEventArgs e) protected void OnChange(ChangeEventArgs e)
{
if (!string.IsNullOrEmpty(e.Value.ToString()))
{ {
Value = e.Value.ToString(); Value = e.Value.ToString();
if (ValueChanged.HasDelegate) if (ValueChanged.HasDelegate)
@ -47,4 +45,3 @@
} }
} }
} }
}

View File

@ -11,7 +11,7 @@
@if (!string.IsNullOrEmpty(SearchProperties)) @if (!string.IsNullOrEmpty(SearchProperties))
{ {
<form autocomplete="off"> <form autocomplete="off">
<div class="input-group my-3"> <div class="input-group my-3 @SearchBoxClass">
<input type="text" id="pagersearch" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" /> <input type="text" id="pagersearch" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button> <button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button> <button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
@ -74,7 +74,7 @@
{ {
<form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance> <form method="post" autocomplete="off" @formname="PagerForm" @onsubmit="Search" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="input-group my-3"> <div class="input-group my-3 @SearchBoxClass">
<input type="text" id="pagersearch" name="_search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" /> <input type="text" id="pagersearch" name="_search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @bind="@_search" />
<button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button> <button type="submit" class="btn btn-primary">@SharedLocalizer["Search"]</button>
<a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a> <a class="btn btn-secondary" href="@PageUrl(1, "")">@SharedLocalizer["Reset"]</a>
@ -359,6 +359,9 @@
[Parameter] [Parameter]
public string SearchProperties { get; set; } // comma delimited list of property names to include in search public string SearchProperties { get; set; } // comma delimited list of property names to include in search
[Parameter]
public string SearchBoxClass { get; set; } // class for Search box
[Parameter] [Parameter]
public string Parameters { get; set; } // optional - querystring parameters in the form of "id=x&name=y" used in static render mode public string Parameters { get; set; } // optional - querystring parameters in the form of "id=x&name=y" used in static render mode

View File

@ -4,11 +4,11 @@ using System.Threading.Tasks;
namespace Oqtane.Modules.Controls namespace Oqtane.Modules.Controls
{ {
public class RichTextEditorInterop public class QuillEditorInterop
{ {
private readonly IJSRuntime _jsRuntime; private readonly IJSRuntime _jsRuntime;
public RichTextEditorInterop(IJSRuntime jsRuntime) public QuillEditorInterop(IJSRuntime jsRuntime)
{ {
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }

View File

@ -0,0 +1,578 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
@inject ISettingService SettingService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<QuillJSTextEditor> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="quill-text-editor">
<TabStrip ActiveTab="@_activetab">
@if (_allowRichText)
{
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (_allowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (!string.IsNullOrEmpty(_toolbarContent))
{
@((MarkupString)_toolbarContent)
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement"></div>
</div>
</div>
</TabPanel>
}
@if (_allowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (_allowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
@if (_allowSettings)
{
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="quill-text-editor-settings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="Scope" ResourceKey="Scope" ResourceType="@resourceType" HelpText="Specify if settings are scoped to the module or site">Scope: </Label>
<div class="col-sm-9">
<select id="Scope" class="form-select" value="@_scopeSetting" @onchange="(e => ScopeChanged(e))">
<option value="Module">@SharedLocalizer["Module"]</option>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<option value="Site">@SharedLocalizer["Site"]</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="AllowRichText" ResourceKey="AllowRichText" ResourceType="@resourceType" HelpText="Specify if editors can use the Rich Text Editor">Rich Text Editor? </Label>
<div class="col-sm-9">
<select id="AllowRichText" class="form-select" @bind="@_allowRichTextSetting" required>
<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="AllowRawHtml" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify if editors can use the Raw HTML Editor">Raw HTML Editor? </Label>
<div class="col-sm-9">
<select id="AllowRawHtml" class="form-select" @bind="@_allowRawHtmlSetting" required>
<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="AllowFileManagement" ResourceKey="AllowFileManagement" ResourceType="@resourceType" HelpText="Specify if editors can upload and insert images">Insert Images? </Label>
<div class="col-sm-9">
<select id="AllowFileManagement" class="form-select" @bind="@_allowFileManagementSetting" required>
<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="Theme" ResourceKey="Theme" ResourceType="@resourceType" HelpText="Specify the Rich Text Editor's theme">Theme: </Label>
<div class="col-sm-9">
<input type="text" id="Theme" class="form-control" @bind="_themeSetting" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="DebugLevel" ResourceKey="DebugLevel" ResourceType="@resourceType" HelpText="Specify the Debug Level">Debug Level: </Label>
<div class="col-sm-9">
<select id="DebugLevel" class="form-select" @bind="_debugLevelSetting">
@foreach (var level in _debugLevels)
{
<option value="@level">@level</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ToolbarContent" ResourceKey="ToolbarContent" ResourceType="@resourceType" HelpText="Specify any toolbar content to customize the Rich Text Editor">Toolbar Content: </Label>
<div class="col-sm-9">
<textarea id="ToolbarContent" class="form-control" @bind="_toolbarContentSetting" rows="3" />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="col-sm-9 offset-sm-3">
<button type="button" class="btn btn-success" @onclick="@(async () => await UpdateSettings())">@Localizer["SaveSettings"]</button>
</div>
</div>
</div>
</TabPanel>
}
</TabStrip>
</div>
@code {
public string Name => "QuillJS";
private string resourceType = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
private bool _settingsLoaded;
private bool _initialized = false;
private QuillEditorInterop _interop;
private FileManager _fileManager;
private string _activetab = "Rich";
private bool _allowSettings = false;
private bool _allowFileManagement = false;
private bool _allowRawHtml = false;
private bool _allowRichText = false;
private string _theme = "snow";
private string _debugLevel = "info";
private string _toolbarContent = string.Empty;
private string _scopeSetting = "Module";
private string _allowFileManagementSetting = "False";
private string _allowRawHtmlSetting = "False";
private string _allowRichTextSetting = "False";
private string _themeSetting = "snow";
private string _debugLevelSetting = "info";
private string _toolbarContentSetting = string.Empty;
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtmlid = "RawHtmlEditor_" + Guid.NewGuid().ToString("N");
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
private bool _contentchanged = false;
private int _editorIndex;
private List<string> _debugLevels = new List<string> { "info", "log", "warn", "error" };
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public string Placeholder { get; set; }
// the following parameters were supported by the original RichTextEditor and can be passed as optional static parameters
[Parameter]
public bool? AllowFileManagement { get; set; }
[Parameter]
public bool? AllowRichText { get; set; }
[Parameter]
public bool? AllowRawHtml { get; set; }
[Parameter]
public string Theme { get; set; }
[Parameter]
public string DebugLevel { get; set; }
public override List<Resource> Resources { get; set; } = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body }
};
protected override void OnInitialized()
{
_interop = new QuillEditorInterop(JSRuntime);
if (string.IsNullOrEmpty(Placeholder))
{
Placeholder = Localizer["Placeholder"];
}
}
protected override void OnParametersSet()
{
LoadSettings();
if (!_allowRichText)
{
_activetab = "Raw";
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// include CSS theme
var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", $"css/quill/quill.{_theme}.css", "text/css", "", "", "");
}
await base.OnAfterRenderAsync(firstRender);
if (_allowRichText)
{
if (firstRender)
{
await _interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
_theme,
_debugLevel);
await _interop.LoadEditorContent(_editorElement, _richhtml);
// preserve a copy of the content (Quill sanitizes content so we need to retrieve it from the editor as it may have been modified)
_originalrichhtml = await _interop.GetHtml(_editorElement);
_initialized = true;
}
else
{
if (_initialized)
{
if (_contentchanged)
{
// reload editor if Content passed to component has changed
await _interop.LoadEditorContent(_editorElement, _richhtml);
_originalrichhtml = await _interop.GetHtml(_editorElement);
_contentchanged = false;
}
else
{
// preserve changed content on re-render event
var richhtml = await _interop.GetHtml(_editorElement);
if (richhtml != _richhtml)
{
_richhtml = richhtml;
await _interop.LoadEditorContent(_editorElement, _richhtml);
}
}
}
}
}
}
public void Initialize(string content)
{
_richhtml = content;
_rawhtml = content;
_originalrichhtml = "";
_richhtml = content;
if (!_contentchanged)
{
_contentchanged = content != _originalrawhtml;
}
_originalrawhtml = _rawhtml; // preserve for comparison later
StateHasChanged();
}
public async Task<string> GetContent()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (_allowRichText)
{
richhtml = await _interop.GetHtml(_editorElement);
}
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml))
{
// convert Quill's empty content to empty string
if (richhtml == "<p><br></p>")
{
richhtml = string.Empty;
}
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (_allowRichText)
{
richhtml = await _interop.GetHtml(_editorElement);
}
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml))
{
// convert Quill's empty content to empty string
if (richhtml == "<p><br></p>")
{
richhtml = string.Empty;
}
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public async Task InsertRichImage()
{
_message = string.Empty;
if (_richfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
await _interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex);
_richhtml = await _interop.GetHtml(_editorElement);
_richfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_editorIndex = await _interop.GetCurrentCursor(_editorElement);
_richfilemanager = true;
}
StateHasChanged();
}
public async Task InsertRawImage()
{
_message = string.Empty;
if (_rawfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new Interop(JSRuntime);
int pos = await interop.GetCaretPosition(_rawhtmlid);
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\" class=\"img-fluid\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos);
_rawfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
private void ScopeChanged(ChangeEventArgs e)
{
_scopeSetting = (string)e.Value;
LoadSettings();
}
private void LoadSettings(bool reload = false)
{
try
{
if (!_settingsLoaded || reload)
{
_allowFileManagement = bool.Parse(GetSetting("Component", "QuillTextEditor_AllowFileManagement", "True"));
_allowRawHtml = bool.Parse(GetSetting("Component", "QuillTextEditor_AllowRawHtml", "True"));
_allowRichText = bool.Parse(GetSetting("Component", "QuillTextEditor_AllowRichText", "True"));
_theme = GetSetting("Component", "QuillTextEditor_Theme", "snow");
_debugLevel = GetSetting("Component", "QuillTextEditor_DebugLevel", "info");
_toolbarContent = GetSetting("Component", "QuillTextEditor_ToolbarContent", string.Empty);
// optional static parameter overrides
if (AllowFileManagement != null) _allowFileManagement = AllowFileManagement.Value;
if (AllowRichText != null) _allowRichText = AllowRichText.Value;
if (AllowRawHtml != null) _allowRawHtml = AllowRawHtml.Value;
if (!string.IsNullOrEmpty(Theme)) _theme = Theme;
if (!string.IsNullOrEmpty(DebugLevel)) _debugLevel = DebugLevel;
}
_allowSettings = PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList);
if (_allowSettings)
{
_allowFileManagementSetting = GetSetting(_scopeSetting, "QuillTextEditor_AllowFileManagement", "True");
_allowRawHtmlSetting = GetSetting(_scopeSetting, "QuillTextEditor_AllowRawHtml", "True");
_allowRichTextSetting = GetSetting(_scopeSetting, "QuillTextEditor_AllowRichText", "True");
_themeSetting = GetSetting(_scopeSetting, "QuillTextEditor_Theme", "snow");
_debugLevelSetting = GetSetting(_scopeSetting, "QuillTextEditor_DebugLevel", "info");
_toolbarContentSetting = GetSetting(_scopeSetting, "QuillTextEditor_ToolbarContent", string.Empty);
}
_settingsLoaded = true;
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string GetSetting(string scope, string settingName, string defaultValue)
{
var settingValue = "";
switch (scope)
{
case "Component":
settingValue = SettingService.GetSetting(PageState.Site.Settings, settingName, defaultValue);
settingValue = SettingService.GetSetting(ModuleState.Settings, settingName, settingValue);
break;
case "Site":
settingValue = SettingService.GetSetting(PageState.Site.Settings, settingName, defaultValue);
break;
case "Module":
settingValue = SettingService.GetSetting(ModuleState.Settings, settingName, defaultValue);
break;
}
return settingValue;
}
private async Task UpdateSettings()
{
try
{
if (_scopeSetting == "Site" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowFileManagement", _allowFileManagementSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRawHtml", _allowRawHtmlSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRichText", _allowRichTextSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_Theme", _themeSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_DebugLevel", _debugLevelSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_ToolbarContent", _toolbarContentSetting);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
}
else if (_scopeSetting == "Module")
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowFileManagement", _allowFileManagementSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRawHtml", _allowRawHtmlSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRichText", _allowRichTextSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_Theme", _themeSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_DebugLevel", _debugLevelSetting);
settings = SettingService.SetSetting(settings, "QuillTextEditor_ToolbarContent", _toolbarContentSetting);
await SettingService.UpdateModuleSettingsAsync(settings,ModuleState.ModuleId);
}
LoadSettings(true);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -1,128 +1,22 @@
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.Extensions.DependencyInjection
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IServiceProvider ServiceProvider
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer @inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;"> <div class="row" style="margin-bottom: 50px;">
<div class="col"> <div class="col">
<TabStrip ActiveTab="@_activetab"> @_textEditorComponent
@if (AllowRichText)
{
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement"></div>
</div>
</div>
</TabPanel>
}
@if (AllowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="@_rawhtmlid" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
</TabStrip>
</div> </div>
</div> </div>
@code { @code {
private bool _initialized = false; private string _textEditorType;
private RenderFragment _textEditorComponent;
private RichTextEditorInterop interop; private ITextEditor _textEditor;
private FileManager _fileManager;
private string _activetab = "Rich";
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtmlid = "RawHtmlEditor_" + Guid.NewGuid().ToString("N");
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
private bool _contentchanged = false;
private int _editorIndex;
[Parameter] [Parameter]
public string Content { get; set; } public string Content { get; set; }
@ -134,203 +28,100 @@
public string Placeholder { get; set; } public string Placeholder { get; set; }
[Parameter] [Parameter]
public bool AllowFileManagement { get; set; } = true; public string Provider { get; set; }
[Parameter] [Parameter(CaptureUnmatchedValues = true)]
public bool AllowRichText { get; set; } = true; public Dictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>();
[Parameter]
public bool AllowRawHtml { get; set; } = true;
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public string DebugLevel { get; set; } = "info";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body }
};
protected override void OnInitialized() protected override void OnInitialized()
{ {
interop = new RichTextEditorInterop(JSRuntime); _textEditorType = GetTextEditorType();
if (string.IsNullOrEmpty(Placeholder))
{
Placeholder = Localizer["Placeholder"];
}
} }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
_richhtml = Content; _textEditorComponent = (builder) =>
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
_originalrichhtml = "";
if (Content != _originalrawhtml)
{ {
_contentchanged = true; // identifies when Content parameter has changed CreateTextEditor(builder);
} };
if (!AllowRichText)
{
_activetab = "Raw";
}
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if(_textEditor != null)
{
_textEditor.Initialize(Content);
}
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
if (AllowRichText)
{
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
await interop.LoadEditorContent(_editorElement, _richhtml);
// preserve a copy of the content (Quill sanitizes content so we need to retrieve it from the editor as it may have been modified)
_originalrichhtml = await interop.GetHtml(_editorElement);
_initialized = true;
}
else
{
if (_initialized)
{
if (_contentchanged)
{
// reload editor if Content passed to component has changed
await interop.LoadEditorContent(_editorElement, _richhtml);
_originalrichhtml = await interop.GetHtml(_editorElement);
}
else
{
// preserve changed content on re-render event
var richhtml = await interop.GetHtml(_editorElement);
if (richhtml != _richhtml)
{
_richhtml = richhtml;
await interop.LoadEditorContent(_editorElement, _richhtml);
}
}
}
}
_contentchanged = false;
}
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
} }
public async Task<string> GetHtml() public async Task<string> GetHtml()
{ {
// evaluate raw html content as first priority return await _textEditor.GetContent();
if (_rawhtml != _originalrawhtml) }
private void CreateTextEditor(RenderTreeBuilder builder)
{ {
return _rawhtml; if(!string.IsNullOrEmpty(_textEditorType))
{
var editorType = Type.GetType(_textEditorType);
if (editorType != null)
{
builder.OpenComponent(0, editorType);
var attributes = new Dictionary<string, object>
{
{ "Placeholder", Placeholder },
{ "ReadOnly", ReadOnly }
};
if (AdditionalAttributes != null)
{
foreach(var key in AdditionalAttributes.Keys)
{
if(!attributes.ContainsKey(key))
{
attributes.Add(key, AdditionalAttributes[key]);
} }
else else
{ {
var richhtml = ""; attributes[key] = AdditionalAttributes[key];
if (AllowRichText)
{
richhtml = await interop.GetHtml(_editorElement);
}
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml))
{
// convert Quill's empty content to empty string
if (richhtml == "<p><br></p>")
{
richhtml = string.Empty;
}
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
} }
} }
} }
public async Task InsertRichImage() var index = 1;
foreach(var name in attributes.Keys)
{ {
_message = string.Empty; if (editorType.GetProperty(name) != null)
if (_richfilemanager)
{ {
var file = _fileManager.GetFile(); builder.AddAttribute(index++, name, attributes[name]);
if (file != null)
{
await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex);
_richhtml = await interop.GetHtml(_editorElement);
_richfilemanager = false;
} }
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_editorIndex = await interop.GetCurrentCursor(_editorElement);
_richfilemanager = true;
}
StateHasChanged();
} }
public async Task InsertRawImage() builder.AddComponentReferenceCapture(index, (c) =>
{ {
_message = string.Empty; _textEditor = (ITextEditor)c;
if (_rawfilemanager) });
builder.CloseComponent();
}
}
}
private string GetTextEditorType()
{ {
var file = _fileManager.GetFile(); const string EditorSettingName = "TextEditor";
if (file != null)
if(!string.IsNullOrEmpty(Provider))
{ {
var interop = new Interop(JSRuntime); var provider = ServiceProvider.GetServices<ITextEditor>().FirstOrDefault(i => i.Name.Equals(Provider, StringComparison.OrdinalIgnoreCase));
int pos = await interop.GetCaretPosition(_rawhtmlid); if(provider != null)
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\" class=\"img-fluid\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos);
_rawfilemanager = false;
}
else
{ {
_message = Localizer["Message.Require.Image"]; return Utilities.GetFullTypeName(provider.GetType().AssemblyQualifiedName);
} }
} }
else
{ return SettingService.GetSetting(PageState.Site.Settings, EditorSettingName, Constants.DefaultTextEditor);
_rawfilemanager = true;
}
StateHasChanged();
} }
} }

View File

@ -23,7 +23,7 @@
</li> </li>
} }
</ul> </ul>
<div class="tab-content"> <div class="tab-content @TabContentClass">
<br /> <br />
@ChildContent @ChildContent
</div> </div>
@ -47,6 +47,9 @@
[Parameter] [Parameter]
public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided) public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided)
[Parameter]
public string TabContentClass { get; set; } // optional - to extend the TabContent div.
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (string.IsNullOrEmpty(Id)) if (string.IsNullOrEmpty(Id))

View File

@ -0,0 +1,33 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
<div class="text-area-editor">
<textarea @bind="_content" @ref="_editor" placeholder="@Placeholder" readonly="@ReadOnly" />
</div>
@code {
public string Name => "TextArea";
private ElementReference _editor;
private string _content;
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public string Placeholder { get; set; }
public void Initialize(string content)
{
_content = content;
StateHasChanged();
}
public async Task<string> GetContent()
{
await Task.CompletedTask;
return _content;
}
}

View File

@ -13,7 +13,7 @@
<TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit"> <TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
@if (_content != null) @if (_content != null)
{ {
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" AllowRawHtml="@_allowrawhtml" @ref="@RichTextEditorHtml"></RichTextEditor> <RichTextEditor Content="@_content" @ref="@RichTextEditorHtml"></RichTextEditor>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@ -51,15 +51,7 @@
public override string Title => "Edit Html/Text"; public override string Title => "Edit Html/Text";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
};
private RichTextEditor RichTextEditorHtml; private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement;
private bool _allowrawhtml;
private string _content = null; private string _content = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@ -72,8 +64,6 @@
{ {
try try
{ {
_allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"));
_allowrawhtml = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true"));
await LoadContent(); await LoadContent();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText
Version = "1.0.1", Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server", ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0,1.0.1", ReleaseVersions = "1.0.0,1.0.1",
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client", SettingsType = string.Empty,
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }

View File

@ -1,61 +0,0 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="files" ResourceKey="AllowFileManagement" ResourceType="@resourceType" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowfilemanagement">
<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="files" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify If Editors Can Enter Raw HTML">Allow Raw HTML: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowrawhtml">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement;
private string _allowrawhtml;
protected override void OnInitialized()
{
try
{
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
_allowrawhtml = SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
settings = SettingService.SetSetting(settings, "AllowRawHtml", _allowrawhtml);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -12,7 +12,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/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -22,9 +22,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.5" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
</ItemGroup> </ItemGroup>

View File

@ -144,8 +144,8 @@
<data name="Month" xml:space="preserve"> <data name="Month" xml:space="preserve">
<value>Month(s)</value> <value>Month(s)</value>
</data> </data>
<data name="ViewJobs.Text" xml:space="preserve"> <data name="ViewLogs.Text" xml:space="preserve">
<value>View Logs</value> <value>View All Logs</value>
</data> </data>
<data name="Frequency" xml:space="preserve"> <data name="Frequency" xml:space="preserve">
<value>Frequency</value> <value>Frequency</value>

View File

@ -132,4 +132,7 @@
<data name="Failed" xml:space="preserve"> <data name="Failed" xml:space="preserve">
<value>Failed</value> <value>Failed</value>
</data> </data>
<data name="Refresh" xml:space="preserve">
<value>Refresh</value>
</data>
</root> </root>

View File

@ -138,4 +138,7 @@
<data name="EditPage.Text" xml:space="preserve"> <data name="EditPage.Text" xml:space="preserve">
<value>Edit</value> <value>Edit</value>
</data> </data>
<data name="Error.Page.Load" xml:space="preserve">
<value>Error Loading Pages</value>
</data>
</root> </root>

View File

@ -0,0 +1,168 @@
<?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="Enabled.Text" xml:space="preserve">
<value>Enabled? </value>
</data>
<data name="Enabled.HelpText" xml:space="preserve">
<value>Specify if search indexing is enabled</value>
</data>
<data name="LastIndexedOn.Text" xml:space="preserve">
<value>Last Indexed: </value>
</data>
<data name="LastIndexedOn.HelpText" xml:space="preserve">
<value>The date/time which the site was last indexed on</value>
</data>
<data name="IgnorePages.Text" xml:space="preserve">
<value>Ignore Pages: </value>
</data>
<data name="IgnorePages.HelpText" xml:space="preserve">
<value>Comma delimited list of pages which should be ignored (based on page path)</value>
</data>
<data name="IgnoreEntities.Text" xml:space="preserve">
<value>Ignore Entities: </value>
</data>
<data name="IgnoreEntities.HelpText" xml:space="preserve">
<value>Comma delimited list of entities which should be ignored</value>
</data>
<data name="MinimumWordLength.Text" xml:space="preserve">
<value>Word Length: </value>
</data>
<data name="MinimumWordLength.HelpText" xml:space="preserve">
<value>Minimum length of a word to be indexed</value>
</data>
<data name="IgnoreWords.Text" xml:space="preserve">
<value>Ignore Words: </value>
</data>
<data name="IgnoreWords.HelpText" xml:space="preserve">
<value>Comma delimited list of words which should be ignored</value>
</data>
<data name="Success.Save" xml:space="preserve">
<value>Search Settings Saved Successfully</value>
</data>
<data name="Error.Save" xml:space="preserve">
<value>Error Saving Search Settings</value>
</data>
<data name="SearchProvider.HelpText" xml:space="preserve">
<value>Specify the search provider for this site</value>
</data>
<data name="SearchProvider.Text" xml:space="preserve">
<value>Search Provider:</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -117,16 +117,16 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="AllowFileManagement.HelpText" xml:space="preserve"> <data name="NoCriteria" xml:space="preserve">
<value>Specify If Editors Can Upload and Select Files</value> <value>You Must Provide Some Search Criteria</value>
</data> </data>
<data name="AllowFileManagement.Text" xml:space="preserve"> <data name="NoResult" xml:space="preserve">
<value>Allow File Management: </value> <value>No Content Matches The Criteria Provided</value>
</data> </data>
<data name="AllowRawHtml.HelpText" xml:space="preserve"> <data name="SearchLabel" xml:space="preserve">
<value>Specify If Editors Can Enter Raw HTML</value> <value>Search:</value>
</data> </data>
<data name="AllowRawHtml.Text" xml:space="preserve"> <data name="SearchPlaceholder" xml:space="preserve">
<value>Allow Raw HTML:</value> <value>Search</value>
</data> </data>
</root> </root>

View File

@ -0,0 +1,180 @@
<?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="Ascending" xml:space="preserve">
<value>Ascending</value>
</data>
<data name="BodyLength.HelpText" xml:space="preserve">
<value>The number of characters displayed for each search result summary. The default is 255 characters.</value>
</data>
<data name="BodyLength.Text" xml:space="preserve">
<value>Body Size:</value>
</data>
<data name="DateRange.HelpText" xml:space="preserve">
<value>Enter the date range for search results. The default includes all content.</value>
</data>
<data name="DateRange.Text" xml:space="preserve">
<value>Date Range:</value>
</data>
<data name="Descending" xml:space="preserve">
<value>Descending</value>
</data>
<data name="ExcludeEntities.HelpText" xml:space="preserve">
<value>Comma delimited list of entities to exclude from search results. By default no entities will be excluded.</value>
</data>
<data name="ExcludeEntities.Text" xml:space="preserve">
<value>Exlude Entities:</value>
</data>
<data name="IncludeEntities.HelpText" xml:space="preserve">
<value>Comma delimited list of entities to include in the search results. By default all entities will be included.</value>
</data>
<data name="IncludeEntities.Text" xml:space="preserve">
<value>Include Entities:</value>
</data>
<data name="LastModified" xml:space="preserve">
<value>LastModified</value>
</data>
<data name="PageSize.HelpText" xml:space="preserve">
<value>The maximum number of search results to retrieve. The default is unlimited.</value>
</data>
<data name="PageSize.Text" xml:space="preserve">
<value>Page Size:</value>
</data>
<data name="Relevance" xml:space="preserve">
<value>Relevance</value>
</data>
<data name="SortField.HelpText" xml:space="preserve">
<value>Specify the default sort field</value>
</data>
<data name="SortField.Text" xml:space="preserve">
<value>Sort By:</value>
</data>
<data name="SortOrder.HelpText" xml:space="preserve">
<value>Specify the default sort order</value>
</data>
<data name="SortOrder.Text" xml:space="preserve">
<value>Sort Order:</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
<data name="To" xml:space="preserve">
<value>To</value>
</data>
</root>

View File

@ -402,9 +402,6 @@
<data name="Retention.Text" xml:space="preserve"> <data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value> <value>Retention (Days):</value>
</data> </data>
<data name="FileExtensions.Heading" xml:space="preserve">
<value>File Extensions</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve"> <data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value> <value>Enter a comma separated list of image file extensions</value>
</data> </data>
@ -429,4 +426,13 @@
<data name="Runtime.Text" xml:space="preserve"> <data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value> <value>Interactivity:</value>
</data> </data>
<data name="TextEditor.HelpText" xml:space="preserve">
<value>Select the text editor for the site</value>
</data>
<data name="TextEditor.Text" xml:space="preserve">
<value>Text Editor:</value>
</data>
<data name="Functionality" xml:space="preserve">
<value>Functionality</value>
</data>
</root> </root>

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="AllowFileManagement.HelpText" xml:space="preserve">
<value>Specify if editors can upload and insert images</value>
</data>
<data name="AllowFileManagement.Text" xml:space="preserve">
<value>Insert Images?</value>
</data>
<data name="AllowRawHtml.HelpText" xml:space="preserve">
<value>Specify if editors can use the Raw HTML Editor</value>
</data>
<data name="AllowRawHtml.Text" xml:space="preserve">
<value>Raw HTML Editor?</value>
</data>
<data name="AllowRichText.HelpText" xml:space="preserve">
<value>Specify if editors can use the Rich Text Editor</value>
</data>
<data name="AllowRichText.Text" xml:space="preserve">
<value>Rich Text Editor? </value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="DebugLevel.HelpText" xml:space="preserve">
<value>Specify the Debug Level</value>
</data>
<data name="DebugLevel.Text" xml:space="preserve">
<value>Debug Level:</value>
</data>
<data name="InsertImage" xml:space="preserve">
<value>Insert Image</value>
</data>
<data name="Message.Require.Image" xml:space="preserve">
<value>You Must Select An Image To Insert</value>
</data>
<data name="Placeholder" xml:space="preserve">
<value>Enter Your Content...</value>
</data>
<data name="SaveSettings" xml:space="preserve">
<value>Save Settings</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Theme.HelpText" xml:space="preserve">
<value>Specify the Rich Text Editor's theme</value>
</data>
<data name="Theme.Text" xml:space="preserve">
<value>Theme:</value>
</data>
<data name="ToolbarContent.HelpText" xml:space="preserve">
<value>Specify any toolbar content to customize the Rich Text Editor</value>
</data>
<data name="ToolbarContent.Text" xml:space="preserve">
<value>Toolbar Content:</value>
</data>
</root>

View File

@ -459,4 +459,16 @@
<data name="Enabled" xml:space="preserve"> <data name="Enabled" xml:space="preserve">
<value>Enabled</value> <value>Enabled</value>
</data> </data>
<data name="Module" xml:space="preserve">
<value>Module</value>
</data>
<data name="Page" xml:space="preserve">
<value>Page</value>
</data>
<data name="Site" xml:space="preserve">
<value>Site</value>
</data>
<data name="User" xml:space="preserve">
<value>User</value>
</data>
</root> </root>

View File

@ -117,16 +117,10 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="InsertImage" xml:space="preserve"> <data name="Search" xml:space="preserve">
<value>Insert Image</value> <value>Search</value>
</data> </data>
<data name="Close" xml:space="preserve"> <data name="SearchPlaceHolder" xml:space="preserve">
<value>Close</value> <value>Search</value>
</data>
<data name="Message.Require.Image" xml:space="preserve">
<value>You Must Select An Image To Insert</value>
</data>
<data name="Placeholder" xml:space="preserve">
<value>Enter Your Content...</value>
</data> </data>
</root> </root>

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models;
namespace Oqtane.Services
{
[PrivateApi("Mark SearchResults classes as private, since it's not very useful in the public docs")]
public interface ISearchResultsService
{
Task<SearchResults> GetSearchResultsAsync(SearchQuery searchQuery);
}
}

View File

@ -46,6 +46,14 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task DeleteSiteAsync(int siteId); Task DeleteSiteAsync(int siteId);
/// <summary>
/// Returns a list of modules
/// </summary>
/// <param name="siteId"></param>
/// <param name="pageId"></param>
/// <returns></returns>
Task<List<Module>> GetModulesAsync(int siteId, int pageId);
[PrivateApi] [PrivateApi]
[Obsolete("This method is deprecated.", false)] [Obsolete("This method is deprecated.", false)]
void SetAlias(Alias alias); void SetAlias(Alias alias);

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SearchResultsService : ServiceBase, ISearchResultsService, IClientService
{
public SearchResultsService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("SearchResults");
public async Task<SearchResults> GetSearchResultsAsync(SearchQuery searchQuery)
{
return await PostJsonAsync<SearchQuery, SearchResults>(ApiUrl, searchQuery);
}
}
}

View File

@ -1,7 +1,6 @@
using Oqtane.Models; using Oqtane.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Net.Http; using System.Net.Http;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Shared; using Oqtane.Shared;
using System; using System;
@ -41,6 +40,11 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{siteId}"); await DeleteAsync($"{Apiurl}/{siteId}");
} }
public async Task<List<Module>> GetModulesAsync(int siteId, int pageId)
{
return await GetJsonAsync<List<Module>>($"{Apiurl}/modules/{siteId}/{pageId}");
}
[Obsolete("This method is deprecated.", false)] [Obsolete("This method is deprecated.", false)]
public void SetAlias(Alias alias) public void SetAlias(Alias alias)
{ {

View File

@ -31,7 +31,7 @@ namespace Oqtane.Services
public async Task<User> GetUserAsync(string username, int siteId) public async Task<User> GetUserAsync(string username, int siteId)
{ {
return await GetUserAsync(username, "", siteId); return await GetJsonAsync<User>($"{Apiurl}/username/{username}?siteid={siteId}");
} }
public async Task<User> GetUserAsync(string username, string email, int siteId) public async Task<User> GetUserAsync(string username, string email, int siteId)

View File

@ -9,13 +9,19 @@
<div class="row flex-xl-nowrap gx-0"> <div class="row flex-xl-nowrap gx-0">
<div class="sidebar"> <div class="sidebar">
<nav class="navbar"> <nav class="navbar">
<Logo /><Menu Orientation="Vertical" /> <Logo />
<Menu Orientation="Vertical" />
</nav> </nav>
</div> </div>
<div class="main g-0"> <div class="main g-0">
<div class="top-row px-4"> <div class="top-row px-4">
<div class="ms-auto"><UserProfile /> <Login /> <ControlPanel LanguageDropdownAlignment="right" /></div> <div class="ms-auto">
<Search CssClass="me-3 text-center d-inline-block" />
<UserProfile />
<Login />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
</div> </div>
<div class="container"> <div class="container">
<div class="row px-4"> <div class="row px-4">
@ -31,9 +37,13 @@
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // obtained from https://cdnjs.com/libraries
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css", Integrity = "sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css",
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js", Integrity = "sha512-X/YkDZyjTf4wyc2Vy16YGCPHwAY8rZJY+POgokZjQB2mhIRFJCckEGc6YyX9eNsPfn0PzThEuNs+uaomE5CO6A==", CrossOrigin = "anonymous", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
}; };
} }

View File

@ -6,10 +6,26 @@
{ {
@if (PageState.RenderMode == RenderModes.Interactive) @if (PageState.RenderMode == RenderModes.Interactive)
{ {
<ModuleActionsInteractive PageState="@PageState" ModuleState="@ModuleState" /> <ModuleActionsInteractive PageState="@_pageState" ModuleState="@ModuleState" />
} }
else else
{ {
<ModuleActionsInteractive PageState="@PageState" ModuleState="@ModuleState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" /> <ModuleActionsInteractive PageState="@_pageState" ModuleState="@ModuleState" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" />
}
}
@code {
private PageState _pageState;
protected override void OnParametersSet()
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
_pageState = new PageState
{
Alias = PageState.Alias,
Page = PageState.Page,
User = PageState.User,
EditMode = PageState.EditMode
};
} }
} }

View File

@ -7,10 +7,9 @@ using Oqtane.Models;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.UI;
using System.Net; using System.Net;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Oqtane.UI;
// ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable UnassignedGetOnlyAutoProperty
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global

View File

@ -32,11 +32,11 @@
{ {
@if (PageState.RenderMode == RenderModes.Interactive) @if (PageState.RenderMode == RenderModes.Interactive)
{ {
<ControlPanelInteractive PageState="@PageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" /> <ControlPanelInteractive PageState="@_pageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" CanViewAdminDashboard="@_canViewAdminDashboard" />
} }
else else
{ {
<ControlPanelInteractive PageState="@PageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" /> <ControlPanelInteractive PageState="@_pageState" SiteState="@SiteState" ButtonClass="@ButtonClass" ContainerClass="@ContainerClass" HeaderClass="@HeaderClass" BodyClass="@BodyClass" ShowLanguageSwitcher="@ShowLanguageSwitcher" LanguageDropdownAlignment="@LanguageDropdownAlignment" CanViewAdminDashboard="@_canViewAdminDashboard" @rendermode="@InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, false)" />
} }
} }
@ -59,6 +59,7 @@
[Parameter] [Parameter]
public string LanguageDropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right public string LanguageDropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right
private PageState _pageState;
private bool _canViewAdminDashboard = false; private bool _canViewAdminDashboard = false;
private bool _showEditMode = false; private bool _showEditMode = false;
@ -73,7 +74,7 @@
} }
else else
{ {
foreach (var module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) foreach (var module in PageState.Modules)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList))
{ {
@ -82,6 +83,24 @@
} }
} }
} }
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
_pageState = new PageState
{
Alias = PageState.Alias,
Site = new Site
{
DefaultContainerType = PageState.Site.DefaultContainerType,
Settings = PageState.Site.Settings,
Themes = PageState.Site.Themes
},
Page = PageState.Page,
User = PageState.User,
Uri = PageState.Uri,
Route = PageState.Route,
RenderMode = PageState.RenderMode,
Runtime = PageState.Runtime
};
} }
private bool CanViewAdminDashboard() private bool CanViewAdminDashboard()

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="@BodyClass"> <div class="@BodyClass">
<div class="container-fluid"> <div class="container-fluid">
@if (_canViewAdminDashboard) @if (CanViewAdminDashboard)
{ {
<div class="row d-flex"> <div class="row d-flex">
<div class="col"> <div class="col">
@ -248,7 +248,9 @@
[Parameter] [Parameter]
public string LanguageDropdownAlignment { get; set; } public string LanguageDropdownAlignment { get; set; }
private bool _canViewAdminDashboard = false; [Parameter]
public bool CanViewAdminDashboard { get; set; }
private bool _deleteConfirmation = false; private bool _deleteConfirmation = false;
private List<string> _categories = new List<string>(); private List<string> _categories = new List<string>();
private List<ModuleDefinition> _allModuleDefinitions; private List<ModuleDefinition> _allModuleDefinitions;
@ -278,44 +280,17 @@
// repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary) // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary)
ComponentSiteState.Hydrate(SiteState); ComponentSiteState.Hydrate(SiteState);
_canViewAdminDashboard = CanViewAdminDashboard();
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
LoadSettingsAsync(); LoadSettingsAsync();
_pages?.Clear();
foreach (Page p in PageState.Pages)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_pages.Add(p);
}
}
_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.Site.SiteId); _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId);
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList();
} }
} }
private bool CanViewAdminDashboard()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
if (admin != null)
{
foreach (var page in PageState.Pages.Where(item => item.ParentId == admin?.PageId))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList))
{
return true;
}
}
}
return false;
}
private void CategoryChanged(ChangeEventArgs e) private void CategoryChanged(ChangeEventArgs e)
{ {
_category = (string)e.Value; _category = (string)e.Value;
@ -339,20 +314,24 @@
StateHasChanged(); StateHasChanged();
} }
private void ModuleTypeChanged(ChangeEventArgs e) private async Task ModuleTypeChanged(ChangeEventArgs e)
{ {
_moduleType = (string)e.Value; _moduleType = (string)e.Value;
if (_moduleType != "new")
{
_pages = await PageService.GetPagesAsync(PageState.Page.SiteId);
}
_pageId = "-"; _pageId = "-";
_moduleId = "-"; _moduleId = "-";
} }
private void PageChanged(ChangeEventArgs e) private async Task PageChanged(ChangeEventArgs e)
{ {
_pageId = (string)e.Value; _pageId = (string)e.Value;
if (_pageId != "-") if (_pageId != "-")
{ {
_modules = PageState.Modules _modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
.Where(module => module.PageId == int.Parse(_pageId) && _modules = _modules.Where(module => module.PageId == int.Parse(_pageId) &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
(_moduleType == "add" || module.ModuleDefinition.IsPortable)) (_moduleType == "add" || module.ModuleDefinition.IsPortable))
.ToList(); .ToList();
@ -371,7 +350,7 @@
if (_moduleType == "new") if (_moduleType == "new")
{ {
Module module = new Module(); Module module = new Module();
module.SiteId = PageState.Site.SiteId; module.SiteId = PageState.Page.SiteId;
module.PageId = PageState.Page.PageId; module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName; module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false; module.AllPages = false;
@ -384,7 +363,7 @@
{ {
var module = await ModuleService.GetModuleAsync(int.Parse(_moduleId)); var module = await ModuleService.GetModuleAsync(int.Parse(_moduleId));
module.ModuleId = 0; module.ModuleId = 0;
module.SiteId = PageState.Site.SiteId; module.SiteId = PageState.Page.SiteId;
module.PageId = PageState.Page.PageId; module.PageId = PageState.Page.PageId;
module.AllPages = false; module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId); module.PermissionList = GenerateDefaultPermissions(module.SiteId);
@ -482,27 +461,19 @@
private void Navigate(string location) private void Navigate(string location)
{ {
Module module; int moduleId;
switch (location) switch (location)
{ {
case "Admin": case "Admin":
// get admin dashboard moduleid // get admin dashboard moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule); moduleId = int.Parse(PageState.Site.Settings[Constants.AdminDashboardModule]);
if (module != null) NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", moduleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
{
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin", module.ModuleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
}
break; break;
case "Add": case "Add":
case "Edit": case "Edit":
string url = "";
// get page management moduleid // get page management moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule); moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
if (module != null) NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
{
url = Utilities.EditUrl(PageState.Alias.Path, "admin/pages", module.ModuleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}");
NavigationManager.NavigateTo(url);
}
break; break;
} }
} }
@ -517,11 +488,11 @@
case "publish": case "publish":
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone)) if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{ {
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Everyone, null, true)); permissions.Add(new Permission(PageState.Page.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
} }
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered)) if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{ {
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Registered, null, true)); permissions.Add(new Permission(PageState.Page.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Registered, null, true));
} }
break; break;
case "unpublish": case "unpublish":

View File

@ -59,7 +59,7 @@ namespace Oqtane.Themes.Controls
logouturl = Utilities.TenantUrl(PageState.Alias, "/pages/logout/"); logouturl = Utilities.TenantUrl(PageState.Alias, "/pages/logout/");
// verify anonymous users can access current page // verify anonymous users can access current page
if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsPageModuleVisible(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate)) if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsEffectiveAndNotExpired(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate))
{ {
returnurl = PageState.Route.PathAndQuery; returnurl = PageState.Route.PathAndQuery;
} }

View File

@ -0,0 +1,61 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@using Microsoft.AspNetCore.Http
@inherits ThemeControlBase
@inject IStringLocalizer<Search> Localizer
@inject NavigationManager NavigationManager
@if (_searchResultsPage != null)
{
<span class="app-search @CssClass">
<form method="post" class="app-form-inline" @formname="@($"SearchForm")" @onsubmit="@PerformSearch" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="text" name="keywords" maxlength="50"
class="form-control d-inline-block pe-5 shadow-none"
@bind="_keywords"
placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" />
<button type="submit" class="btn btn-search">
<span class="oi oi-magnifying-glass align-middle"></span>
</button>
</form>
</span>
}
@code {
private Page _searchResultsPage;
private string _keywords = "";
[Parameter]
public string CssClass { get; set; }
[Parameter]
public string SearchResultPagePath { get; set; } = "search";
[CascadingParameter]
HttpContext HttpContext { get; set; }
[SupplyParameterFromForm(FormName = "SearchForm")]
public string KeyWords { get => ""; set => _keywords = value; }
protected override void OnInitialized()
{
if(!string.IsNullOrEmpty(SearchResultPagePath))
{
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
}
}
private void PerformSearch()
{
if (_searchResultsPage != null)
{
var url = NavigateUrl(_searchResultsPage.Path, $"q={_keywords}");
NavigationManager.NavigateTo(url);
}
}
}

View File

@ -17,9 +17,13 @@ namespace Oqtane.Themes.OqtaneTheme
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // obtained from https://cdnjs.com/libraries
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.2/cyborg/bootstrap.min.css", Integrity = "sha512-RfNxVfFNFgqk9MXO4TCKXYXn9hgc+keHCg3xFFGbnp2q7Cifda+YYzMTDHwsQtNx4DuqIMgfvZead7XOtB9CDQ==", CrossOrigin = "anonymous" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css",
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==",
CrossOrigin = "anonymous" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js", Integrity = "sha512-X/YkDZyjTf4wyc2Vy16YGCPHwAY8rZJY+POgokZjQB2mhIRFJCckEGc6YyX9eNsPfn0PzThEuNs+uaomE5CO6A==", CrossOrigin = "anonymous", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
} }
}; };
} }

View File

@ -6,7 +6,12 @@
<nav class="navbar navbar-dark bg-primary fixed-top"> <nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" /> <Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto"> <div class="controls ms-auto">
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel LanguageDropdownAlignment="right" /></div> <div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" />
<UserProfile ShowRegister="@_register" />
<Login ShowLogin="@_login" />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
</div> </div>
</nav> </nav>
<div class="content"> <div class="content">

View File

@ -0,0 +1,13 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.UI;
public partial class ModuleInstance;

View File

@ -15,9 +15,6 @@
} }
@code { @code {
// this component is on the static side of the render mode boundary
// it passes state as serializable parameters across the boundary
[CascadingParameter] [CascadingParameter]
protected PageState PageState { get; set; } protected PageState PageState { get; set; }
@ -30,6 +27,7 @@
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
_prerender = ModuleState.Prerender ?? PageState.Site.Prerender; _prerender = ModuleState.Prerender ?? PageState.Site.Prerender;
_comment = "<!-- rendermode: "; _comment = "<!-- rendermode: ";
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Static) if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Static)
{ {
@ -40,6 +38,13 @@
_comment += $"{RenderModes.Interactive}:{PageState.Runtime} - prerender: {_prerender}"; _comment += $"{RenderModes.Interactive}:{PageState.Runtime} - prerender: {_prerender}";
} }
_comment += " -->"; _comment += " -->";
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive)
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components
PageState.Site.Pages = new List<Page>();
}
} }

View File

@ -9,6 +9,7 @@ namespace Oqtane.UI
public Alias Alias { get; set; } public Alias Alias { get; set; }
public Site Site { get; set; } public Site Site { get; set; }
public Page Page { get; set; } public Page Page { get; set; }
public List<Module> Modules { get; set; }
public User User { get; set; } public User User { get; set; }
public Uri Uri { get; set; } public Uri Uri { get; set; }
public Route Route { get; set; } public Route Route { get; set; }
@ -29,15 +30,11 @@ namespace Oqtane.UI
public List<Page> Pages public List<Page> Pages
{ {
get { return Site.Pages; } get { return Site?.Pages; }
}
public List<Module> Modules
{
get { return Site.Modules; }
} }
public List<Language> Languages public List<Language> Languages
{ {
get { return Site.Languages; } get { return Site?.Languages; }
} }
} }
} }

View File

@ -43,7 +43,7 @@ else
DynamicComponent = builder => DynamicComponent = builder =>
{ {
foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) foreach (Module module in PageState.Modules)
{ {
// set renderid - this allows the framework to determine which components should be rendered when PageState changes // set renderid - this allows the framework to determine which components should be rendered when PageState changes
if (module.RenderId != PageState.RenderId) if (module.RenderId != PageState.RenderId)

View File

@ -0,0 +1,13 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.UI;
public partial class RenderModeBoundary;

View File

@ -2,6 +2,7 @@
@using System.Net @using System.Net
@using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Http
@using System.Globalization @using System.Globalization
@using System.Security.Claims
@namespace Oqtane.UI @namespace Oqtane.UI
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState @inject SiteState SiteState
@ -96,6 +97,7 @@
{ {
Site site = null; Site site = null;
Page page = null; Page page = null;
List<Module> modules = null;
User user = null; User user = null;
var editmode = false; var editmode = false;
var refresh = false; var refresh = false;
@ -158,7 +160,8 @@
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey)) if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
{ {
// get user // get user
user = await UserService.GetUserAsync(authState.User.Identity.Name, SiteState.Alias.SiteId); var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
user = await UserService.GetUserAsync(userid, SiteState.Alias.SiteId);
if (user != null) if (user != null)
{ {
user.IsAuthenticated = authState.User.Identity.IsAuthenticated; user.IsAuthenticated = authState.User.Identity.IsAuthenticated;
@ -212,7 +215,7 @@
return; return;
} }
if (PageState == null || refresh || PageState.Page.Path != route.PagePath) if (refresh || PageState == null || PageState.Page.Path != route.PagePath)
{ {
page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
} }
@ -253,7 +256,7 @@
} }
// check if user is authorized to view page // check if user is authorized to view page
if (page != null && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))) if (page != null && UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)))
{ {
// edit mode // edit mode
if (user != null) if (user != null)
@ -273,11 +276,21 @@
} }
} }
// get modules for current page
if (refresh || PageState.Modules == null || !PageState.Modules.Any() || PageState.Modules.First().PageId != page.PageId)
{
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
else
{
modules = PageState.Modules;
}
// load additional metadata for current page // load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias); page = ProcessPage(page, site, user, SiteState.Alias);
// load additional metadata for modules // load additional metadata for modules
(page, site.Modules) = ProcessModules(page, site.Modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); (page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
// populate page state (which acts as a client-side cache for subsequent requests) // populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState _pagestate = new PageState
@ -285,6 +298,7 @@
Alias = SiteState.Alias, Alias = SiteState.Alias,
Site = site, Site = site,
Page = page, Page = page,
Modules = modules,
User = user, User = user,
Uri = new Uri(_absoluteUri, UriKind.Absolute), Uri = new Uri(_absoluteUri, UriKind.Absolute),
Route = route, Route = route,

View File

@ -66,21 +66,17 @@
{ {
if (!string.IsNullOrEmpty(content)) if (!string.IsNullOrEmpty(content))
{ {
// format head content, remove scripts, and filter duplicate elements if (PageState.RenderMode == RenderModes.Interactive)
content = content.Replace("\n", ""); {
var index = content.IndexOf("<"); // remove scripts
var index = content.IndexOf("<script");
while (index >= 0) while (index >= 0)
{ {
var element = content.Substring(index, content.IndexOf(">", index) - index + 1); content = content.Remove(index, content.IndexOf("</script>") + 9 - index);
if (!string.IsNullOrEmpty(element) && (PageState.RenderMode == RenderModes.Static || (!element.ToLower().StartsWith("<script") && !element.ToLower().StartsWith("</script")))) index = content.IndexOf("<script");
{
if (!headcontent.Contains(element))
{
headcontent += element + "\n";
} }
} }
index = content.IndexOf("<", index + 1); headcontent += content + "\n";
}
} }
return headcontent; return headcontent;
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="MySql.Data" Version="8.3.0" /> <PackageReference Include="MySql.Data" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,7 +34,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup> </ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> --> <!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> --> <!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -14,7 +14,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/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace> <RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid> <ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>5.1.2</ApplicationDisplayVersion> <ApplicationDisplayVersion>5.2.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
@ -65,11 +65,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.5" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.40" /> <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.40" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.40" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.40" />

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>5.1.2</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>5.1.2</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright> <copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.1.2/Oqtane.Framework.5.1.2.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.2.0/Oqtane.Framework.5.2.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane framework</tags> <tags>oqtane framework</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>5.1.2</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>5.1.2</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>5.1.2</version> <version>5.2.0</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.2.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.0.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.2.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.0.Upgrade.zip" -Force

View File

@ -132,6 +132,7 @@
_renderMode = site.RenderMode; _renderMode = site.RenderMode;
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.Prerender; _prerender = site.Prerender;
var modules = new List<Module>();
Route route = new Route(url, alias.Path); Route route = new Route(url, alias.Path);
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));
@ -156,6 +157,10 @@
{ {
HandlePageNotFound(site, page, route); HandlePageNotFound(site, page, route);
} }
else
{
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
if (site.VisitorTracking) if (site.VisitorTracking)
{ {
@ -169,7 +174,7 @@
} }
// includes resources // includes resources
var resources = GetPageResources(alias, site, page, 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);
@ -221,6 +226,7 @@
Alias = alias, Alias = alias,
Site = site, Site = site,
Page = page, Page = page,
Modules = modules,
User = null, User = null,
Uri = new Uri(url, UriKind.Absolute), Uri = new Uri(url, UriKind.Absolute),
Route = route, Route = route,
@ -332,7 +338,7 @@
{ {
var values = visitorCookieValue.Split('|'); var values = visitorCookieValue.Split('|');
int.TryParse(values[0], out _visitorId); int.TryParse(values[0], out _visitorId);
DateTime.TryParse(values[1], out expiry); DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry);
} }
else // legacy cookie format else // legacy cookie format
{ {
@ -419,7 +425,7 @@
Context.Response.Cookies.Append( Context.Response.Cookies.Append(
visitorCookieName, visitorCookieName,
$"{_visitorId}|{expiry}", $"{_visitorId}|{expiry.ToString("M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture)}",
new CookieOptions() new CookieOptions()
{ {
Expires = DateTimeOffset.UtcNow.AddYears(10), Expires = DateTimeOffset.UtcNow.AddYears(10),
@ -574,8 +580,17 @@
else else
{ {
// use custom element which can execute script on every page transition // use custom element which can execute script on every page transition
@if (string.IsNullOrEmpty(resource.Integrity) && string.IsNullOrEmpty(resource.CrossOrigin))
{
return "<page-script src=\"" + resource.Url + "\"></page-script>"; return "<page-script src=\"" + resource.Url + "\"></page-script>";
} }
else
{
// use modulepreload for external resources
return "<link rel=\"modulepreload\" href=\"" + resource.Url + "\" integrity=\"" + resource.Integrity + "\" crossorigin=\"" + resource.CrossOrigin + "\" />\n" +
"<page-script src=\"" + resource.Url + "\"></page-script>";
}
}
} }
else else
{ {
@ -591,7 +606,7 @@
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture))); CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)));
} }
private List<Resource> GetPageResources(Alias alias, Site site, Page page, 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>();
@ -617,7 +632,7 @@
} }
} }
foreach (Module module in site.Modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid)) foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
{ {
var typename = ""; var typename = "";
if (module.ModuleDefinition != null) if (module.ModuleDefinition != null)
@ -683,15 +698,18 @@
} }
} }
if (site.RenderMode == RenderModes.Interactive)
{
// site level resources for modules in site // site level resources for modules in site
var modules = site.Modules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList(); var sitemodules = await SiteService.GetModulesAsync(site.SiteId, -1);
foreach (var module in modules) foreach (var module in sitemodules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList())
{ {
if (module.ModuleDefinition?.Resources != null) if (module.ModuleDefinition?.Resources != null)
{ {
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
} }
} }
}
return resources; return resources;
} }

View File

@ -0,0 +1,16 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2024-06-27 by @iJungleboy.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
using Oqtane.Documentation;
namespace Oqtane.Components;
[PrivateApi]
public class _Placeholder;

View File

@ -9,7 +9,6 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using System.Net; using System.Net;
using System.Security.Policy;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {

View File

@ -194,7 +194,7 @@ namespace Oqtane.Controllers
{ {
try try
{ {
if (moduletype.GetInterface("IInstallable") != null) if (moduletype.GetInterface(nameof(IInstallable)) != null)
{ {
_tenantManager.SetTenant(tenant.TenantId); _tenantManager.SetTenant(tenant.TenantId);
var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype);

View File

@ -0,0 +1,42 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class SearchResultsController : Controller
{
private readonly ISearchService _searchService;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SearchResultsController(ISearchService searchService, ILogManager logger, ITenantManager tenantManager)
{
_searchService = searchService;
_logger = logger;
_alias = tenantManager.GetAlias();
}
[HttpPost]
public async Task<SearchResults> Post([FromBody] SearchQuery searchQuery)
{
if (ModelState.IsValid && searchQuery.SiteId == _alias.SiteId)
{
return await _searchService.GetSearchResultsAsync(searchQuery);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Search Results Post Attempt {SearchQuery}", searchQuery);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
}
}

View File

@ -81,5 +81,12 @@ namespace Oqtane.Controllers
{ {
await _siteService.DeleteSiteAsync(id); await _siteService.DeleteSiteAsync(id);
} }
// GET api/<controller>/modules/5/6
[HttpGet("modules/{siteId}/{pageId}")]
public async Task<IEnumerable<Module>> GetModules(int siteId, int pageId)
{
return await _siteService.GetModulesAsync(siteId, pageId);
}
} }
} }

View File

@ -72,15 +72,13 @@ namespace Oqtane.Controllers
} }
} }
// GET api/<controller>/name/{name}/{email}?siteid=x // GET api/<controller>/username/{username}?siteid=x
[HttpGet("name/{name}/{email}")] [HttpGet("username/{username}")]
public User Get(string name, string email, string siteid) public User Get(string username, string siteid)
{ {
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId) if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{ {
name = (name == "-") ? "" : name; User user = _userManager.GetUser(username, SiteId);
email = (email == "-") ? "" : email;
User user = _userManager.GetUser(name, email, SiteId);
if (user == null) if (user == null)
{ {
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
@ -95,7 +93,36 @@ namespace Oqtane.Controllers
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {Email} {SiteId}", name, email, siteid); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {SiteId}", username, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/name/{username}/{email}?siteid=x
[HttpGet("search/{username}/{email}")]
public User Get(string username, string email, string siteid)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
username = (username == "-") ? "" : username;
email = (email == "-") ? "" : email;
User user = _userManager.GetUser(username, email, SiteId);
if (user == null)
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
else
{
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || _userPermissions.GetUser(User).UserId == user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return Filter(user);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Get Attempt {Username} {Email} {SiteId}", username, email, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null; return null;
} }
@ -340,14 +367,11 @@ namespace Oqtane.Controllers
if (user.IsAuthenticated) if (user.IsAuthenticated)
{ {
user.Username = User.Identity.Name; user.Username = User.Identity.Name;
if (User.HasClaim(item => item.Type == ClaimTypes.NameIdentifier)) user.UserId = User.UserId();
{
user.UserId = int.Parse(User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
string roles = ""; string roles = "";
foreach (var claim in User.Claims.Where(item => item.Type == ClaimTypes.Role)) foreach (var roleName in User.Roles())
{ {
roles += claim.Value + ";"; roles += roleName + ";";
} }
if (roles != "") roles = ";" + roles; if (roles != "") roles = ";" + roles;
user.Roles = roles; user.Roles = roles;

View File

@ -33,14 +33,10 @@ namespace Oqtane.Extensions
} }
} }
public static string Roles(this ClaimsPrincipal claimsPrincipal) public static string[] Roles(this ClaimsPrincipal claimsPrincipal)
{ {
var roles = ""; return claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)) .Select(item => item.Value).ToArray();
{
roles += ((roles == "") ? "" : ";") + claim.Value;
}
return roles;
} }
public static string SiteKey(this ClaimsPrincipal claimsPrincipal) public static string SiteKey(this ClaimsPrincipal claimsPrincipal)

View File

@ -7,6 +7,7 @@ namespace Oqtane.Extensions
{ {
public static class HttpContextExtensions public static class HttpContextExtensions
{ {
// this method should only be used in scenarios where HttpContent exists (ie. within Controllers)
public static Alias GetAlias(this HttpContext context) public static Alias GetAlias(this HttpContext context)
{ {
if (context != null && context.Items.ContainsKey(Constants.HttpContextAliasKey)) if (context != null && context.Items.ContainsKey(Constants.HttpContextAliasKey))
@ -16,6 +17,7 @@ namespace Oqtane.Extensions
return null; return null;
} }
// this method should only be used in scenarios where HttpContent exists (ie. within Controllers)
public static Dictionary<string, string> GetSiteSettings(this HttpContext context) public static Dictionary<string, string> GetSiteSettings(this HttpContext context)
{ {
if (context != null && context.Items.ContainsKey(Constants.HttpContextSiteSettingsKey)) if (context != null && context.Items.ContainsKey(Constants.HttpContextSiteSettingsKey))

View File

@ -19,8 +19,10 @@ using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Infrastructure.Interfaces; using Oqtane.Infrastructure.Interfaces;
using Oqtane.Interfaces;
using Oqtane.Managers; using Oqtane.Managers;
using Oqtane.Modules; using Oqtane.Modules;
using Oqtane.Providers;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
@ -97,6 +99,13 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IUrlMappingService, UrlMappingService>(); services.AddScoped<IUrlMappingService, UrlMappingService>();
services.AddScoped<IVisitorService, VisitorService>(); services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ISearchResultsService, SearchResultsService>();
services.AddScoped<ISearchService, SearchService>();
services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.TextAreaTextEditor>();
return services; return services;
} }
@ -131,6 +140,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<ILanguageRepository, LanguageRepository>(); services.AddTransient<ILanguageRepository, LanguageRepository>();
services.AddTransient<IVisitorRepository, VisitorRepository>(); services.AddTransient<IVisitorRepository, VisitorRepository>();
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>(); services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();
services.AddTransient<ISearchContentRepository, SearchContentRepository>();
// managers // managers
services.AddTransient<IDBContextDependencies, DBContextDependencies>(); services.AddTransient<IDBContextDependencies, DBContextDependencies>();

View File

@ -452,7 +452,7 @@ namespace Oqtane.Infrastructure
{ {
try try
{ {
if (moduleType.GetInterface("IInstallable") != null) if (moduleType.GetInterface(nameof(IInstallable)) != null)
{ {
tenantManager.SetTenant(tenant.TenantId); tenantManager.SetTenant(tenant.TenantId);
var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable; var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable;

View File

@ -19,12 +19,8 @@ namespace Oqtane.Infrastructure.EventSubscribers
// when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed // when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh) if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh)
{ {
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}*", true); _cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}");
} _cache.Remove($"modules:{syncEvent.TenantId}:{syncEvent.EntityId}");
// when user is modified (ie. roles) a a site reload event is raised and the site cache item for the user needs to be refreshed
if (syncEvent.EntityName == EntityNames.User && syncEvent.Action == SyncEventActions.Reload)
{
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.SiteId}:{syncEvent.EntityId}", true);
} }
// when a site entity is updated, the hosting model may have changed so the client assemblies cache items need to be refreshed // when a site entity is updated, the hosting model may have changed so the client assemblies cache items need to be refreshed

View File

@ -149,7 +149,7 @@ namespace Oqtane.Infrastructure
catch (Exception ex) catch (Exception ex)
{ {
// error // error
log += ex.Message + "<br />"; log += $"NotificationId: {notification.NotificationId} - {ex.Message}<br />";
} }
} }
} }

View File

@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Extensions;
using Oqtane.Interfaces;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
public class SearchIndexJob : HostedServiceBase
{
private const string SearchLastIndexedOnSetting = "Search_LastIndexedOn";
private const string SearchEnabledSetting = "Search_Enabled";
private const string SearchIgnorePagesSetting = "Search_IgnorePages";
private const string SearchIgnoreEntitiesSetting = "Search_IgnoreEntities";
public SearchIndexJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
Name = "Search Index Job";
Frequency = "m"; // run every minute.
Interval = 1;
IsEnabled = true;
}
public override async Task<string> ExecuteJobAsync(IServiceProvider provider)
{
string log = "";
// get services
var siteRepository = provider.GetRequiredService<ISiteRepository>();
var settingRepository = provider.GetRequiredService<ISettingRepository>();
var tenantManager = provider.GetRequiredService<ITenantManager>();
var pageRepository = provider.GetRequiredService<IPageRepository>();
var pageModuleRepository = provider.GetRequiredService<IPageModuleRepository>();
var searchService = provider.GetRequiredService<ISearchService>();
var sites = siteRepository.GetSites().ToList();
foreach (var site in sites)
{
log += $"Indexing Site: {site.Name}<br />";
// initialize
var siteSettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
if (!Convert.ToBoolean(siteSettings.GetValue(SearchEnabledSetting, "true")))
{
log += $"Indexing Disabled<br />";
continue;
}
var tenantId = tenantManager.GetTenant().TenantId;
tenantManager.SetAlias(tenantId, site.SiteId);
var currentTime = DateTime.UtcNow;
var lastIndexedOn = Convert.ToDateTime(siteSettings.GetValue(SearchLastIndexedOnSetting, DateTime.MinValue.ToString()));
var ignorePages = siteSettings.GetValue(SearchIgnorePagesSetting, "").Split(',');
var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "").Split(',');
var pages = pageRepository.GetPages(site.SiteId);
var pageModules = pageModuleRepository.GetPageModules(site.SiteId);
var searchContents = new List<SearchContent>();
// index pages
foreach (var page in pages)
{
if (!string.IsNullOrEmpty(page.Path) && (Constants.InternalPagePaths.Contains(page.Path) || ignorePages.Contains(page.Path)))
{
continue;
}
bool changed = false;
bool removed = false;
if (page.ModifiedOn >= lastIndexedOn && !ignoreEntities.Contains(EntityNames.Page))
{
changed = true;
removed = page.IsDeleted || !Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate);
var searchContent = new SearchContent
{
SiteId = page.SiteId,
EntityName = EntityNames.Page,
EntityId = page.PageId.ToString(),
Title = !string.IsNullOrEmpty(page.Title) ? page.Title : page.Name,
Description = string.Empty,
Body = string.Empty,
Url = $"{(!string.IsNullOrEmpty(page.Path) && !page.Path.StartsWith("/") ? "/" : "")}{page.Path}",
Permissions = $"{EntityNames.Page}:{page.PageId}",
ContentModifiedBy = page.ModifiedBy,
ContentModifiedOn = page.ModifiedOn,
AdditionalContent = string.Empty,
CreatedOn = DateTime.UtcNow,
IsDeleted = removed,
TenantId = tenantId
};
searchContents.Add(searchContent);
}
// index modules
foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId))
{
if (pageModule.ModifiedOn >= lastIndexedOn && !changed)
{
changed = true;
}
var searchable = false;
if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "")
{
Type type = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType);
if (type?.GetInterface(nameof(ISearchable)) != null)
{
try
{
searchable = true;
// determine if reindexing is necessary
var lastindexedon = (changed) ? DateTime.MinValue : lastIndexedOn;
// index module content
var serverManager = (ISearchable)ActivatorUtilities.CreateInstance(provider, type);
var searchcontents = await serverManager.GetSearchContentsAsync(pageModule, lastindexedon);
if (searchcontents != null)
{
foreach (var searchContent in searchcontents)
{
if (!ignoreEntities.Contains(searchContent.EntityName))
{
ValidateSearchContent(searchContent, pageModule, tenantId, removed);
searchContents.Add(searchContent);
}
}
}
}
catch (Exception ex)
{
log += $"Error Indexing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}<br />";
}
}
}
if (!searchable && changed && !ignoreEntities.Contains(EntityNames.Module))
{
// module does not implement ISearchable
var searchContent = new SearchContent();
ValidateSearchContent(searchContent, pageModule, tenantId, removed);
searchContents.Add(searchContent);
}
}
}
// save search contents
log += await searchService.SaveSearchContentsAsync(searchContents, siteSettings);
log += $"Items Indexed: {searchContents.Count}<br />";
// update last indexed on
SaveSearchLastIndexedOn(settingRepository, site.SiteId, currentTime);
}
return log;
}
private void ValidateSearchContent(SearchContent searchContent, PageModule pageModule, int tenantId, bool removed)
{
// set default values
searchContent.SiteId = pageModule.Module.SiteId;
searchContent.TenantId = tenantId;
searchContent.CreatedOn = DateTime.UtcNow;
if (string.IsNullOrEmpty(searchContent.EntityName))
{
searchContent.EntityName = EntityNames.Module;
}
if (string.IsNullOrEmpty(searchContent.EntityId))
{
searchContent.EntityId = pageModule.ModuleId.ToString();
}
if (string.IsNullOrEmpty(searchContent.Title))
{
searchContent.Title = string.Empty;
if (!string.IsNullOrEmpty(pageModule.Title))
{
searchContent.Title = pageModule.Title;
}
else if (pageModule.Page != null)
{
searchContent.Title = !string.IsNullOrEmpty(pageModule.Page.Title) ? pageModule.Page.Title : pageModule.Page.Name;
}
}
if (searchContent.Description == null) { searchContent.Description = string.Empty;}
if (searchContent.Body == null) { searchContent.Body = string.Empty; }
if (string.IsNullOrEmpty(searchContent.Url))
{
searchContent.Url = string.Empty;
if (pageModule.Page != null)
{
searchContent.Url = $"{(!string.IsNullOrEmpty(pageModule.Page.Path) && !pageModule.Page.Path.StartsWith("/") ? "/" : "")}{pageModule.Page.Path}";
}
}
if (string.IsNullOrEmpty(searchContent.Permissions))
{
searchContent.Permissions = $"{EntityNames.Module}:{pageModule.ModuleId},{EntityNames.Page}:{pageModule.PageId}";
}
if (string.IsNullOrEmpty(searchContent.ContentModifiedBy))
{
searchContent.ContentModifiedBy = pageModule.ModifiedBy;
}
if (searchContent.ContentModifiedOn == DateTime.MinValue)
{
searchContent.ContentModifiedOn = pageModule.ModifiedOn;
}
if (string.IsNullOrEmpty(searchContent.AdditionalContent))
{
searchContent.AdditionalContent = string.Empty;
}
if (removed || pageModule.IsDeleted || !Utilities.IsEffectiveAndNotExpired(pageModule.EffectiveDate, pageModule.ExpiryDate))
{
searchContent.IsDeleted = true;
}
}
private void SaveSearchLastIndexedOn(ISettingRepository settingRepository, int siteId, DateTime lastIndexedOn)
{
var setting = settingRepository.GetSetting(EntityNames.Site, siteId, SearchLastIndexedOnSetting);
if (setting == null)
{
setting = new Setting
{
EntityName = EntityNames.Site,
EntityId = siteId,
SettingName = SearchLastIndexedOnSetting,
SettingValue = Convert.ToString(lastIndexedOn),
};
settingRepository.AddSetting(setting);
}
else
{
setting.SettingValue = Convert.ToString(lastIndexedOn);
settingRepository.UpdateSetting(setting);
}
}
}
}

View File

@ -203,20 +203,22 @@ namespace Oqtane.Infrastructure
} }
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel) if (Enum.Parse<LogLevel>(log.Level) >= notifylevel)
{ {
var subject = $"Site {log.Level} Notification";
string body = $"Log Message: {log.Message}";
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
foreach (var userrole in _userRoles.GetUserRoles(log.SiteId.Value)) if (alias != null)
{ {
if (userrole.Role.Name == RoleNames.Host) subject = $"{alias.Name} Site {log.Level} Notification";
body = $"Log Message: {log.Message}<br /><br />Please visit {alias.Protocol}://{alias.Name}/admin/log?id={log.LogId} for more information";
}
foreach (var userrole in _userRoles.GetUserRoles(RoleNames.Host, log.SiteId.Value))
{ {
var subject = $"{alias.Name} Site {log.Level} Notification";
var url = $"{_accessor.HttpContext.Request.Scheme}://{alias.Name}/admin/log?id={log.LogId}";
string body = $"Log Message: {log.Message}<br /><br />Please visit {url} for more information";
var notification = new Notification(log.SiteId.Value, userrole.User, subject, body); var notification = new Notification(log.SiteId.Value, userrole.User, subject, body);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
} }
} }
}
} }
} }
} }

View File

@ -0,0 +1,774 @@
using Oqtane.Models;
using Oqtane.Infrastructure;
using System.Collections.Generic;
using Oqtane.Shared;
using Oqtane.Documentation;
namespace Oqtane.SiteTemplates
{
[PrivateApi("Mark Site-Template classes as private, since it's not very useful in the public docs")]
public class AdminSiteTemplate : ISiteTemplate
{
public string Name
{
get { return "Admin Site Template"; }
}
public List<PageTemplate> CreateSite(Site site)
{
var pageTemplates = new List<PageTemplate>();
var seed = 1000; // order
// user pages
pageTemplates.Add(new PageTemplate
{
Name = "Login",
Parent = "",
Path = "login",
Order = seed + 1,
Icon = Icons.LockLocked,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Login.Index).ToModuleDefinitionName(), Title = "User Login", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Register",
Parent = "",
Path = "register",
Order = seed + 3,
Icon = Icons.Person,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Register.Index).ToModuleDefinitionName(), Title = "User Registration", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Reset",
Parent = "",
Path = "reset",
Order = seed + 5,
Icon = Icons.Person,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Reset.Index).ToModuleDefinitionName(), Title = "Password Reset", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Profile",
Parent = "",
Path = "profile",
Order = seed + 7,
Icon = Icons.Person,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UserProfile.Index).ToModuleDefinitionName(), Title = "User Profile", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Search",
Parent = "",
Path = "search",
Order = seed + 9,
Icon = Icons.MagnifyingGlass,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = typeof(Oqtane.Modules.Admin.SearchResults.Index).ToModuleDefinitionName(), Title = "Search", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Not Found",
Parent = "",
Path = "404",
Order = seed + 11,
Icon = Icons.X,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Not Found", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = "<p>The page you requested does not exist or you do not have sufficient rights to view it.</p>"
}
}
});
// admin pages
pageTemplates.Add(new PageTemplate
{
Name = "Admin",
Parent = "",
Path = "admin",
Order = seed + 51,
Icon = "",
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Dashboard.Index).ToModuleDefinitionName(), Title = "Admin Dashboard", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Site Settings",
Parent = "Admin",
Order = 1,
Path = "admin/site",
Icon = Icons.Home,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Site.Index).ToModuleDefinitionName(), Title = "Site Settings", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Page Management",
Parent = "Admin",
Order = 3,
Path = "admin/pages",
Icon = Icons.Layers,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Pages.Index).ToModuleDefinitionName(), Title = "Page Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "User Management",
Parent = "Admin",
Order = 5,
Path = "admin/users",
Icon = Icons.People,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Users.Index).ToModuleDefinitionName(), Title = "User Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Profile Management",
Parent = "Admin",
Order = 7,
Path = "admin/profiles",
Icon = Icons.Person,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Profiles.Index).ToModuleDefinitionName(), Title = "Profile Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Role Management",
Parent = "Admin",
Order = 9,
Path = "admin/roles",
Icon = Icons.LockLocked,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Roles.Index).ToModuleDefinitionName(), Title = "Role Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "File Management",
Parent = "Admin",
Order = 11,
Path = "admin/files",
Icon = Icons.File,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Files.Index).ToModuleDefinitionName(), Title = "File Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Recycle Bin",
Parent = "Admin",
Order = 13,
Path = "admin/recyclebin",
Icon = Icons.Trash,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.RecycleBin.Index).ToModuleDefinitionName(), Title = "Recycle Bin", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Url Mappings",
Parent = "Admin",
Order = 15,
Path = "admin/urlmappings",
Icon = Icons.LinkBroken,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.UrlMappings.Index).ToModuleDefinitionName(), Title = "Url Mappings", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Visitor Management",
Parent = "Admin",
Order = 17,
Path = "admin/visitors",
Icon = Icons.Eye,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Visitors.Index).ToModuleDefinitionName(), Title = "Visitor Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Search Settings",
Parent = "Admin",
Order = 19,
Path = "admin/search",
Icon = Icons.MagnifyingGlass,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Search.Index).ToModuleDefinitionName(), Title = "Search Settings", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
// host pages (order starts at 51)
pageTemplates.Add(new PageTemplate
{
Name = "Event Log",
Parent = "Admin",
Order = 51,
Path = "admin/log",
Icon = Icons.List,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Logs.Index).ToModuleDefinitionName(), Title = "Event Log", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Site Management",
Parent = "Admin",
Order = 53,
Path = "admin/sites",
Icon = Icons.Globe,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sites.Index).ToModuleDefinitionName(), Title = "Site Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Module Management",
Parent = "Admin",
Order = 55,
Path = "admin/modules",
Icon = Icons.Browser,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.ModuleDefinitions.Index).ToModuleDefinitionName(), Title = "Module Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Theme Management",
Parent = "Admin",
Order = 57,
Path = "admin/themes",
Icon = Icons.Brush,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Themes.Index).ToModuleDefinitionName(), Title = "Theme Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Language Management",
Parent = "Admin",
Order = 59,
Path = "admin/languages",
Icon = Icons.Text,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Languages.Index).ToModuleDefinitionName(), Title = "Language Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Scheduled Jobs",
Parent = "Admin",
Order = 61,
Path = "admin/jobs",
Icon = Icons.Timer,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Jobs.Index).ToModuleDefinitionName(), Title = "Scheduled Jobs", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "Sql Management",
Parent = "Admin",
Order = 63,
Path = "admin/sql",
Icon = Icons.Spreadsheet,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Sql.Index).ToModuleDefinitionName(), Title = "Sql Management", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "System Info",
Parent = "Admin",
Order = 65,
Path = "admin/system",
Icon = Icons.MedicalCross,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.SystemInfo.Index).ToModuleDefinitionName(), Title = "System Info", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
pageTemplates.Add(new PageTemplate
{
Name = "System Update",
Parent = "Admin",
Order = 67,
Path = "admin/update",
Icon = Icons.Aperture,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
PageTemplateModules = new List<PageTemplateModule>
{
new PageTemplateModule
{
ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Upgrade.Index).ToModuleDefinitionName(), Title = "System Update", Pane = PaneNames.Default,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Host, true),
new Permission(PermissionNames.Edit, RoleNames.Host, true)
},
Content = ""
}
}
});
return pageTemplates;
}
}
}

View File

@ -66,6 +66,9 @@ namespace Oqtane.Infrastructure
case "5.1.0": case "5.1.0":
Upgrade_5_1_0(tenant, scope); Upgrade_5_1_0(tenant, scope);
break; break;
case "5.2.0":
Upgrade_5_2_0(tenant, scope);
break;
} }
} }
} }
@ -136,10 +139,11 @@ namespace Oqtane.Infrastructure
private void Upgrade_3_0_1(Tenant tenant, IServiceScope scope) private void Upgrade_3_0_1(Tenant tenant, IServiceScope scope)
{ {
var pageTemplates = new List<PageTemplate>(); var pageTemplates = new List<PageTemplate>
pageTemplates.Add(new PageTemplate
{ {
new PageTemplate
{
Update = false,
Name = "Url Mappings", Name = "Url Mappings",
Parent = "Admin", Parent = "Admin",
Order = 33, Order = 33,
@ -165,10 +169,10 @@ namespace Oqtane.Infrastructure
Content = "" Content = ""
} }
} }
}); },
new PageTemplate
pageTemplates.Add(new PageTemplate
{ {
Update = false,
Name = "Visitor Management", Name = "Visitor Management",
Parent = "Admin", Parent = "Admin",
Order = 35, Order = 35,
@ -194,13 +198,10 @@ namespace Oqtane.Infrastructure
Content = "" Content = ""
} }
} }
});
var sites = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
foreach (Site site in sites.GetSites().ToList())
{
sites.CreatePages(site, pageTemplates, null);
} }
};
AddPagesToSites(scope, pageTemplates);
} }
private void Upgrade_3_1_3(Tenant tenant, IServiceScope scope) private void Upgrade_3_1_3(Tenant tenant, IServiceScope scope)
@ -383,7 +384,70 @@ namespace Oqtane.Infrastructure
Debug.WriteLine($"Oqtane Error: Error In 5.1.0 Upgrade Logic - {ex}"); Debug.WriteLine($"Oqtane Error: Error In 5.1.0 Upgrade Logic - {ex}");
} }
} }
}
private void Upgrade_5_2_0(Tenant tenant, IServiceScope scope)
{
var pageTemplates = new List<PageTemplate>
{
new PageTemplate
{
Update = false,
Name = "Search",
Parent = "",
Path = "search",
Icon = Icons.MagnifyingGlass,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = typeof(Oqtane.Modules.Admin.SearchResults.Index).ToModuleDefinitionName(), Title = "Search", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}
}
}
},
new PageTemplate
{
Update = false,
Name = "Search Settings",
Parent = "",
Path = "admin/search",
Icon = Icons.MagnifyingGlass,
IsNavigation = false,
IsPersonalizable = false,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
},
PageTemplateModules = new List<PageTemplateModule> {
new PageTemplateModule { ModuleDefinitionName = typeof(Oqtane.Modules.Admin.Search.Index).ToModuleDefinitionName(), Title = "Search Settings", Pane = PaneNames.Default,
PermissionList = new List<Permission> {
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}
}
}
}
};
AddPagesToSites(scope, pageTemplates);
}
private void AddPagesToSites(IServiceScope scope, List<PageTemplate> pageTemplates)
{
var sites = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
foreach (var site in sites.GetSites().ToList())
{
sites.CreatePages(site, pageTemplates, null);
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
@ -25,15 +26,15 @@ namespace Oqtane.Managers
private readonly ITenantManager _tenantManager; private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications; private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders; private readonly IFolderRepository _folders;
private readonly IFileRepository _files;
private readonly IProfileRepository _profiles; private readonly IProfileRepository _profiles;
private readonly ISettingRepository _settings; private readonly ISettingRepository _settings;
private readonly ISiteRepository _sites;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly IMemoryCache _cache;
private readonly IStringLocalizer<UserManager> _localizer; private readonly IStringLocalizer<UserManager> _localizer;
private readonly ISiteRepository _siteRepo;
public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IFileRepository files, IProfileRepository profiles, ISettingRepository settings, ISyncManager syncManager, ILogManager logger, IStringLocalizer<UserManager> localizer, ISiteRepository siteRepo) public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IProfileRepository profiles, ISettingRepository settings, ISiteRepository sites, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IStringLocalizer<UserManager> localizer)
{ {
_users = users; _users = users;
_roles = roles; _roles = roles;
@ -43,17 +44,21 @@ namespace Oqtane.Managers
_tenantManager = tenantManager; _tenantManager = tenantManager;
_notifications = notifications; _notifications = notifications;
_folders = folders; _folders = folders;
_files = files;
_profiles = profiles; _profiles = profiles;
_settings = settings; _settings = settings;
_sites = sites;
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_cache = cache;
_localizer = localizer; _localizer = localizer;
_siteRepo = siteRepo;
} }
public User GetUser(int userid, int siteid) public User GetUser(int userid, int siteid)
{ {
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"user:{userid}:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
User user = _users.GetUser(userid); User user = _users.GetUser(userid);
if (user != null) if (user != null)
{ {
@ -61,11 +66,18 @@ namespace Oqtane.Managers
user.Roles = GetUserRoles(user.UserId, user.SiteId); user.Roles = GetUserRoles(user.UserId, user.SiteId);
} }
return user; return user;
});
} }
public User GetUser(string username, int siteid) public User GetUser(string username, int siteid)
{ {
return GetUser(username, "", siteid); User user = _users.GetUser(username);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
}
return user;
} }
public User GetUser(string username, string email, int siteid) public User GetUser(string username, string email, int siteid)
@ -84,6 +96,8 @@ namespace Oqtane.Managers
string roles = ""; string roles = "";
List<UserRole> userroles = _userRoles.GetUserRoles(userId, siteId).ToList(); List<UserRole> userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
foreach (UserRole userrole in userroles) foreach (UserRole userrole in userroles)
{
if (Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate))
{ {
roles += userrole.Role.Name + ";"; roles += userrole.Role.Name + ";";
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin)) if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin))
@ -95,6 +109,7 @@ namespace Oqtane.Managers
roles += RoleNames.Registered + ";"; roles += RoleNames.Registered + ";";
} }
} }
}
if (roles != "") roles = ";" + roles; if (roles != "") roles = ";" + roles;
return roles; return roles;
} }
@ -153,7 +168,7 @@ namespace Oqtane.Managers
if (User != null) if (User != null)
{ {
string siteName = _siteRepo.GetSite(user.SiteId).Name; string siteName = _sites.GetSite(user.SiteId).Name;
if (!user.EmailConfirmed) if (!user.EmailConfirmed)
{ {
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
@ -201,6 +216,8 @@ namespace Oqtane.Managers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null) if (identityuser != null)
{ {
var alias = _tenantManager.GetAlias();
if (!string.IsNullOrEmpty(user.Password)) if (!string.IsNullOrEmpty(user.Password))
{ {
var validator = new PasswordValidator<IdentityUser>(); var validator = new PasswordValidator<IdentityUser>();
@ -224,7 +241,6 @@ namespace Oqtane.Managers
// if email address changed and it is not confirmed, verification is required for new email address // if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed) if (!user.EmailConfirmed)
{ {
var alias = _tenantManager.GetAlias();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
@ -242,6 +258,7 @@ namespace Oqtane.Managers
user = _users.UpdateUser(user); user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
_cache.Remove($"user:{user.UserId}:{alias.SiteKey}");
user.Password = ""; // remove sensitive information user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
} }
@ -324,7 +341,7 @@ namespace Oqtane.Managers
_users.UpdateUser(user); _users.UpdateUser(user);
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
string url = alias.Protocol + alias.Name; string url = alias.Protocol + alias.Name;
string siteName = _siteRepo.GetSite(alias.SiteId).Name; string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["TwoFactorEmailSubject"]; string subject = _localizer["TwoFactorEmailSubject"];
subject = subject.Replace("[SiteName]", siteName); subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["TwoFactorEmailBody"].Value; string body = _localizer["TwoFactorEmailBody"].Value;
@ -376,7 +393,7 @@ namespace Oqtane.Managers
user = _users.GetUser(user.Username); user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _siteRepo.GetSite(alias.SiteId).Name; string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["UserLockoutEmailSubject"]; string subject = _localizer["UserLockoutEmailSubject"];
subject = subject.Replace("[SiteName]", siteName); subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["UserLockoutEmailBody"].Value; string body = _localizer["UserLockoutEmailBody"].Value;
@ -429,7 +446,7 @@ namespace Oqtane.Managers
user = _users.GetUser(user.Username); user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _siteRepo.GetSite(alias.SiteId).Name; string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotPasswordEmailSubject"]; string subject = _localizer["ForgotPasswordEmailSubject"];
subject = subject.Replace("[SiteName]", siteName); subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotPasswordEmailBody"].Value; string body = _localizer["ForgotPasswordEmailBody"].Value;

View File

@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentEntityBuilder : BaseEntityBuilder<SearchContentEntityBuilder>
{
private const string _entityTableName = "SearchContent";
private readonly PrimaryKey<SearchContentEntityBuilder> _primaryKey = new("PK_SearchContent", x => x.SearchContentId);
public SearchContentEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override SearchContentEntityBuilder BuildTable(ColumnsBuilder table)
{
SearchContentId = AddAutoIncrementColumn(table, "SearchContentId");
SiteId = AddIntegerColumn(table, "SiteId");
EntityName = AddStringColumn(table, "EntityName", 50);
EntityId = AddStringColumn(table, "EntityId", 50);
Title = AddStringColumn(table, "Title", 200);
Description = AddMaxStringColumn(table, "Description");
Body = AddMaxStringColumn(table, "Body");
Url = AddStringColumn(table, "Url", 500);
Permissions = AddStringColumn(table, "Permissions", 100);
ContentModifiedBy = AddStringColumn(table, "ContentModifiedBy", 256);
ContentModifiedOn = AddDateTimeColumn(table, "ContentModifiedOn");
AdditionalContent = AddMaxStringColumn(table, "AdditionalContent");
CreatedOn = AddDateTimeColumn(table, "CreatedOn");
return this;
}
public OperationBuilder<AddColumnOperation> SearchContentId { get; private set; }
public OperationBuilder<AddColumnOperation> SiteId { get; private set; }
public OperationBuilder<AddColumnOperation> EntityName { get; private set; }
public OperationBuilder<AddColumnOperation> EntityId { get; private set; }
public OperationBuilder<AddColumnOperation> Title { get; private set; }
public OperationBuilder<AddColumnOperation> Description { get; private set; }
public OperationBuilder<AddColumnOperation> Body { get; private set; }
public OperationBuilder<AddColumnOperation> Url { get; private set; }
public OperationBuilder<AddColumnOperation> Permissions { get; private set; }
public OperationBuilder<AddColumnOperation> ContentModifiedBy { get; private set; }
public OperationBuilder<AddColumnOperation> ContentModifiedOn { get; private set; }
public OperationBuilder<AddColumnOperation> AdditionalContent { get; private set; }
public OperationBuilder<AddColumnOperation> CreatedOn { get; private set; }
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentPropertyEntityBuilder : BaseEntityBuilder<SearchContentPropertyEntityBuilder>
{
private const string _entityTableName = "SearchContentProperty";
private readonly PrimaryKey<SearchContentPropertyEntityBuilder> _primaryKey = new("PK_SearchContentProperty", x => x.PropertyId);
private readonly ForeignKey<SearchContentPropertyEntityBuilder> _searchContentForeignKey = new("FK_SearchContentProperty_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade);
public SearchContentPropertyEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_searchContentForeignKey);
}
protected override SearchContentPropertyEntityBuilder BuildTable(ColumnsBuilder table)
{
PropertyId = AddAutoIncrementColumn(table, "PropertyId");
SearchContentId = AddIntegerColumn(table, "SearchContentId");
Name = AddStringColumn(table, "Name", 50);
Value = AddStringColumn(table, "Value", 50);
return this;
}
public OperationBuilder<AddColumnOperation> PropertyId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchContentId { get; private set; }
public OperationBuilder<AddColumnOperation> Name { get; private set; }
public OperationBuilder<AddColumnOperation> Value { get; private set; }
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentWordEntityBuilder : BaseEntityBuilder<SearchContentWordEntityBuilder>
{
private const string _entityTableName = "SearchContentWord";
private readonly PrimaryKey<SearchContentWordEntityBuilder> _primaryKey = new("PK_SearchContentWord", x => x.SearchContentWordId);
private readonly ForeignKey<SearchContentWordEntityBuilder> _foreignKey1 = new("FK_SearchContentWord_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade);
private readonly ForeignKey<SearchContentWordEntityBuilder> _foreignKey2 = new("FK_SearchContentWord_SearchWord", x => x.SearchWordId, "SearchWord", "SearchWordId", ReferentialAction.Cascade);
public SearchContentWordEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_foreignKey1);
ForeignKeys.Add(_foreignKey2);
}
protected override SearchContentWordEntityBuilder BuildTable(ColumnsBuilder table)
{
SearchContentWordId = AddAutoIncrementColumn(table, "SearchContentWordId");
SearchContentId = AddIntegerColumn(table, "SearchContentId");
SearchWordId = AddIntegerColumn(table, "SearchWordId");
Count = AddIntegerColumn(table, "Count");
CreatedOn = AddDateTimeColumn(table, "CreatedOn");
ModifiedOn = AddDateTimeColumn(table, "ModifiedOn");
return this;
}
public OperationBuilder<AddColumnOperation> SearchContentWordId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchContentId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchWordId { get; private set; }
public OperationBuilder<AddColumnOperation> Count { get; private set; }
public OperationBuilder<AddColumnOperation> CreatedOn { get; private set; }
public OperationBuilder<AddColumnOperation> ModifiedOn { get; private set; }
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchWordEntityBuilder : BaseEntityBuilder<SearchWordEntityBuilder>
{
private const string _entityTableName = "SearchWord";
private readonly PrimaryKey<SearchWordEntityBuilder> _primaryKey = new("PK_SearchWord", x => x.SearchWordId);
public SearchWordEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override SearchWordEntityBuilder BuildTable(ColumnsBuilder table)
{
SearchWordId = AddAutoIncrementColumn(table, "SearchWordId");
Word = AddStringColumn(table, "Word", 255);
CreatedOn = AddDateTimeColumn(table, "CreatedOn");
return this;
}
public OperationBuilder<AddColumnOperation> SearchWordId { get; private set; }
public OperationBuilder<AddColumnOperation> Word { get; private set; }
public OperationBuilder<AddColumnOperation> CreatedOn { get; private set; }
}
}

View File

@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.05.02.00.01")]
public class AddSearchTables : MultiDatabaseMigration
{
public AddSearchTables(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var searchContentEntityBuilder = new SearchContentEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentEntityBuilder.Create();
var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentPropertyEntityBuilder.Create();
var searchWordEntityBuilder = new SearchWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchWordEntityBuilder.Create();
searchWordEntityBuilder.AddIndex("IX_SearchWord", "Word", true);
var searchContentWordEntityBuilder = new SearchContentWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordEntityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var searchContentWordEntityBuilder = new SearchContentWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordEntityBuilder.Drop();
var searchWordEntityBuilder = new SearchWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchWordEntityBuilder.DropIndex("IX_SearchWord");
searchWordEntityBuilder.Drop();
var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentPropertyEntityBuilder.Drop();
var searchContentEntityBuilder = new SearchContentEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentEntityBuilder.Drop();
}
}
}

View File

@ -0,0 +1,75 @@
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Interfaces;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Oqtane.Shared;
using System.IO;
namespace Oqtane.Modules.Admin.Files.Manager
{
public class FileManager : ISearchable
{
private readonly IFolderRepository _folderRepository;
private readonly IFileRepository _fileRepository;
private const string DocumentExtensions = ".txt,.htm,.html";
public FileManager(IFolderRepository folderRepository, IFileRepository fileRepository)
{
_folderRepository = folderRepository;
_fileRepository = fileRepository;
}
public Task<List<SearchContent>> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
{
var searchContents = new List<SearchContent>();
var folders = _folderRepository.GetFolders(pageModule.Module.SiteId);
foreach ( var folder in folders)
{
bool changed = false;
bool removed = false;
if (folder.ModifiedOn >= lastIndexedOn)
{
changed = true;
removed = folder.IsDeleted.Value;
}
var files = _fileRepository.GetFiles(folder.FolderId);
foreach (var file in files)
{
if (file.ModifiedOn >= lastIndexedOn || changed)
{
var path = folder.Path + file.Name;
var body = "";
if (DocumentExtensions.Contains(Path.GetExtension(file.Name)))
{
// get the contents of the file
body = System.IO.File.ReadAllText(_fileRepository.GetFilePath(file));
}
var searchContent = new SearchContent
{
SiteId = folder.SiteId,
EntityName = EntityNames.File,
EntityId = file.FileId.ToString(),
Title = path,
Body = body,
Url = $"{Constants.FileUrl}{folder.Path}{file.Name}",
Permissions = $"{EntityNames.Folder}:{folder.FolderId}",
ContentModifiedBy = file.ModifiedBy,
ContentModifiedOn = file.ModifiedOn,
IsDeleted = (removed || file.IsDeleted.Value)
};
searchContents.Add(searchContent);
}
}
}
return Task.FromResult(searchContents);
}
}
}

View File

@ -8,20 +8,30 @@ using Oqtane.Shared;
using Oqtane.Migrations.Framework; using Oqtane.Migrations.Framework;
using Oqtane.Documentation; using Oqtane.Documentation;
using System.Linq; using System.Linq;
using Oqtane.Interfaces;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
// ReSharper disable ConvertToUsingDeclaration // ReSharper disable ConvertToUsingDeclaration
namespace Oqtane.Modules.HtmlText.Manager namespace Oqtane.Modules.HtmlText.Manager
{ {
[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 HtmlTextManager : MigratableModuleBase, IInstallable, IPortable public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISearchable
{ {
private readonly IServiceProvider _serviceProvider;
private readonly IHtmlTextRepository _htmlText; private readonly IHtmlTextRepository _htmlText;
private readonly IDBContextDependencies _DBContextDependencies; private readonly IDBContextDependencies _DBContextDependencies;
private readonly ISqlRepository _sqlRepository; private readonly ISqlRepository _sqlRepository;
public HtmlTextManager(IHtmlTextRepository htmlText, IDBContextDependencies DBContextDependencies, ISqlRepository sqlRepository) public HtmlTextManager(
IServiceProvider serviceProvider,
IHtmlTextRepository htmlText,
IDBContextDependencies DBContextDependencies,
ISqlRepository sqlRepository)
{ {
_serviceProvider = serviceProvider;
_htmlText = htmlText; _htmlText = htmlText;
_DBContextDependencies = DBContextDependencies; _DBContextDependencies = DBContextDependencies;
_sqlRepository = sqlRepository; _sqlRepository = sqlRepository;
@ -39,6 +49,28 @@ namespace Oqtane.Modules.HtmlText.Manager
return content; return content;
} }
public Task<List<SearchContent>> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
{
var searchContents = new List<SearchContent>();
var htmltexts = _htmlText.GetHtmlTexts(pageModule.ModuleId);
if (htmltexts != null && htmltexts.Any())
{
var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First();
if (htmltext.CreatedOn >= lastIndexedOn)
{
searchContents.Add(new SearchContent
{
Body = htmltext.Content,
ContentModifiedBy = htmltext.CreatedBy,
ContentModifiedOn = htmltext.CreatedOn
});
}
}
return Task.FromResult(searchContents);
}
public void ImportModule(Module module, string content, string version) public void ImportModule(Module module, string content, string version)
{ {
content = WebUtility.HtmlDecode(content); content = WebUtility.HtmlDecode(content);

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>5.1.2</Version> <Version>5.2.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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/v5.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -33,19 +33,20 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" /> <EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.5" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.7" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.5" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.5" /> <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.7" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.8" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

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