Merge pull request #4552 from oqtane/dev

5.2.1 Release
This commit is contained in:
Shaun Walker 2024-08-22 12:15:15 -04:00 committed by GitHub
commit 1aee385679
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 885 additions and 683 deletions

View File

@ -40,10 +40,14 @@
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveFile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
@if (_name.ToLower().EndsWith(".zip"))
{
<button type="button" class="btn btn-primary mx-1" @onclick="UnzipFile">Unzip</button>
}
<br /><br />
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>
</form>
}
@ -126,4 +130,18 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task UnzipFile()
{
try
{
await FileService.UnzipFileAsync(_fileId);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Unzipping File {FileId} {Error}", _fileId, ex.Message);
AddModuleMessage(Localizer["Error.File.Unzip"], MessageType.Error);
}
}
}

View File

@ -8,67 +8,77 @@
@if (_folders != null)
{
<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="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required>
@if (PageState.QueryString.ContainsKey("id"))
{
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
}
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<TabStrip>
<TabPanel Name="Settings" ResourceKey="Settings" Heading="Settings">
<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="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required>
@if (PageState.QueryString.ContainsKey("id"))
{
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
}
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="Select the folder type. Private folders are only accessible by authorized users. Public folders can be accessed by all users" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
@if (PageState.QueryString.ContainsKey("id"))
{
<input id="type" class="form-control" readonly @bind="@_type" />
}
else
{
<select id="type" class="form-select" @bind="@_type" required>
<option value="@FolderTypes.Private">@Localizer[FolderTypes.Private]</option>
<option value="@FolderTypes.Public">@Localizer[FolderTypes.Public]</option>
</select>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required />
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="Select the folder type. Private folders are only accessible by authorized users. Public folders can be accessed by all users" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
@if (PageState.QueryString.ContainsKey("id"))
{
<input id="type" class="form-control" readonly @bind="@_type" />
}
else
{
<select id="type" class="form-select" @bind="@_type" required>
<option value="@FolderTypes.Private">@Localizer[FolderTypes.Private]</option>
<option value="@FolderTypes.Public">@Localizer[FolderTypes.Public]</option>
</select>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="col-sm-12">
<Label Class="col-sm-3" For="permissions" HelpText="Select the permissions you want for the folder" ResourceKey="Permissions">Permissions: </Label>
@if (PageState.QueryString.ContainsKey("id"))
{
<br />
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>
}
</form>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Folder" PermissionNames="@(PermissionNames.Browse + "," + PermissionNames.View + "," + PermissionNames.Edit)" PermissionList="@_permissions" @ref="_permissionGrid" />
</div>
</div>
</div>
</form>
<br /><br />
</TabPanel>
</TabStrip>
<br />
@if (!_isSystem)
{
<button type="button" class="btn btn-success" @onclick="SaveFolder">@SharedLocalizer["Save"]</button>
@ -80,11 +90,6 @@
@((MarkupString)"&nbsp;")
<ActionDialog Header="Delete Folder" Message="Are You Sure You Wish To Delete This Folder?" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFolder())" ResourceKey="DeleteFolder" />
}
<br /><br />
@if (PageState.QueryString.ContainsKey("id"))
{
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>
}
}
@code {

View File

@ -152,7 +152,8 @@ else
private async Task Refresh()
{
ShowProgressIndicator();
await GetJobs();
StateHasChanged();
HideProgressIndicator();
}
}

View File

@ -78,7 +78,8 @@ else
private async Task Refresh()
{
ShowProgressIndicator();
await GetJobLogs();
StateHasChanged();
HideProgressIndicator();
}
}

View File

@ -208,17 +208,20 @@
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
// return url is not specified if user navigated directly to login page
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(PageState.ReturnUrl, true));
NavigationManager.NavigateTo(NavigateUrl(returnurl, true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(PageState.ReturnUrl) };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(returnurl) };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}

View File

@ -5,7 +5,8 @@
@inject NavigationManager NavigationManager
@inject IPageService PageService
@inject IPageModuleService PageModuleService
@inject IThemeService ThemeService
@inject IModuleService ModuleService
@inject IThemeService ThemeService
@inject ISystemService SystemService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -417,7 +418,8 @@
_permissions = _page.PermissionList;
// page modules
_pageModules = PageState.Modules;
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
_pageModules = modules.Where(item => item.PageId == _page.PageId && !item.IsDeleted).ToList();
// audit
_createdby = _page.CreatedBy;

View File

@ -53,35 +53,36 @@
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<ActionDialog Header="Reindex" Message="Are You Sure You Wish To Reindex Search Content?" Action="Reindex" Class="btn btn-danger" OnClick="@(async () => await Reindex())" ResourceKey="Reindex" />
<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;
private string _enabled = "True";
private string _lastIndexedOn = "";
private string _ignorePages = "";
private string _ignoreEntities = "";
private string _minimumWordLength = "3";
private string _ignoreWords = "the,be,to,of,and,a,i,in,that,have,it,for,not,on,with,he,as,you,do,at,this,but,his,by,from,they,we,say,her,she,or,an,will,my,one,all,would,there,their,what,so,up,out,if,about,who,get,which,go,me,when,make,can,like,time,no,just,him,know,take,people,into,year,your,good,some,could,them,see,other,than,then,now,look,only,come,its,over,think,also,back,after,use,two,how,our,work,first,well,way,even,new,want,because,any,these,give,day,most,us";
protected override async Task OnInitializedAsync()
{
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", "");
_enabled = SettingService.GetSetting(settings, "Search_Enabled", _enabled);
_lastIndexedOn = SettingService.GetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn);
_ignorePages = SettingService.GetSetting(settings, "Search_IgnorePages", _ignorePages);
_ignoreEntities = SettingService.GetSetting(settings, "Search_IgnoreEntities", _ignoreEntities);
_minimumWordLength = SettingService.GetSetting(settings, "Search_MininumWordLength", _minimumWordLength);
_ignoreWords = SettingService.GetSetting(settings, "Search_IgnoreWords", _ignoreWords);
}
private async Task Save()
{
try
{
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);
@ -93,10 +94,28 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Save"], MessageType.Success);
}
catch (Exception ex)
{
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Search Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}
private async Task Reindex()
{
try
{
_lastIndexedOn = DateTime.MinValue.ToString();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Message.Reindex"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Search Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}

View File

@ -102,7 +102,7 @@
private void Search()
{
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, $"page=1&q={_keywords}"));
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, $"page=1&q={WebUtility.UrlEncode(_keywords)}"));
}
private async Task PerformSearch()

View File

@ -10,13 +10,13 @@
<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 />
<input id="includeentities" type="text" class="form-control" @bind="@_includeEntities" />
</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>
<input id="excludeentities" class="form-control" @bind="@_excludeEntities" />
</div>
</div>
<div class="row mb-1 align-items-center">

View File

