Enhance Settings API for public Site Settings. Added Settings to Site model by default. Added new parameters to Login and UserProfile components. Enhanced Oqtane Theme settings to use new component parameters. Enhanced image download and resizing logic.

This commit is contained in:
Shaun Walker 2021-09-20 17:15:52 -04:00
parent db85e088bf
commit f739db1e42
25 changed files with 458 additions and 188 deletions

View File

@ -27,7 +27,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="Enter the url of the file you wish to download" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@url" required />
<input id="url" class="form-control" @bind="@_url" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -42,6 +42,12 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the name of the file being downloaded" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Download">@SharedLocalizer["Download"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@ -53,9 +59,10 @@
@code {
private ElementReference form;
private bool validated = false;
private string url = string.Empty;
private string _url = string.Empty;
private List<Folder> _folders;
private int _folderId = -1;
private string _name = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -75,22 +82,24 @@
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (url == string.Empty || _folderId == -1)
if (_url == string.Empty || _folderId == -1)
{
AddModuleMessage(Localizer["Message.Required.UrlFolder"], MessageType.Warning);
return;
}
var filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
if (string.IsNullOrEmpty(_name))
{
_name = _url.Substring(_url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
if (!Constants.UploadableFiles.Split(',')
.Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", "")))
{
AddModuleMessage(Localizer["Message.Download.InvalidExtension"], MessageType.Warning);
return;
}
if (!filename.IsPathOrFileValid())
if (!_name.IsPathOrFileValid())
{
AddModuleMessage(Localizer["Message.Required.UrlName"], MessageType.Warning);
return;
@ -98,13 +107,13 @@
try
{
await FileService.UploadFileAsync(url, _folderId);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", url);
await FileService.UploadFileAsync(_url, _folderId, _name);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", _url);
AddModuleMessage(Localizer["Success.Download.File"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading File From Url {Url} {Error}", url, ex.Message);
await logger.LogError(ex, "Error Downloading File From Url {Url} {Error}", _url, ex.Message);
AddModuleMessage(Localizer["Error.Download.InvalidUrl"], MessageType.Error);
}
}

View File

@ -81,6 +81,7 @@
</TabPanel>
}
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModule">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />

View File

@ -162,6 +162,7 @@
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@ThemeSettingsComponent
</TabPanel>
<br />
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>

View File

@ -12,7 +12,7 @@
@if (PageState.User != null && photo != null)
{
<img src="@ImageUrl(photofileid, "400x400", "crop")" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
<img src="@ImageUrl(photofileid, 400, 400, "crop")" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
}
else
{

View File

@ -134,9 +134,9 @@ namespace Oqtane.Modules
return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment);
}
public string ImageUrl(int fileid, string size, string mode)
public string ImageUrl(int fileid, int width, int height, string mode)
{
return Utilities.ImageUrl(PageState.Alias, fileid, size, mode);
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode);
}
public virtual Dictionary<string, string> GetUrlParameters(string parametersTemplate = "")

View File

@ -156,4 +156,10 @@
<data name="UploadFiles.Heading" xml:space="preserve">
<value>Upload Files</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the file being downloaded</value>
</data>
<data name="Name.Text" xml:space="preserve">
<value>Name:</value>
</data>
</root>

View File

@ -309,4 +309,7 @@
<data name="Extend" xml:space="preserve">
<value>Extend</value>
</data>
<data name="Not Specified" xml:space="preserve">
<value>Not Specified</value>
</data>
</root>

View File

@ -1,65 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Microsoft ResX Schema
Version 2.0
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
@ -117,10 +117,28 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title.HelpText" xml:space="preserve">
<value>Specify If The Page Footer Should Be Displayed</value>
<data name="Footer.HelpText" xml:space="preserve">
<value>Specify if a Footer pane should always be displayed in a fixed location at the bottom of the page.</value>
</data>
<data name="Title.Text" xml:space="preserve">
<data name="Footer.Text" xml:space="preserve">
<value>Display Footer?</value>
</data>
<data name="Login.HelpText" xml:space="preserve">
<value>Specify if a Login option should be displayed, Note that this option does not prevent the login page from being accessible via a direct url.</value>
</data>
<data name="Login.Text" xml:space="preserve">
<value>Show Login?</value>
</data>
<data name="Page" xml:space="preserve">
<value>Page</value>
</data>
<data name="Register.HelpText" xml:space="preserve">
<value>Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.</value>
</data>
<data name="Register.Text" xml:space="preserve">
<value>Show Register?</value>
</data>
<data name="Site" xml:space="preserve">
<value>Site</value>
</data>
</root>

View File

@ -67,9 +67,9 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/{fileId}");
}
public async Task<File> UploadFileAsync(string url, int folderId)
public async Task<File> UploadFileAsync(string url, int folderId, string name)
{
return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}");
return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}");
}
public async Task<string> UploadFilesAsync(int folderId, string[] files, string id)

View File

@ -62,8 +62,9 @@ namespace Oqtane.Services
/// </summary>
/// <param name="url"></param>
/// <param name="folderId"></param>
/// <param name="name"></param>
/// <returns></returns>
Task<File> UploadFileAsync(string url, int folderId);
Task<File> UploadFileAsync(string url, int folderId, string name);
/// <summary>
/// Upload one or more files.

View File

@ -1,4 +1,4 @@
using Oqtane.Models;
using Oqtane.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -51,5 +51,9 @@ namespace Oqtane.Services
string GetSetting(Dictionary<string, string> settings, string settingName, string defaultValue);
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue);
}
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPublic);
Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2);
}
}