@ -309,7 +309,7 @@
}
</td>
<td>@context.Name</td>
<td>@context.IsDefault</td>
<td>@((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"])</td>
}
else
{

View File

@ -51,25 +51,25 @@ else
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Header</h5>
<form method="post" @formname="@($"ActionDialogCloseForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<form class="app-form-inline" method="post" @formname="@($"ActionDialogCloseForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="modal-header">
<h5 class="modal-title">@Header</h5>
<button type="submit" class="btn-close" aria-label="Close"></button>
</form>
</div>
</div>
</form>
<div class="modal-body">
<p>@Message</p>
</div>
<div class="modal-footer">
@if (!string.IsNullOrEmpty(Action))
{
<form method="post" @formname="@($"ActionDialogConfirmForm{Id}")" @onsubmit="Confirm" data-enhance>
<form method="post" @formname="@($"ActionDialogConfirmForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="Confirm" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button>
</form>
}
<form method="post" @formname="@($"ActionDialogCancelForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<form method="post" @formname="@($"ActionDialogCancelForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button>
</form>
@ -87,7 +87,7 @@ else
}
else
{
<form method="post" @formname="@($"ActionDialogActionForm{Id}")" @onsubmit="DisplayModal" data-enhance>
<form method="post" class="app-form-inline" @formname="@($"ActionDialogActionForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_openIconSpan) @_openText</button>
</form>
@ -199,6 +199,8 @@ else
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized();
if (string.IsNullOrEmpty(Id)) Id = "1";
if (PageState.QueryString.ContainsKey("dialog"))
{
_visible = (PageState.QueryString["dialog"] == Id);

View File

@ -314,7 +314,6 @@
await SetImage();
await OnSelect.InvokeAsync(FileId);
StateHasChanged();
}
private async Task SetImage()

View File

@ -111,7 +111,7 @@
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync()
protected override async Task OnParametersSetAsync()
{
if (!string.IsNullOrEmpty(Permissions))
{

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>5.2.0</Version>
<Version>5.2.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,9 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
</ItemGroup>

View File

@ -150,14 +150,11 @@
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the folder name</value>
</data>
<data name="Permissions.HelpText" xml:space="preserve">
<value>Select the permissions you want for the folder</value>
</data>
<data name="Parent.Text" xml:space="preserve">
<value>Parent: </value>
</data>
<data name="Permissions.Text" xml:space="preserve">
<value>Permissions: </value>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="DeleteFolder.Header" xml:space="preserve">
<value>Delete Folder</value>
@ -198,4 +195,7 @@
<data name="Message.Folder.Duplicate" xml:space="preserve">
<value>Folder Name Specified Already Exists In Parent</value>
</data>
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
</root>

View File

@ -165,4 +165,16 @@
<data name="SearchProvider.Text" xml:space="preserve">
<value>Search Provider:</value>
</data>
<data name="Message.Reindex" xml:space="preserve">
<value>The search index will be rebuilt for this site. Please be patient during the reindexing process.</value>
</data>
<data name="Reindex.Text" xml:space="preserve">
<value>Reindex</value>
</data>
<data name="Reindex.Header" xml:space="preserve">
<value>Reindex</value>
</data>
<data name="Reindex.Message" xml:space="preserve">
<value>Are You Sure You Wish To Reindex Search Content?</value>
</data>
</root>

View File

@ -213,6 +213,9 @@
<data name="Reset" xml:space="preserve">
<value>Reset</value>
</data>
<data name="Search Settings" xml:space="preserve">
<value>Search Settings</value>
</data>
<data name="Search" xml:space="preserve">
<value>Search</value>
</data>

View File

@ -75,5 +75,10 @@ namespace Oqtane.Services
{
return await GetByteArrayAsync($"{Apiurl}/download/{fileId}");
}
public async Task UnzipFileAsync(int fileId)
{
await PutAsync($"{Apiurl}/unzip/{fileId}");
}
}
}

View File

@ -92,5 +92,13 @@ namespace Oqtane.Services
/// </param>
/// <returns></returns>
Task<List<File>> GetFilesAsync(int siteId, string folderPath);
/// <summary>
/// Unzips the contents of a zip file
/// </summary>
/// <param name="fileId">Reference to the <see cref="File"/></param>
/// </param>
/// <returns></returns>
Task UnzipFileAsync(int fileId);
}
}

View File

@ -179,6 +179,17 @@ namespace Oqtane.Services
/// <returns></returns>
Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId);
/// <summary>
/// Updates setting for a given entityName and Id
/// </summary>
/// <param name="entityName"></param>
/// <param name="entityId"></param>
/// <param name="settingName"></param>
/// <param name="settingValue"></param>
/// <param name="isPrivate"></param>
/// <returns></returns>
Task AddOrUpdateSettingAsync(string entityName, int entityId, string settingName, string settingValue, bool isPrivate);
/// <summary>
/// Returns a specific setting
/// </summary>

View File

@ -35,6 +35,7 @@ namespace Oqtane.Services
if (_factory != null)
{
var client = _factory.CreateClient("oqtane");
client.BaseAddress = new Uri(_siteState.Alias.Protocol + _siteState.Alias.Name);
if (!client.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
{
client.DefaultRequestHeaders.Add(Constants.AntiForgeryTokenHeaderName, _siteState.AntiForgeryToken);

View File

@ -12,7 +12,7 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SettingService : ServiceBase, ISettingService
{
public SettingService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public SettingService(HttpClient http, SiteState siteState) : base(http, siteState) {}
private string Apiurl => CreateApiUrl("Setting");
@ -134,7 +134,7 @@ namespace Oqtane.Services
public async Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId)
{
var dictionary = new Dictionary<string, string>();
var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
var settings = await GetSettingsAsync(entityName, entityId, "");
if (settings != null)
{
foreach (Setting setting in settings.OrderBy(item => item.SettingName).ToList())
@ -147,7 +147,7 @@ namespace Oqtane.Services
public async Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId)
{
var settingsList = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
var settingsList = await GetSettingsAsync(entityName, entityId, "");
foreach (KeyValuePair<string, string> kvp in settings)
{
@ -192,14 +192,14 @@ namespace Oqtane.Services
}
}
public async Task AddOrUpdateSettingAsync(string entityName, int entityId, string settingName, string settingValue, bool isPrivate)
{
await PutAsync($"{Apiurl}/{entityName}/{entityId}/{settingName}/{settingValue}/{isPrivate}");
}
public async Task DeleteSettingAsync(string entityName, int entityId, string settingName)
{
var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
var setting = settings.FirstOrDefault(item => item.SettingName == settingName);
if (setting != null)
{
await DeleteAsync($"{Apiurl}/{setting.SettingId}/{entityName}");
}
await DeleteAsync($"{Apiurl}/{entityName}/{entityId}/{settingName}");
}
public async Task<List<Setting>> GetSettingsAsync(string entityName, int entityId, string settingName)

View File

@ -134,8 +134,7 @@
if (PageState.User != null)
{
// preserve edit mode for authenticated users
var userSettings = new Dictionary<string, string> { { "CP-editmode", (PageState.EditMode) ? PageState.Page.PageId.ToString() : "-1" } };
await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId);
await SettingService.AddOrUpdateSettingAsync(EntityNames.User, PageState.User.UserId, "CP-editmode", (PageState.EditMode) ? PageState.Page.PageId.ToString() : "-1", false);
}
// preserve other querystring parameters

View File

@ -52,7 +52,7 @@
{
if (_searchResultsPage != null)
{
var url = NavigateUrl(_searchResultsPage.Path, $"q={_keywords}");
var url = NavigateUrl(_searchResultsPage.Path, $"q={WebUtility.UrlEncode(_keywords)}");
NavigationManager.NavigateTo(url);
}
}

View File

@ -261,17 +261,17 @@
// edit mode
if (user != null)
{
if (querystring.ContainsKey("editmode") && querystring["edit"] == "true")
var editpageid = user.Settings.ContainsKey("CP-editmode") ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1;
if ((querystring.ContainsKey("edit") && querystring["edit"] == "true") || page.PageId == editpageid)
{
editmode = true;
}
else
{
editmode = (page.PageId == ((user.Settings.ContainsKey("CP-editmode")) ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1));
if (!editmode)
if (editpageid != -1)
{
var userSettings = new Dictionary<string, string> { { "CP-editmode", "-1" } };
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId);
// reset edit mode page
await SettingService.AddOrUpdateSettingAsync(EntityNames.User, user.UserId, "CP-editmode", "-1", false);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -429,7 +429,10 @@
new CookieOptions()
{
Expires = DateTimeOffset.UtcNow.AddYears(10),
IsEssential = true
IsEssential = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = true // Optional: Helps mitigate XSS attacks
}
);
}
@ -516,9 +519,9 @@
"<script>" + Environment.NewLine +
" // Blazor Static Rendering Scroll Position" + Environment.NewLine +
" window.interceptNavigation = () => {" + Environment.NewLine +
" let currentUrl = window.location.href;" + Environment.NewLine +
" let currentUrl = window.location.pathname;" + Environment.NewLine +
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
" let newUrl = window.location.href;" + Environment.NewLine +
" let newUrl = window.location.pathname;" + Environment.NewLine +
" if (currentUrl != newUrl) {" + Environment.NewLine +
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
" }" + Environment.NewLine +
@ -601,9 +604,19 @@
private void SetLocalizationCookie(string culture)
{
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = true // Optional: Helps mitigate XSS attacks
};
Context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)));
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
cookieOptions
);
}
private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action)

View File

@ -21,6 +21,7 @@ using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Png;
using System.Net.Http;
using Microsoft.AspNetCore.Cors;
using System.IO.Compression;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@ -171,18 +172,30 @@ namespace Oqtane.Controllers
{
if (_userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit))
{
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
if (HasValidFileExtension(file.Name) && file.Name.IsPathOrFileValid())
{
file = CreateFile(file.Name, folder.FolderId, filepath);
file = _files.AddFile(file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Added {File}", file);
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
{
file = CreateFile(file.Name, folder.FolderId, filepath);
if (file != null)
{
file = _files.AddFile(file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Added {File}", file);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Does Not Exist At Path {FilePath}", filepath);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
file = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Does Not Exist At Path {FilePath}", filepath);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", file.Name);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
file = null;
}
}
@ -213,29 +226,38 @@ namespace Oqtane.Controllers
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, File.FolderId, PermissionNames.Edit) // ensure user had edit rights to original folder
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit)) // ensure user has edit rights to new folder
{
if (File.Name != file.Name || File.FolderId != file.FolderId)
if (HasValidFileExtension(file.Name) && file.Name.IsPathOrFileValid())
{
file.Folder = _folders.GetFolder(file.FolderId, false);
string folderpath = _folders.GetFolderPath(file.Folder);
if (!Directory.Exists(folderpath))
if (File.Name != file.Name || File.FolderId != file.FolderId)
{
Directory.CreateDirectory(folderpath);
file.Folder = _folders.GetFolder(file.FolderId, false);
string folderpath = _folders.GetFolderPath(file.Folder);
if (!Directory.Exists(folderpath))
{
Directory.CreateDirectory(folderpath);
}
System.IO.File.Move(_files.GetFilePath(File), Path.Combine(folderpath, file.Name));
}
System.IO.File.Move(_files.GetFilePath(File), Path.Combine(folderpath, file.Name));
}
var newfile = CreateFile(File.Name, file.Folder.FolderId, _files.GetFilePath(file));
if (newfile != null)
var newfile = CreateFile(File.Name, file.Folder.FolderId, _files.GetFilePath(file));
if (newfile != null)
{
file.Extension = newfile.Extension;
file.Size = newfile.Size;
file.ImageWidth = newfile.ImageWidth;
file.ImageHeight = newfile.ImageHeight;
}
file = _files.UpdateFile(file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "File Updated {File}", file);
}
else
{
file.Extension = newfile.Extension;
file.Size = newfile.Size;
file.ImageWidth = newfile.ImageWidth;
file.ImageHeight = newfile.ImageHeight;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", file.Name);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
file = null;
}
file = _files.UpdateFile(file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "File Updated {File}", file);
}
else
{
@ -247,6 +269,57 @@ namespace Oqtane.Controllers
return file;
}
// PUT api/<controller>/unzip/5
[HttpPut("unzip/{id}")]
[Authorize(Roles = RoleNames.Admin)]
public void Unzip(int id)
{
var zipfile = _files.GetFile(id, false);
if (zipfile != null && zipfile.Folder.SiteId == _alias.SiteId && zipfile.Extension.ToLower() == "zip")
{
// extract files
string folderpath = _folders.GetFolderPath(zipfile.Folder);
using (ZipArchive archive = ZipFile.OpenRead(Path.Combine(folderpath, zipfile.Name)))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (HasValidFileExtension(entry.Name) && entry.Name.IsPathOrFileValid())
{
entry.ExtractToFile(Path.Combine(folderpath, entry.Name), true);
var file = CreateFile(entry.Name, zipfile.Folder.FolderId, Path.Combine(folderpath, entry.Name));
if (file != null)
{
if (file.FileId == 0)
{
file = _files.AddFile(file);
}
else
{
file = _files.UpdateFile(file);
}
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Extracted {File}", file);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", entry.Name);
}
}
}
// delete zip file
_files.DeleteFile(zipfile.FileId);
System.IO.File.Delete(Path.Combine(folderpath, zipfile.Name));
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Zip File Removed {File}", zipfile);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Unzip Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Registered)]
@ -289,64 +362,55 @@ namespace Oqtane.Controllers
folder = _folders.GetFolder(FolderId);
}
var _UploadableFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
_UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles;
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList))
{
string folderPath = _folders.GetFolderPath(folder);
CreateDirectory(folderPath);
if (string.IsNullOrEmpty(name))
{
name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
// check for allowable file extensions
if (!_UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return file;
}
if (!name.IsPathOrFileValid())
if (HasValidFileExtension(name) && name.IsPathOrFileValid())
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, $"File Could Not Be Downloaded From Url Due To Its File Name Not Allowed {url}");
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
return file;
}
try
{
string targetPath = Path.Combine(folderPath, name);
// remove file if it already exists
if (System.IO.File.Exists(targetPath))
try
{
System.IO.File.Delete(targetPath);
}
string folderPath = _folders.GetFolderPath(folder);
CreateDirectory(folderPath);
using (var client = new HttpClient())
{
using (var stream = await client.GetStreamAsync(url))
string targetPath = Path.Combine(folderPath, name);
if (System.IO.File.Exists(targetPath))
{
using (var fileStream = new FileStream(targetPath, FileMode.CreateNew))
System.IO.File.Delete(targetPath);
}
using (var client = new HttpClient())
{
using (var stream = await client.GetStreamAsync(url))
{
await stream.CopyToAsync(fileStream);
using (var fileStream = new FileStream(targetPath, FileMode.CreateNew))
{
await stream.CopyToAsync(fileStream);
}
}
}
}
file = CreateFile(name, folder.FolderId, targetPath);
if (file != null)
file = CreateFile(name, folder.FolderId, targetPath);
if (file != null)
{
file = _files.AddFile(file);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Downloaded {File}", file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
}
}
catch (Exception ex)
{
file = _files.AddFile(file);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Could Not Be Downloaded From Url {Url} {Error}", url, ex.Message);
}
}
catch (Exception ex)
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Could Not Be Downloaded From Url {Url} {Error}", url, ex.Message);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", name);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
file = null;
}
}
else
@ -368,21 +432,11 @@ namespace Oqtane.Controllers
return;
}
// Get the UploadableFiles extensions
string _UploadableFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
_UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles;
// ensure filename is valid
string token = ".part_";
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token))
{
return;
}
// check for allowable file extensions (ignore token)
var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", "");
if (!_UploadableFiles.Split(',').Contains(extension.ToLower()))
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return;
}
@ -428,7 +482,7 @@ namespace Oqtane.Controllers
{
file = _files.UpdateFile(file);
}
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Upload Succeeded {File}", Path.Combine(folderPath, upload));
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload));
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
}
}
@ -838,10 +892,18 @@ namespace Oqtane.Controllers
{
_files.DeleteFile(file.FileId);
}
file = null;
_logger.Log(LogLevel.Warning, this, LogFunction.Create, "File Exceeds Folder Capacity And Has Been Removed {Folder} {File}", folder, filepath);
}
return file;
}
private bool HasValidFileExtension(string fileName)
{
var _uploadableFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
return _uploadableFiles.Split(',').Contains(Path.GetExtension(fileName).ToLower().Replace(".", ""));
}
}
}

View File

@ -109,7 +109,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.AddSetting(setting);
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Create);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Setting Added {Setting}", setting);
}
else
@ -131,7 +131,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && setting.SettingId == id && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.UpdateSetting(setting);
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Update);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Updated {Setting}", setting);
}
else
@ -146,15 +146,53 @@ namespace Oqtane.Controllers
return setting;
}
// DELETE api/<controller>/5/xxx
[HttpDelete("{id}/{entityName}")]
public void Delete(string entityName, int id)
// PUT api/<controller>/site/1/settingname/x/false
[HttpPut("{entityName}/{entityId}/{settingName}/{settingValue}/{isPrivate}")]
public void Put(string entityName, int entityId, string settingName, string settingValue, bool isPrivate)
{
Setting setting = _settings.GetSetting(entityName, id);
if (IsAuthorized(entityName, entityId, PermissionNames.Edit))
{
Setting setting = _settings.GetSetting(entityName, entityId, settingName);
if (setting == null)
{
setting = new Setting();
setting.EntityName = entityName;
setting.EntityId = entityId;
setting.SettingName = settingName;
setting.SettingValue = settingValue;
setting.IsPrivate = isPrivate;
setting = _settings.AddSetting(setting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Created {Setting}", setting);
}
else
{
if (setting.SettingValue != settingValue || setting.IsPrivate != isPrivate)
{
setting.SettingValue = settingValue;
setting.IsPrivate = isPrivate;
setting = _settings.UpdateSetting(setting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Updated {Setting}", setting);
}
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, "User Not Authorized To Add Or Update Setting {EntityName} {EntityId} {SettingName}", entityName, entityId, settingName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// DELETE api/<controller>/site/1/settingname
[HttpDelete("{entityName}/{entityId}/{settingName}")]
public void Delete(string entityName, int entityId, string settingName)
{
Setting setting = _settings.GetSetting(entityName, entityId, settingName);
if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
_settings.DeleteSetting(setting.EntityName, id);
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Delete);
_settings.DeleteSetting(setting.EntityName, setting.SettingId);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Setting Deleted {Setting}", setting);
}
else
@ -300,7 +338,7 @@ namespace Oqtane.Controllers
return filter;
}
private void AddSyncEvent(string EntityName, int SettingId, string Action)
private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action)
{
_syncManager.AddSyncEvent(_alias, EntityName + "Setting", SettingId, Action);
@ -311,6 +349,9 @@ namespace Oqtane.Controllers
case EntityNames.Site:
_syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh);
break;
case EntityNames.User:
_syncManager.AddSyncEvent(_alias, EntityName, EntityId, SyncEventActions.Update);
break;
}
}
}