View File

@ -109,21 +109,31 @@ namespace Oqtane.Services
foreach (KeyValuePair<string, string> kvp in settings)
{
Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key,StringComparison.OrdinalIgnoreCase));
string value = kvp.Value;
bool ispublic = false;
if (value.StartsWith("[Public]"))
{
value = value.Substring(8); // remove [Public]
ispublic = true;
}
Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase));
if (setting == null)
{
setting = new Setting();
setting.EntityName = entityName;
setting.EntityId = entityId;
setting.SettingName = kvp.Key;
setting.SettingValue = kvp.Value;
setting.SettingValue = value;
setting.IsPublic = ispublic;
setting = await AddSettingAsync(setting);
}
else
{
if (setting.SettingValue != kvp.Value)
{
setting.SettingValue = kvp.Value;
setting.SettingValue = value;
setting.IsPublic = ispublic;
setting = await UpdateSettingAsync(setting);
}
}
@ -163,11 +173,17 @@ namespace Oqtane.Services
}
public Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue)
{
return SetSetting(settings, settingName, settingValue, false);
}
public Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPublic)
{
if (settings == null)
{
settings = new Dictionary<string, string>();
}
settingValue = (isPublic) ? "[Public]" + settingValue : settingValue;
if (settings.ContainsKey(settingName))
{
settings[settingName] = settingValue;
@ -178,5 +194,28 @@ namespace Oqtane.Services
}
return settings;
}
public Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2)
{
if (settings1 == null)
{
settings1 = new Dictionary<string, string>();
}
if (settings2 != null)
{
foreach (var setting in settings2)
{
if (settings1.ContainsKey(setting.Key))
{
settings1[setting.Key] = setting.Value;
}
else
{
settings1.Add(setting.Key, setting.Value);
}
}
}
return settings1;
}
}
}

View File

@ -12,7 +12,16 @@
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
</Authorized>
<NotAuthorized>
<button type="button" class="btn btn-primary" @onclick="LoginUser">@SharedLocalizer["Login"]</button>
@if (ShowLogin)
{
<button type="button" class="btn btn-primary" @onclick="LoginUser">@SharedLocalizer["Login"]</button>
}
</NotAuthorized>
</AuthorizeView>
</span>
@code
{
[Parameter]
public bool ShowLogin { get; set; }
}

View File