View File

@ -26,19 +26,17 @@ namespace Oqtane.Controllers
private readonly IUserManager _userManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly ISettingRepository _settings;
private readonly IJwtManager _jwtManager;
private readonly IFileRepository _files;
private readonly ILogManager _logger;
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, ISettingRepository settings, IJwtManager jwtManager, IFileRepository files, ILogManager logger)
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ILogManager logger)
{
_users = users;
_tenantManager = tenantManager;
_userManager = userManager;
_sites = sites;
_userPermissions = userPermissions;
_settings = settings;
_jwtManager = jwtManager;
_files = files;
_logger = logger;
@ -56,12 +54,6 @@ namespace Oqtane.Controllers
{
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
@ -83,12 +75,6 @@ namespace Oqtane.Controllers
{
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
@ -112,12 +98,6 @@ namespace Oqtane.Controllers
{
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

View File

@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Linq;
using System;
namespace Oqtane.Extensions
{
public static class QueryableExtensions
{
public static IQueryable<T> FilterByItems<T, TItem>(this IQueryable<T> query, IEnumerable<TItem> items,
Expression<Func<T, TItem, bool>> filterPattern, bool isOr)
{
Expression predicate = null;
foreach (var item in items)
{
var itemExpr = Expression.Constant(item);
var itemCondition = ExpressionReplacer.Replace(filterPattern.Body, filterPattern.Parameters[1], itemExpr);
if (predicate == null)
predicate = itemCondition;
else
{
predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
itemCondition);
}
}
predicate ??= Expression.Constant(false);
var filterLambda = Expression.Lambda<Func<T, bool>>(predicate, filterPattern.Parameters[0]);
return query.Where(filterLambda);
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
[return: NotNullIfNotNull(nameof(node))]
public override Expression Visit(Expression node)
{
if (node != null && _replaceMap.TryGetValue(node, out var replacement))
return replacement;
return base.Visit(node);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
.ToDictionary(i => (Expression)lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
}
}
}
}

View File

@ -17,18 +17,24 @@ namespace Oqtane.Infrastructure.EventSubscribers
public void EntityChanged(SyncEvent syncEvent)
{
// 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 || syncEvent.Action == SyncEventActions.Reload))
{
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}");
_cache.Remove($"modules:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
// when a site entity is updated, the hosting model may have changed so the client assemblies cache items need to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Update)
if (syncEvent.EntityName == EntityNames.Site && (syncEvent.Action == SyncEventActions.Update || syncEvent.Action == SyncEventActions.Delete))
{
_cache.Remove($"assemblieslist:{syncEvent.TenantId}:{syncEvent.EntityId}");
_cache.Remove($"assemblies:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
// when a users settings are changed, the user cache item needs to be refreshed
if (syncEvent.EntityName == EntityNames.User && syncEvent.Action == SyncEventActions.Update)
{
_cache.Remove($"user:{syncEvent.EntityId}:{syncEvent.TenantId}:{syncEvent.SiteId}");
}
}
}
}

View File

@ -186,18 +186,21 @@ namespace Oqtane.Infrastructure
if (string.IsNullOrEmpty(searchContent.Title))
{
searchContent.Title = string.Empty;
if (!string.IsNullOrEmpty(pageModule.Title))
{
searchContent.Title = pageModule.Title;
}
else if (pageModule.Page != null)
if (pageModule.Page != null)
{
searchContent.Title = !string.IsNullOrEmpty(pageModule.Page.Title) ? pageModule.Page.Title : pageModule.Page.Name;
}
else
{
searchContent.Title = pageModule.Title;
}
}
if (searchContent.Description == null)
{
searchContent.Description = (searchContent.Title != pageModule.Title) ? pageModule.Title : string.Empty;
}
if (searchContent.Description == null) { searchContent.Description = string.Empty;}
if (searchContent.Body == null) { searchContent.Body = string.Empty; }
if (string.IsNullOrEmpty(searchContent.Url))

View File

@ -210,7 +210,7 @@ namespace Oqtane.Infrastructure
if (alias != null)
{
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";
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))

View File

@ -266,7 +266,6 @@ namespace Oqtane.SiteTemplates
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>

View File

@ -66,8 +66,8 @@ namespace Oqtane.Infrastructure
case "5.1.0":
Upgrade_5_1_0(tenant, scope);
break;
case "5.2.0":
Upgrade_5_2_0(tenant, scope);
case "5.2.1":
Upgrade_5_2_1(tenant, scope);
break;
}
}
@ -201,7 +201,7 @@ namespace Oqtane.Infrastructure
}
};
AddPagesToSites(scope, pageTemplates);
AddPagesToSites(scope, tenant, pageTemplates);
}
private void Upgrade_3_1_3(Tenant tenant, IServiceScope scope)
@ -386,7 +386,7 @@ namespace Oqtane.Infrastructure
}
}
private void Upgrade_5_2_0(Tenant tenant, IServiceScope scope)
private void Upgrade_5_2_1(Tenant tenant, IServiceScope scope)
{
var pageTemplates = new List<PageTemplate>
{
@ -438,14 +438,16 @@ namespace Oqtane.Infrastructure
}
};
AddPagesToSites(scope, pageTemplates);
AddPagesToSites(scope, tenant, pageTemplates);
}
private void AddPagesToSites(IServiceScope scope, List<PageTemplate> pageTemplates)
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
{
var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>();
var sites = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
foreach (var site in sites.GetSites().ToList())
{
tenants.SetAlias(tenant.TenantId, site.SiteId);
sites.CreatePages(site, pageTemplates, null);
}
}

View File