@ -13,9 +13,9 @@
<button type="button" class="btn btn-primary" @onclick="UpdateProfile">@context.User.Identity.Name</button>
</Authorized>
<NotAuthorized>
@if (PageState.Site.AllowRegistration)
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<button type="button" class="btn btn-primary" @onclick="RegisterUser">@Localizer["Register"]</button>
<button type="button" class="btn btn-primary" @onclick="RegisterUser">@Localizer["Register"]</button>
}
</NotAuthorized>
</AuthorizeView>
@ -23,6 +23,9 @@
@code {
[Parameter]
public bool ShowRegister { get; set; }
private void RegisterUser()
{
NavigationManager.NavigateTo(NavigateUrl("register"));

View File

@ -6,7 +6,7 @@
<nav class="navbar navbar-expand-md navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto">
<div class="controls-group"><UserProfile /> <Login /> <ControlPanel /></div>
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel /></div>
</div>
</nav>
<div class="content">
@ -117,13 +117,18 @@
new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js", Integrity = "sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM", CrossOrigin = "anonymous" }
};
private bool _login = true;
private bool _register = true;
private bool _footer = false;
protected override void OnParametersSet()
{
try
{
_footer = bool.Parse(SettingService.GetSetting(PageState.Page.Settings, GetType().Namespace + ":Footer", "false"));
var settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
_login = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true"));
_register = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true"));
_footer = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "false"));
}
catch
{

View File

@ -2,47 +2,149 @@
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@inject IStringLocalizer<ThemeSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@attribute [OqtaneIgnore]
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="footer" ResourceKey="Footer" HelpText="Specify If A Footer Should Always Be Displayed In A Fixed Location At The Bottom Of The Browser Window">Display Fixed Footer?</Label>
<div class="col-sm-9">
<select id="footer" class="form-select" @bind="@_footer">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scope" ResourceKey="Scope" HelpText="Specify if the settings are applicable to this page or the entire site.">Setting Scope:</Label>
<div class="col-sm-9">
<select id="scope" class="form-select" value="@_scope" @onchange="(e => ScopeChanged(e))">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<option value="site">@Localizer["Site"]</option>
}
<option value="page">@Localizer["Page"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="login" ResourceKey="Login" HelpText="Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.">Show Login?</Label>
<div class="col-sm-9">
<select id="login" class="form-select" @bind="@_login">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="register" ResourceKey="Register" HelpText="Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.">Show Register?</Label>
<div class="col-sm-9">
<select id="register" class="form-select" @bind="@_register">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="footer" ResourceKey="Footer" HelpText="Specify if a Footer pane should always be displayed in a fixed location at the bottom of the page">Display Fixed Footer?</Label>
<div class="col-sm-9">
<select id="footer" class="form-select" @bind="@_footer">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private string _scope = "page";
private string _login = "-";
private string _register = "-";
private string _footer = "-";
@code {
private string _footer = "false";
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
try
{
try
{
_footer = SettingService.GetSetting(PageState.Page.Settings, GetType().Namespace + ":Footer", "false");
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
}
await LoadSettings();
}
public async Task UpdateSettings()
catch (Exception ex)
{
try
{
var settings = PageState.Page.Settings;
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer);
await SettingService.UpdatePageSettingsAsync(settings, PageState.Page.PageId);
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
}
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
private async Task LoadSettings()
{
await Task.Yield();
Dictionary<string, string> settings;
if (_scope == "site")
{
settings = PageState.Site.Settings;
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true");
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "false");
}
else
{
settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "-");
}
}
private async Task ScopeChanged(ChangeEventArgs eventArgs)
{
try
{
_scope = (string)eventArgs.Value;
await LoadSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
Dictionary<string, string> settings;
if (_scope == "site")
{
settings = PageState.Site.Settings;
}
else
{
settings = PageState.Page.Settings;
}
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login, true);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register, true);
}
if (_footer != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer, true);
}
if (_scope == "site")
{
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
}
else
{
await SettingService.UpdatePageSettingsAsync(settings, PageState.Page.PageId);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Settings {Error}", ex.Message);
AddModuleMessage("Error Saving Settings", MessageType.Error);
}
}
}

View File

@ -104,9 +104,9 @@ namespace Oqtane.Themes
return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment);
}
public string ImageUrl(int fileid, string size, string mode)
public string ImageUrl(int fileid, int width, int height, string mode)
{
return Utilities.ImageUrl(PageState.Alias, fileid, size, mode);
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode);
}
}
}

View File