@ -64,6 +64,9 @@ namespace Oqtane.Managers
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || user.UserId == user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return user;
});
@ -74,8 +77,7 @@ namespace Oqtane.Managers
User user = _users.GetUser(username);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
user = GetUser(user.UserId, siteid);
}
return user;
}
@ -85,8 +87,7 @@ namespace Oqtane.Managers
User user = _users.GetUser(username, email);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
user = GetUser(user.UserId, siteid);
}
return user;
}

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Master
{
[DbContext(typeof(MasterDBContext))]
[Migration("Master.05.02.01.01")]
public class AddIndexes : MultiDatabaseMigration
{
public AddIndexes(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var aliasEntityBuilder = new AliasEntityBuilder(migrationBuilder, ActiveDatabase);
aliasEntityBuilder.DropIndex("IX_Alias");
aliasEntityBuilder.AddIndex("IX_Alias", "Name", true);
var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase);
themeEntityBuilder.AddIndex("IX_Theme", "ThemeName", true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -1,4 +1,3 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;

View File

@ -0,0 +1,41 @@
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.01.01")]
public class AddIndexes : MultiDatabaseMigration
{
public AddIndexes(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.AddIndex("IX_Language", new string[] { "SiteId", "Code" }, true);
var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase);
pageModuleEntityBuilder.AddIndex("IX_PageModule_Module", "ModuleId");
pageModuleEntityBuilder.AddIndex("IX_PageModule_Page", "PageId");
var searchContentEntityBuilder = new SearchContentEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentEntityBuilder.AddIndex("IX_SearchContent", new string[] { "SiteId", "EntityName", "EntityId" }, true);
var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentPropertyEntityBuilder.AddIndex("IX_SearchContentProperty", new string[] { "SearchContentId", "Name" }, true);
var searchContentWordEntityBuilder = new SearchContentWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordEntityBuilder.AddIndex("IX_SearchContentWord", new string[] { "SearchContentId", "SearchWordId" }, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -57,6 +57,7 @@ namespace Oqtane.Modules.Admin.Files.Manager
EntityName = EntityNames.File,
EntityId = file.FileId.ToString(),
Title = path,
Description = "",
Body = body,
Url = $"{Constants.FileUrl}{folder.Path}{file.Name}",
Permissions = $"{EntityNames.Folder}:{folder.FolderId}",

View File

@ -20,18 +20,15 @@ namespace Oqtane.Modules.HtmlText.Manager
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISearchable
{
private readonly IServiceProvider _serviceProvider;
private readonly IHtmlTextRepository _htmlText;
private readonly IDBContextDependencies _DBContextDependencies;
private readonly ISqlRepository _sqlRepository;
public HtmlTextManager(
IServiceProvider serviceProvider,
IHtmlTextRepository htmlText,
IDBContextDependencies DBContextDependencies,
ISqlRepository sqlRepository)
{
_serviceProvider = serviceProvider;
_htmlText = htmlText;
_DBContextDependencies = DBContextDependencies;
_sqlRepository = sqlRepository;
@ -53,19 +50,15 @@ namespace Oqtane.Modules.HtmlText.Manager
{
var searchContents = new List<SearchContent>();
var htmltexts = _htmlText.GetHtmlTexts(pageModule.ModuleId);
if (htmltexts != null && htmltexts.Any())
var htmltext = _htmlText.GetHtmlTexts(pageModule.ModuleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault();
if (htmltext != null && htmltext.CreatedOn >= lastIndexedOn)
{
var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First();
if (htmltext.CreatedOn >= lastIndexedOn)
searchContents.Add(new SearchContent
{
searchContents.Add(new SearchContent
{
Body = htmltext.Content,
ContentModifiedBy = htmltext.CreatedBy,
ContentModifiedOn = htmltext.CreatedOn
});
}
Body = htmltext.Content,
ContentModifiedBy = htmltext.CreatedBy,
ContentModifiedOn = htmltext.CreatedOn
});
}
return Task.FromResult(searchContents);

View File

@ -1,11 +1,7 @@
using System.Linq;
using Oqtane.Documentation;
using System.Collections.Generic;
using Microsoft.Extensions.Caching.Memory;
using System;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Repository
{
@ -13,24 +9,16 @@ namespace Oqtane.Modules.HtmlText.Repository
public class HtmlTextRepository : IHtmlTextRepository, ITransientService
{
private readonly IDbContextFactory<HtmlTextContext> _factory;
private readonly IMemoryCache _cache;
private readonly SiteState _siteState;
public HtmlTextRepository(IDbContextFactory<HtmlTextContext> factory, IMemoryCache cache, SiteState siteState)
public HtmlTextRepository(IDbContextFactory<HtmlTextContext> factory)
{
_factory = factory;
_cache = cache;
_siteState = siteState;
}
public IEnumerable<Models.HtmlText> GetHtmlTexts(int moduleId)
{
return _cache.GetOrCreate($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
using var db = _factory.CreateDbContext();
return db.HtmlText.Where(item => item.ModuleId == moduleId).ToList();
});
using var db = _factory.CreateDbContext();
return db.HtmlText.Where(item => item.ModuleId == moduleId).ToList();
}
public Models.HtmlText GetHtmlText(int htmlTextId)
@ -44,7 +32,6 @@ namespace Oqtane.Modules.HtmlText.Repository
using var db = _factory.CreateDbContext();
db.HtmlText.Add(htmlText);
db.SaveChanges();
ClearCache(htmlText.ModuleId);
return htmlText;
}
@ -53,47 +40,7 @@ namespace Oqtane.Modules.HtmlText.Repository
using var db = _factory.CreateDbContext();
Models.HtmlText htmlText = db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId);
if (htmlText != null) db.HtmlText.Remove(htmlText);
ClearCache(htmlText.ModuleId);
db.SaveChanges();
}
public async Task<IEnumerable<Models.HtmlText>> GetHtmlTextsAsync(int moduleId)
{
return await _cache.GetOrCreateAsync($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}", async entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
using var db = _factory.CreateDbContext();
return await db.HtmlText.Where(item => item.ModuleId == moduleId).ToListAsync();
});
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId)
{
using var db = _factory.CreateDbContext();
return await db.HtmlText.FindAsync(htmlTextId);
}
public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
{
using var db = _factory.CreateDbContext();
db.HtmlText.Add(htmlText);
await db.SaveChangesAsync();
ClearCache(htmlText.ModuleId);
return htmlText;
}
public async Task DeleteHtmlTextAsync(int htmlTextId)
{
using var db = _factory.CreateDbContext();
Models.HtmlText htmlText = db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId);
db.HtmlText.Remove(htmlText);
ClearCache(htmlText.ModuleId);
await db.SaveChangesAsync();
}
private void ClearCache(int moduleId)
{
_cache.Remove($"HtmlText:{_siteState.Alias.SiteKey}:{moduleId}");
}
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
namespace Oqtane.Modules.HtmlText.Repository
@ -11,10 +10,5 @@ namespace Oqtane.Modules.HtmlText.Repository
Models.HtmlText GetHtmlText(int htmlTextId);
Models.HtmlText AddHtmlText(Models.HtmlText htmlText);
void DeleteHtmlText(int htmlTextId);
Task<IEnumerable<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText);
Task DeleteHtmlTextAsync(int htmlTextId);
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Documentation;
using Oqtane.Enums;
using Oqtane.Infrastructure;
@ -17,24 +19,26 @@ namespace Oqtane.Modules.HtmlText.Services
{
private readonly IHtmlTextRepository _htmlText;
private readonly IUserPermissions _userPermissions;
private readonly IMemoryCache _cache;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public ServerHtmlTextService(IHtmlTextRepository htmlText, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
public ServerHtmlTextService(IHtmlTextRepository htmlText, IUserPermissions userPermissions, IMemoryCache cache, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_htmlText = htmlText;
_userPermissions = userPermissions;
_cache = cache;
_logger = logger;
_accessor = accessor;
_alias = tenantManager.GetAlias();
}
public async Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId)
public Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId)
{
if (_accessor.HttpContext.User.IsInRole(RoleNames.Registered))
{
return (await _htmlText.GetHtmlTextsAsync(moduleId)).ToList();
return Task.FromResult(GetCachedHtmlTexts(moduleId));
}
else
{
@ -43,19 +47,11 @@ namespace Oqtane.Modules.HtmlText.Services
}
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int moduleId)
public Task<Models.HtmlText> GetHtmlTextAsync(int moduleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View))
{
var htmltexts = await _htmlText.GetHtmlTextsAsync(moduleId);
if (htmltexts != null && htmltexts.Any())
{
return htmltexts.OrderByDescending(item => item.CreatedOn).First();
}
else
{
return null;
}
return Task.FromResult(GetCachedHtmlTexts(moduleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault());
}
else
{
@ -64,11 +60,11 @@ namespace Oqtane.Modules.HtmlText.Services
}
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
public Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View))
{
return await _htmlText.GetHtmlTextAsync(htmlTextId);
return Task.FromResult(GetCachedHtmlTexts(moduleId)?.FirstOrDefault(item => item.HtmlTextId == htmlTextId));
}
else
{
@ -77,11 +73,12 @@ namespace Oqtane.Modules.HtmlText.Services
}
}
public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
public Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, htmlText.ModuleId, PermissionNames.Edit))
{
htmlText = await _htmlText.AddHtmlTextAsync(htmlText);
htmlText = _htmlText.AddHtmlText(htmlText);
ClearCache(htmlText.ModuleId);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText);
}
else
@ -89,20 +86,36 @@ namespace Oqtane.Modules.HtmlText.Services
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Add Attempt {HtmlText}", htmlText);
htmlText = null;
}
return htmlText;
return Task.FromResult(htmlText);
}
public async Task DeleteHtmlTextAsync(int htmlTextId, int moduleId)
public Task DeleteHtmlTextAsync(int htmlTextId, int moduleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.Edit))
{
await _htmlText.DeleteHtmlTextAsync(htmlTextId);
_htmlText.DeleteHtmlText(htmlTextId);
ClearCache(moduleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", htmlTextId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Delete Attempt {HtmlTextId} {ModuleId}", htmlTextId, moduleId);
}
return Task.CompletedTask;
}
private List<Models.HtmlText> GetCachedHtmlTexts(int moduleId)
{
return _cache.GetOrCreate($"HtmlText:{_alias.SiteKey}:{moduleId}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return _htmlText.GetHtmlTexts(moduleId).ToList();
});
}
private void ClearCache(int moduleId)
{
_cache.Remove($"HtmlText:{_alias.SiteKey}:{moduleId}");
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>5.2.0</Version>
<Version>5.2.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -33,21 +33,21 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.7" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.8" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.8" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />

View File

@ -51,7 +51,7 @@ namespace Oqtane.Providers
ContentModifiedOn = searchContent.ContentModifiedOn,
SearchContentProperties = searchContent.SearchContentProperties,
Snippet = BuildSnippet(searchContent, searchQuery),
Score = CalculateScore(searchContent, searchQuery)
Score = (searchContent.Count / 100f)
};
return searchResult;
@ -99,17 +99,6 @@ namespace Oqtane.Providers
return snippet;
}
private float CalculateScore(SearchContent searchContent, SearchQuery searchQuery)
{
var score = 0f;
foreach (var keyword in SearchUtils.GetKeywords(searchQuery.Keywords))
{
score += searchContent.SearchContentWords.Where(i => i.SearchWord.Word.StartsWith(keyword)).Sum(i => i.Count);
}
return score / 100;
}
public Task SaveSearchContent(SearchContent searchContent, Dictionary<string, string> siteSettings)
{
// remove existing search content

View File

@ -6,15 +6,6 @@ namespace Oqtane.Repository
{
public interface ISiteRepository
{
// asynchronous methods
Task<IEnumerable<Site>> GetSitesAsync();
Task<Site> AddSiteAsync(Site site);
Task<Site> UpdateSiteAsync(Site site);
Task<Site> GetSiteAsync(int siteId);
Task<Site> GetSiteAsync(int siteId, bool tracking);
Task DeleteSiteAsync(int siteId);
// synchronous methods
IEnumerable<Site> GetSites();
Site AddSite(Site site);
Site UpdateSite(Site site);

View File

@ -185,6 +185,7 @@ namespace Oqtane.Repository
if (siteId != -1)
{
var siteKey = _tenants.GetAlias().SiteKey;
var assemblies = new List<string>();
// get all module definition permissions for site
List<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.ModuleDefinition).ToList();
@ -193,12 +194,11 @@ namespace Oqtane.Repository
var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList();
// populate module definition site settings and permissions
var serverState = _serverState.GetServerState(siteKey);
foreach (ModuleDefinition moduledefinition in ModuleDefinitions)
{
moduledefinition.SiteId = siteId;
var setting = settings.FirstOrDefault(item => item.EntityId == moduledefinition.ModuleDefinitionId && item.SettingName == $"{settingprefix}{_tenants.GetAlias().SiteKey}");
var setting = settings.FirstOrDefault(item => item.EntityId == moduledefinition.ModuleDefinitionId && item.SettingName == $"{settingprefix}{siteKey}");
if (setting != null)
{
moduledefinition.IsEnabled = bool.Parse(setting.SettingValue);
@ -211,17 +211,17 @@ namespace Oqtane.Repository
if (moduledefinition.IsEnabled)
{
// build list of assemblies for site
if (!serverState.Assemblies.Contains(moduledefinition.AssemblyName))
if (!assemblies.Contains(moduledefinition.AssemblyName))
{
serverState.Assemblies.Add(moduledefinition.AssemblyName);
assemblies.Add(moduledefinition.AssemblyName);
}
if (!string.IsNullOrEmpty(moduledefinition.Dependencies))
{
foreach (var assembly in moduledefinition.Dependencies.Replace(".dll", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Reverse())
{
if (!serverState.Assemblies.Contains(assembly.Trim()))
if (!assemblies.Contains(assembly.Trim()))
{
serverState.Assemblies.Insert(0, assembly.Trim());
assemblies.Insert(0, assembly.Trim());
}
}
}
@ -248,6 +248,13 @@ namespace Oqtane.Repository
}
}
// cache site assemblies
var serverState = _serverState.GetServerState(siteKey);
foreach (var assembly in assemblies)
{
if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly);
}
// clean up any orphaned permissions
var ids = new HashSet<int>(ModuleDefinitions.Select(item => item.ModuleDefinitionId));
foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId)))

View File

@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
@ -20,50 +24,83 @@ namespace Oqtane.Repository
public async Task<IEnumerable<SearchContent>> GetSearchContentsAsync(SearchQuery searchQuery)
{
using var db = _dbContextFactory.CreateDbContext();
var searchContents = db.SearchContent.AsNoTracking()
.Include(i => i.SearchContentProperties)
.Include(i => i.SearchContentWords)
.ThenInclude(w => w.SearchWord)
.Where(i => i.SiteId == searchQuery.SiteId);
var keywords = SearchUtils.GetKeywords(searchQuery.Keywords);
var searchContents = db.SearchContentWord
.AsNoTracking()
.Include(item => item.SearchContent)
.Include(item => item.SearchWord)
.Where(item => item.SearchContent.SiteId == searchQuery.SiteId)
.FilterByItems(keywords, (item, keyword) => item.SearchWord.Word.StartsWith(keyword), true)
.GroupBy(item => new
{
item.SearchContent.SearchContentId,
item.SearchContent.SiteId,
item.SearchContent.EntityName,
item.SearchContent.EntityId,
item.SearchContent.Title,
item.SearchContent.Description,
item.SearchContent.Body,
item.SearchContent.Url,
item.SearchContent.Permissions,
item.SearchContent.ContentModifiedBy,
item.SearchContent.ContentModifiedOn,
item.SearchContent.AdditionalContent,
item.SearchContent.CreatedOn
})
.Select(result => new SearchContent
{
SearchContentId = result.Key.SearchContentId,
SiteId = result.Key.SiteId,
EntityName = result.Key.EntityName,
EntityId = result.Key.EntityId,
Title = result.Key.Title,
Description = result.Key.Description,
Body = result.Key.Body,
Url = result.Key.Url,
Permissions = result.Key.Permissions,
ContentModifiedBy = result.Key.ContentModifiedBy,
ContentModifiedOn = result.Key.ContentModifiedOn,
AdditionalContent = result.Key.AdditionalContent,
CreatedOn = result.Key.CreatedOn,
Count = result.Sum(group => group.Count)
});
if (searchQuery.Properties != null && searchQuery.Properties.Any())
{
searchContents = searchContents.Include(item => item.SearchContentProperties);
}
if (!string.IsNullOrEmpty(searchQuery.IncludeEntities))
{
searchContents = searchContents.Where(i => searchQuery.IncludeEntities.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(i.EntityName));
searchContents = searchContents.Where(item => searchQuery.IncludeEntities.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(item.EntityName));
}
if (!string.IsNullOrEmpty(searchQuery.ExcludeEntities))
{
searchContents = searchContents.Where(i => !searchQuery.ExcludeEntities.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(i.EntityName));
searchContents = searchContents.Where(item => !searchQuery.ExcludeEntities.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(item.EntityName));
}
if (searchQuery.FromDate != DateTime.MinValue)
if (searchQuery.FromDate.Date != DateTime.MinValue.Date)
{
searchContents = searchContents.Where(i => i.ContentModifiedOn >= searchQuery.FromDate);
searchContents = searchContents.Where(item => item.ContentModifiedOn >= searchQuery.FromDate);
}
if (searchQuery.ToDate != DateTime.MaxValue)
if (searchQuery.ToDate.Date != DateTime.MaxValue.Date)
{
searchContents = searchContents.Where(i => i.ContentModifiedOn <= searchQuery.ToDate);
searchContents = searchContents.Where(item => item.ContentModifiedOn <= searchQuery.ToDate);
}
if (searchQuery.Properties != null && searchQuery.Properties.Any())
{
foreach (var property in searchQuery.Properties)
{
searchContents = searchContents.Where(i => i.SearchContentProperties.Any(p => p.Name == property.Key && p.Value == property.Value));
searchContents = searchContents.Where(item => item.SearchContentProperties.Any(p => p.Name == property.Key && p.Value == property.Value));
}
}
var filteredContentList = new List<SearchContent>();
if (!string.IsNullOrEmpty(searchQuery.Keywords))
{
foreach (var keyword in SearchUtils.GetKeywords(searchQuery.Keywords))
{
filteredContentList.AddRange(await searchContents.Where(i => i.SearchContentWords.Any(w => w.SearchWord.Word.StartsWith(keyword))).ToListAsync());
}
}
return filteredContentList.DistinctBy(i => i.UniqueKey);
return await searchContents.ToListAsync();
}
public SearchContent AddSearchContent(SearchContent searchContent)

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -51,58 +50,6 @@ namespace Oqtane.Repository
_logger = logger;
}
// asynchronous methods
public async Task<IEnumerable<Site>> GetSitesAsync()
{
using var db = _factory.CreateDbContext();
return await db.Site.OrderBy(item => item.Name).ToListAsync();
}
public async Task<Site> AddSiteAsync(Site site)
{
site.SiteGuid = Guid.NewGuid().ToString();
using var db = _factory.CreateDbContext();
db.Site.Add(site);
await db.SaveChangesAsync();
CreateSite(site);
return site;
}
public async Task<Site> UpdateSiteAsync(Site site)
{
using var db = _factory.CreateDbContext();
db.Entry(site).State = EntityState.Modified;
await db.SaveChangesAsync();
return site;
}
public async Task<Site> GetSiteAsync(int siteId)
{
return await GetSiteAsync(siteId, true);
}
public async Task<Site> GetSiteAsync(int siteId, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return await db.Site.FindAsync(siteId);
}
else
{
return await db.Site.AsNoTracking().FirstOrDefaultAsync(item => item.SiteId == siteId);
}
}
public async Task DeleteSiteAsync(int siteId)
{
using var db = _factory.CreateDbContext();
var site = db.Site.Find(siteId);
db.Site.Remove(site);
await db.SaveChangesAsync();
}
// synchronous methods
public IEnumerable<Site> GetSites()
{
using var db = _factory.CreateDbContext();

View File

@ -161,12 +161,12 @@ namespace Oqtane.Repository
if (siteId != -1)
{
var siteKey = _tenants.GetAlias().SiteKey;
var assemblies = new List<string>();
// get settings for site
var settings = _settings.GetSettings(EntityNames.Theme).ToList();
// populate theme site settings
var serverState = _serverState.GetServerState(siteKey);
foreach (Theme theme in Themes)
{
theme.SiteId = siteId;
@ -184,22 +184,29 @@ namespace Oqtane.Repository
if (theme.IsEnabled)
{
// build list of assemblies for site
if (!serverState.Assemblies.Contains(theme.AssemblyName))
if (!assemblies.Contains(theme.AssemblyName))
{
serverState.Assemblies.Add(theme.AssemblyName);
assemblies.Add(theme.AssemblyName);
}
if (!string.IsNullOrEmpty(theme.Dependencies))
{
foreach (var assembly in theme.Dependencies.Replace(".dll", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Reverse())
{
if (!serverState.Assemblies.Contains(assembly.Trim()))
if (!assemblies.Contains(assembly.Trim()))
{
serverState.Assemblies.Insert(0, assembly.Trim());
assemblies.Insert(0, assembly.Trim());
}
}
}
}
}
// cache site assemblies
var serverState = _serverState.GetServerState(siteKey);
foreach (var assembly in assemblies)
{
if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly);
}
}
return Themes;

View File

@ -37,12 +37,27 @@ namespace Oqtane.Services
var searchProvider = GetSearchProvider(searchQuery.SiteId);
var searchResults = await searchProvider.GetSearchResultsAsync(searchQuery);
var totalResults = 0;
// trim results
var results = searchResults.Where(i => HasViewPermission(i, searchQuery))
.OrderBy(i => i.Url).ThenByDescending(i => i.Score)
.DistinctBy(i => i.Url);
// security trim results and aggregate by Url
var results = searchResults.Where(item => HasViewPermission(item, searchQuery))
.OrderBy(item => item.Url).ThenByDescending(item => item.Score)
.GroupBy(group => group.Url)
.Select(result => new SearchResult
{
SearchContentId = result.First().SearchContentId,
SiteId = result.First().SiteId,
EntityName = result.First().EntityName,
EntityId = result.First().EntityId,
Title = result.First().Title,
Description = result.First().Description,
Body = result.First().Body,
Url = result.First().Url,
Permissions = result.First().Permissions,
ContentModifiedBy = result.First().ContentModifiedBy,
ContentModifiedOn = result.First().ContentModifiedOn,
SearchContentProperties = result.First().SearchContentProperties,
Snippet = result.First().Snippet,
Score = result.Sum(group => group.Score) // recalculate score
});
// sort results
if (searchQuery.SortOrder == SearchSortOrder.Descending)
@ -76,12 +91,10 @@ namespace Oqtane.Services
}
}
totalResults = results.Count();
return new SearchResults
{
Results = results.Skip(searchQuery.PageIndex * searchQuery.PageSize).Take(searchQuery.PageSize).ToList(),
TotalResults = totalResults
TotalResults = results.Count()
};
}

View File

@ -50,23 +50,23 @@ namespace Oqtane.Services
_accessor = accessor;
}
public async Task<List<Site>> GetSitesAsync()
public Task<List<Site>> GetSitesAsync()
{
List<Site> sites = new List<Site>();
if (_accessor.HttpContext.User.IsInRole(RoleNames.Host))
{
sites = (await _sites.GetSitesAsync()).ToList();
sites = _sites.GetSites().ToList();
}
return sites;
return Task.FromResult(sites);
}
public async Task<Site> GetSiteAsync(int siteId)
public Task<Site> GetSiteAsync(int siteId)
{
var alias = _tenantManager.GetAlias();
var site = await _cache.GetOrCreateAsync($"site:{alias.SiteKey}", async entry =>
var site = _cache.GetOrCreate($"site:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return await GetSite(siteId);
return GetSite(siteId);
});
// trim pages based on user permissions
@ -83,13 +83,13 @@ namespace Oqtane.Services
site = site.Clone(site);
site.Pages = pages;
return site;
return Task.FromResult(site);
}
private async Task<Site> GetSite(int siteid)
private Site GetSite(int siteid)
{
var alias = _tenantManager.GetAlias();
var site = await _sites.GetSiteAsync(siteid);
var site = _sites.GetSite(siteid);
if (site != null && site.SiteId == alias.SiteId)
{
// site settings
@ -116,7 +116,7 @@ namespace Oqtane.Services
site.Pages = GetPagesHierarchy(site.Pages);
// framework modules
var modules = await GetModulesAsync(site.SiteId);
var modules = GetModules(site.SiteId);
site.Settings.Add(Constants.AdminDashboardModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule).ModuleId.ToString());
site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString());
@ -179,11 +179,11 @@ namespace Oqtane.Services
return hierarchy;
}
public async Task<Site> AddSiteAsync(Site site)
public Task<Site> AddSiteAsync(Site site)
{
if (_accessor.HttpContext.User.IsInRole(RoleNames.Host))
{
site = await _sites.AddSiteAsync(site);
site = _sites.AddSite(site);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.Site, site.SiteId, SyncEventActions.Create);
_logger.Log(site.SiteId, LogLevel.Information, this, LogFunction.Create, "Site Added {Site}", site);
}
@ -191,18 +191,18 @@ namespace Oqtane.Services
{
site = null;
}
return site;
return Task.FromResult(site);
}
public async Task<Site> UpdateSiteAsync(Site site)
public Task<Site> UpdateSiteAsync(Site site)
{
if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin))
{
var alias = _tenantManager.GetAlias();
var current = await _sites.GetSiteAsync(site.SiteId, false);
var current = _sites.GetSite(site.SiteId, false);
if (site.SiteId == alias.SiteId && site.TenantId == alias.TenantId && current != null)
{
site = await _sites.UpdateSiteAsync(site);
site = _sites.UpdateSite(site);
_syncManager.AddSyncEvent(alias, EntityNames.Site, site.SiteId, SyncEventActions.Update);
string action = SyncEventActions.Refresh;
if (current.RenderMode != site.RenderMode || current.Runtime != site.Runtime)
@ -222,19 +222,20 @@ namespace Oqtane.Services
{
site = null;
}
return site;
return Task.FromResult(site);
}
public async Task DeleteSiteAsync(int siteId)
public Task DeleteSiteAsync(int siteId)
{
if (_accessor.HttpContext.User.IsInRole(RoleNames.Host))
{
var alias = _tenantManager.GetAlias();
var site = await _sites.GetSiteAsync(siteId);
var site = _sites.GetSite(siteId);
if (site != null && site.SiteId == alias.SiteId)
{
await _sites.DeleteSiteAsync(siteId);
_sites.DeleteSite(siteId);
_syncManager.AddSyncEvent(alias, EntityNames.Site, site.SiteId, SyncEventActions.Delete);
_syncManager.AddSyncEvent(alias, EntityNames.Site, site.SiteId, SyncEventActions.Refresh);
_logger.Log(siteId, LogLevel.Information, this, LogFunction.Delete, "Site Deleted {SiteId}", siteId);
}
else
@ -242,15 +243,16 @@ namespace Oqtane.Services
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Delete Attempt {SiteId}", siteId);
}
}
return Task.CompletedTask;
}
public async Task<List<Module>> GetModulesAsync(int siteId, int pageId)
public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
{
var alias = _tenantManager.GetAlias();
var sitemodules = await _cache.GetOrCreateAsync($"modules:{alias.SiteKey}", async entry =>
var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return await GetModulesAsync(siteId);
return GetModules(siteId);
});
var modules = new List<Module>();
@ -261,23 +263,21 @@ namespace Oqtane.Services
modules.Add(module);
}
}
return modules;
return Task.FromResult(modules);
}
public async Task<List<Module>> GetModulesAsync(int siteId)
private List<Module> GetModules(int siteId)
{
var alias = _tenantManager.GetAlias();
return await _cache.GetOrCreateAsync($"modules:{alias.SiteKey}", async entry =>
return _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return await GetModules(siteId);
return GetPageModules(siteId);
});
}
private async Task<List<Module>> GetModules(int siteId)
private List<Module> GetPageModules(int siteId)
{
await Task.Yield(); // force method to async
List<ModuleDefinition> moduledefinitions = _moduleDefinitions.GetModuleDefinitions(siteId).ToList();
var settings = _settings.GetSettings(EntityNames.Module).ToList();
var modules = new List<Module>();

View File

@ -100,6 +100,7 @@ namespace Oqtane
options.Cookie.Name = Constants.AntiForgeryTokenCookieName;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.HttpOnly = true;
});
services.AddIdentityCore<IdentityUser>(options => { })