@ -188,9 +188,9 @@ namespace Oqtane.Controllers
}
}
// GET api/<controller>/upload?url=x&folderid=y
// GET api/<controller>/upload?url=x&folderid=y&name=z
[HttpGet("upload")]
public Models.File UploadFile(string url, string folderid)
public Models.File UploadFile(string url, string folderid, string name)
{
Models.File file = null;
@ -206,16 +206,19 @@ namespace Oqtane.Controllers
string folderPath = _folders.GetFolderPath(folder);
CreateDirectory(folderPath);
string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
if (string.IsNullOrEmpty(name))
{
name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
// check for allowable file extensions
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
if (!Constants.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 (!filename.IsPathOrFileValid())
if (!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;
@ -225,7 +228,7 @@ namespace Oqtane.Controllers
try
{
var client = new WebClient();
string targetPath = Path.Combine(folderPath, filename);
string targetPath = Path.Combine(folderPath, name);
// remove file if it already exists
if (System.IO.File.Exists(targetPath))
{
@ -233,15 +236,15 @@ namespace Oqtane.Controllers
}
client.DownloadFile(url, targetPath);
file = CreateFile(filename, folder.FolderId, targetPath);
file = CreateFile(name, folder.FolderId, targetPath);
if (file != null)
{
file = _files.AddFile(file);
}
}
catch
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url {Url}", url);
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url {Url} {Error}", url, ex.Message);
}
}
else
@ -494,8 +497,8 @@ namespace Oqtane.Controllers
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null;
}
[HttpGet("image/{id}/{size}/{mode?}")]
public IActionResult GetImage(int id, string size, string mode)
[HttpGet("image/{id}/{width}/{height}/{mode?}")]
public IActionResult GetImage(int id, int width, int height, string mode)
{
var file = _files.GetFile(id);
if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions))
@ -505,27 +508,31 @@ namespace Oqtane.Controllers
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
{
size = size.ToLower();
mode = (string.IsNullOrEmpty(mode)) ? "crop" : mode;
if ((_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.Permissions) ||
size.Contains("x") && !string.IsNullOrEmpty(file.Folder.ImageSizes) && file.Folder.ImageSizes.ToLower().Split(",").Contains(size))
&& Enum.TryParse(mode, true, out ResizeMode resizemode))
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + mode.ToLower() + ".png");
if (!System.IO.File.Exists(imagepath))
{
var imagepath = CreateImage(filepath, size, resizemode.ToString());
if (!string.IsNullOrEmpty(imagepath))
if ((_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.Permissions) ||
!string.IsNullOrEmpty(file.Folder.ImageSizes) && file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString()))
&& Enum.TryParse(mode, true, out ResizeMode resizemode))
{
return PhysicalFile(imagepath, file.GetMimeType());
imagepath = CreateImage(filepath, width, height, resizemode.ToString(), imagepath);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Creating Image For File {File} {Size}", file, size);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder Or Invalid Mode Specification {Folder} {Width} {Height} {Mode}", file.Folder, width, height, mode);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
if (!string.IsNullOrEmpty(imagepath))
{
return PhysicalFile(imagepath, file.GetMimeType());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder Or Invalid Mode Specification {Folder} {Size} {Mode}", file.Folder, size, mode);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Displaying Image For File {File} {Width} {Height}", file, width, height);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
else
@ -550,38 +557,31 @@ namespace Oqtane.Controllers
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null;
}
private string CreateImage(string filepath, string size, string mode)
private string CreateImage(string filepath, int width, int height, string mode, string imagepath)
{
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + size + "." + mode.ToLower() + ".png");
if (!System.IO.File.Exists(imagepath))
try
{
try
FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using (Image image = Image.Load(stream))
{
FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using (Image image = Image.Load(stream))
{
var parts = size.Split('x');
int width = (!string.IsNullOrEmpty(parts[0])) ? int.Parse(parts[0]) : 0;
int height = (!string.IsNullOrEmpty(parts[1])) ? int.Parse(parts[1]) : 0;
Enum.TryParse(mode, true, out ResizeMode resizemode);
Enum.TryParse(mode, true, out ResizeMode resizemode);
image.Mutate(x =>
x.Resize(new ResizeOptions
{
Size = new Size(width, height),
Mode = resizemode
})
.BackgroundColor(new Rgba32(255, 255, 255, 0)));
image.Mutate(x =>
x.Resize(new ResizeOptions
{
Size = new Size(width, height),
Mode = resizemode
})
.BackgroundColor(new Rgba32(255, 255, 255, 0)));
image.Save(imagepath, new PngEncoder());
}
stream.Close();
}
catch // error creating image
{
imagepath = "";
image.Save(imagepath, new PngEncoder());
}
stream.Close();
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Error}", filepath, width, height, mode, ex.Message);
imagepath = "";
}
return imagepath;

View File