View File

@ -2,13 +2,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
namespace [Owner].Module.[Module].Services
{
public class [Module]Service : ServiceBase, I[Module]Service, IService
public class [Module]Service : ServiceBase, I[Module]Service
{
public [Module]Service(IHttpClientFactory http, SiteState siteState) : base(http, siteState) { }

View File

@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />

View File

@ -11,12 +11,5 @@ namespace [Owner].Module.[Module].Repository
Models.[Module] Add[Module](Models.[Module] [Module]);
Models.[Module] Update[Module](Models.[Module] [Module]);
void Delete[Module](int [Module]Id);
Task<IEnumerable<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, bool tracking);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id);
}
}

View File

@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Repository
{
@ -62,54 +61,5 @@ namespace [Owner].Module.[Module].Repository
db.[Module].Remove([Module]);
db.SaveChanges();
}
public async Task<IEnumerable<Models.[Module]>> Get[Module]sAsync(int ModuleId)
{
using var db = _factory.CreateDbContext();
return await db.[Module].Where(item => item.ModuleId == ModuleId).ToListAsync();
}
public async Task<Models.[Module]> Get[Module]Async(int [Module]Id)
{
return await Get[Module]Async([Module]Id, true);
}
public async Task<Models.[Module]> Get[Module]Async(int [Module]Id, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return await db.[Module].FindAsync([Module]Id);
}
else
{
return await db.[Module].AsNoTracking().FirstOrDefaultAsync(item => item.[Module]Id == [Module]Id);
}
}
public async Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
{
using var db = _factory.CreateDbContext();
db.[Module].Add([Module]);
await db.SaveChangesAsync();
return [Module];
}
public async Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module])
{
using var db = _factory.CreateDbContext();
db.Entry([Module]).State = EntityState.Modified;
await db.SaveChangesAsync();
return [Module];
}
public async Task Delete[Module]Async(int [Module]Id)
{
using var db = _factory.CreateDbContext();
Models.[Module] [Module] = db.[Module].Find([Module]Id);
db.[Module].Remove([Module]);
await db.SaveChangesAsync();
}
}
}

View File

@ -5,14 +5,13 @@ using Microsoft.AspNetCore.Http;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Security;
using Oqtane.Shared;
using [Owner].Module.[Module].Repository;
namespace [Owner].Module.[Module].Services
{
public class Server[Module]Service : I[Module]Service, ITransientService
public class Server[Module]Service : I[Module]Service
{
private readonly I[Module]Repository _[Module]Repository;
private readonly IUserPermissions _userPermissions;
@ -29,11 +28,11 @@ namespace [Owner].Module.[Module].Services
_alias = tenantManager.GetAlias();
}
public async Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId)
public Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return (await _[Module]Repository.Get[Module]sAsync(ModuleId)).ToList();
return Task.FromResult(_[Module]Repository.Get[Module]s(ModuleId).ToList());
}
else
{
@ -42,11 +41,11 @@ namespace [Owner].Module.[Module].Services
}
}
public async Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId)
public Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return await _[Module]Repository.Get[Module]Async([Module]Id);
return Task.FromResult(_[Module]Repository.Get[Module]([Module]Id));
}
else
{
@ -55,11 +54,11 @@ namespace [Owner].Module.[Module].Services
}
}
public async Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
public Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
{
[Module] = await _[Module]Repository.Add[Module]Async([Module]);
[Module] = _[Module]Repository.Add[Module]([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]);
}
else
@ -67,14 +66,14 @@ namespace [Owner].Module.[Module].Services
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Add Attempt {[Module]}", [Module]);
[Module] = null;
}
return [Module];
return Task.FromResult([Module]);
}
public async Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module])
public Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module])
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
{
[Module] = await _[Module]Repository.Update[Module]Async([Module]);
[Module] = _[Module]Repository.Update[Module]([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);
}
else
@ -82,20 +81,21 @@ namespace [Owner].Module.[Module].Services
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Update Attempt {[Module]}", [Module]);
[Module] = null;
}
return [Module];
return Task.FromResult([Module]);
}
public async Task Delete[Module]Async(int [Module]Id, int ModuleId)
public Task Delete[Module]Async(int [Module]Id, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
await _[Module]Repository.Delete[Module]Async([Module]Id);
_[Module]Repository.Delete[Module]([Module]Id);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", [Module]Id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", [Module]Id, ModuleId);
}
return Task.CompletedTask;
}
}
}

View File

@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using [Owner].Module.[Module].Repository;
using [Owner].Module.[Module].Services;
namespace [Owner].Module.[Module].Startup
{
public class [Module]ServerStartup : IServerStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// not implemented
}
public void ConfigureMvc(IMvcBuilder mvcBuilder)
{
// not implemented
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<I[Module]Service, Server[Module]Service>();
services.AddDbContextFactory<[Module]Context>(opt => { }, ServiceLifetime.Transient);
}
}
}

View File

@ -19,10 +19,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
</ItemGroup>
<ItemGroup>

View File

@ -17,9 +17,10 @@ namespace [Owner].Theme.[Theme]
Resources = new List<Resource>()
{
// 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 = "~/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" }
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" }
}
};

View File

@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
</ItemGroup>
<ItemGroup>

View File

@ -233,7 +233,7 @@ app {
}
.app-form-inline {
display: inline-block;
display: inline;
}
.app-search{
display: inline-block;

View File

@ -71,7 +71,7 @@ namespace Oqtane.Models
/// Protocol for the request from which the alias was resolved (ie. http or https )
/// </summary>
[NotMapped]
public string Protocol { get; set; } = "https"; // default value
public string Protocol { get; set; } = "https://"; // default value
/// <summary>
/// Base Url for static resources (note that this will only be set for remote clients)

View File

@ -31,21 +31,22 @@ namespace Oqtane.Models
public string AdditionalContent { get; set; }
[NotMapped]
public bool IsDeleted { get; set; }
public List<SearchContentProperty> SearchContentProperties { get; set; }
public DateTime CreatedOn { get; set; }
public List<SearchContentProperty> SearchContentProperties { get; set; } // only used during updates
[NotMapped]
public int TenantId { get; set; }
public int Count { get; set; } // only populated for queries
[NotMapped]
public bool IsDeleted { get; set; } // only used during updates
[NotMapped]
public int TenantId { get; set; } // only used during updates
[NotMapped]
public string UniqueKey => $"{TenantId}:{SiteId}:{EntityName}:{EntityId}";
public List<SearchContentWord> SearchContentWords { get; set; }
// constructors
public SearchContent() { }

View File

@ -20,5 +20,7 @@ namespace Oqtane.Models
public DateTime ModifiedOn { get; set; }
public SearchWord SearchWord { get; set; }
public SearchContent SearchContent { get; set; }
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>5.2.0</Version>
<Version>5.2.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -19,8 +19,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />

View File

@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
public static readonly string Version = "5.2.0";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0";
public static readonly string Version = "5.2.1";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>5.2.0</Version>
<Version>5.2.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -1,6 +1,6 @@
# Latest Release
[5.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2) was released on May 28, 2024 and is primarily a stabilization release. This release includes 68 pull requests by 9 different contributors, pushing the total number of project commits all-time to over 5300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) was released on July 25, 2024 and is a major release including 109 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
@ -18,7 +18,7 @@ Please note that this project is owned by the .NET Foundation and is governed by
**Using Version 5:**
- Install **[.NET 8.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
- Install **[.NET 8.0.7 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
- Install the latest edition (v17.9 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
@ -63,6 +63,11 @@ Backlog (TBD)
- [ ] Folder Providers
- [ ] Generative AI Integration
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) (Jul 25, 2024)
- [x] Site Content Search
- [x] RichTextEditor extensibility
- [x] Scalability and performance improvements
[5.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2) (May 28, 2024)
- [x] Stabilization improvements