@ -39,6 +39,10 @@ namespace Oqtane.Controllers
if (IsAuthorized(entityname, entityid, PermissionNames.View))
{
settings = _settings.GetSettings(entityname, entityid).ToList();
if (entityname == EntityNames.Site && !User.IsInRole(RoleNames.Admin))
{
settings = settings.Where(item => item.IsPublic).ToList();
}
}
else
{
@ -55,6 +59,10 @@ namespace Oqtane.Controllers
Setting setting = _settings.GetSetting(id);
if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.View))
{
if (setting.EntityName == EntityNames.Site && !User.IsInRole(RoleNames.Admin) && !setting.IsPublic)
{
setting = null;
}
return setting;
}
else
@ -72,10 +80,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.AddSetting(setting);
if (setting.EntityName == EntityNames.Module)
{
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId);
}
AddSyncEvent(setting.EntityName);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Setting Added {Setting}", setting);
}
else
@ -94,10 +99,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.UpdateSetting(setting);
if (setting.EntityName == EntityNames.Module)
{
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId);
}
AddSyncEvent(setting.EntityName);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Updated {Setting}", setting);
}
else
@ -117,10 +119,7 @@ namespace Oqtane.Controllers
if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
_settings.DeleteSetting(id);
if (setting.EntityName == EntityNames.Module)
{
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId);
}
AddSyncEvent(setting.EntityName);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Setting Deleted {Setting}", setting);
}
else
@ -144,7 +143,14 @@ namespace Oqtane.Controllers
authorized = User.IsInRole(RoleNames.Host);
break;
case EntityNames.Site:
authorized = User.IsInRole(RoleNames.Admin);
if (permissionName == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Admin);
}
else
{
authorized = true;
}
break;
case EntityNames.Page:
case EntityNames.Module:
@ -161,5 +167,17 @@ namespace Oqtane.Controllers
}
return authorized;
}
private void AddSyncEvent(string EntityName)
{
switch (EntityName)
{
case EntityNames.Module:
case EntityNames.Page:
case EntityNames.Site:
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId);
break;
}
}
}
}

View File

@ -15,13 +15,15 @@ namespace Oqtane.Controllers
public class SiteController : Controller
{
private readonly ISiteRepository _sites;
private readonly ISettingRepository _settings;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public SiteController(ISiteRepository sites, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
public SiteController(ISiteRepository sites, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
{
_sites = sites;
_settings = settings;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
@ -42,6 +44,12 @@ namespace Oqtane.Controllers
var site = _sites.GetSite(id);
if (site.SiteId == _alias.SiteId)
{
var settings = _settings.GetSettings(EntityNames.Site, site.SiteId);
if (!User.IsInRole(RoleNames.Admin))
{
settings = settings.Where(item => item.IsPublic);
}
site.Settings = settings.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
return site;
}
else

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.02.03.00.02")]
public class AddSettingIsPublic : MultiDatabaseMigration
{
public AddSettingIsPublic(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.AddBooleanColumn("IsPublic", true);
settingEntityBuilder.UpdateColumn("IsPublic", "0");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.DropColumn("IsPublic");
}
}
}

View File

@ -104,10 +104,13 @@ namespace Oqtane.Models
/// </summary>
[NotMapped]
public List<Resource> Resources { get; set; }
[NotMapped]
public string Permissions { get; set; }
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
[NotMapped]
public int Level { get; set; }

View File

@ -32,6 +32,11 @@ namespace Oqtane.Models
/// </summary>
public string SettingValue { get; set; }
/// <summary>
/// Indicates if this setting is publicly available - only applicable to Site Settings as other entities have more granular permissions
/// </summary>
public bool IsPublic { get; set; }
#region IAuditable Properties
/// <inheritdoc/>

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models
@ -81,6 +82,9 @@ namespace Oqtane.Models
[NotMapped]
public string SiteTemplateType { get; set; }
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
[NotMapped]
[Obsolete("This property is deprecated.", false)]
public string DefaultLayoutType { get; set; }

View File

@ -112,10 +112,10 @@ namespace Oqtane.Shared
return $"{aliasUrl}{Constants.ContentUrl}{fileId}{method}";
}
public static string ImageUrl(Alias alias, int fileId, string size, string mode)
public static string ImageUrl(Alias alias, int fileId, int width, int height, string mode)
{
var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : "";
return $"{aliasUrl}{Constants.ImageUrl}{fileId}/{size}/{mode}";
return $"{aliasUrl}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}";
}
public static string TenantUrl(Alias alias, string url)