Merge remote-tracking branch 'oqtane/dev' into dev
This commit is contained in:
@ -179,10 +179,6 @@
|
||||
ManageScripts(resources, alias);
|
||||
|
||||
// generate scripts
|
||||
if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server)
|
||||
{
|
||||
_scripts += CreateReconnectScript();
|
||||
}
|
||||
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
|
||||
{
|
||||
_scripts += CreatePWAScript(alias, site, route);
|
||||
@ -196,28 +192,29 @@
|
||||
_bodyResources += ParseScripts(site.BodyContent);
|
||||
|
||||
// set culture if not specified
|
||||
string culture = Context.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||
if (culture == null)
|
||||
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
|
||||
if (cultureCookie == null)
|
||||
{
|
||||
// get default language for site
|
||||
if (site.Languages.Any())
|
||||
{
|
||||
// use default language if specified otherwise use first language in collection
|
||||
culture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
|
||||
cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
|
||||
}
|
||||
else
|
||||
{
|
||||
culture = LocalizationManager.GetDefaultCulture();
|
||||
// fallback language
|
||||
cultureCookie = LocalizationManager.GetDefaultCulture();
|
||||
}
|
||||
SetLocalizationCookie(culture);
|
||||
// convert language code to culture cookie format (ie. "c=en|uic=en")
|
||||
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie));
|
||||
SetLocalizationCookie(cultureCookie);
|
||||
}
|
||||
|
||||
// set language for page
|
||||
if (!string.IsNullOrEmpty(culture))
|
||||
if (!string.IsNullOrEmpty(cultureCookie))
|
||||
{
|
||||
// localization cookie value in form of c=en|uic=en
|
||||
_language = culture.Split('|')[0];
|
||||
_language = _language.Replace("c=", "");
|
||||
_language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name;
|
||||
}
|
||||
|
||||
// create initial PageState
|
||||
@ -494,25 +491,6 @@
|
||||
"</script>" + Environment.NewLine;
|
||||
}
|
||||
|
||||
private string CreateReconnectScript()
|
||||
{
|
||||
return Environment.NewLine +
|
||||
"<script>" + Environment.NewLine +
|
||||
" // Interactive Blazor Server Reconnect" + Environment.NewLine +
|
||||
" new MutationObserver((mutations, observer) => {" + Environment.NewLine +
|
||||
" if (document.querySelector('#components-reconnect-modal h5 a')) {" + Environment.NewLine +
|
||||
" async function attemptReload() {" + Environment.NewLine +
|
||||
" await fetch('');" + Environment.NewLine +
|
||||
" location.reload();" + Environment.NewLine +
|
||||
" }" + Environment.NewLine +
|
||||
" observer.disconnect();" + Environment.NewLine +
|
||||
" attemptReload();" + Environment.NewLine +
|
||||
" setInterval(attemptReload, 5000);" + Environment.NewLine +
|
||||
" }" + Environment.NewLine +
|
||||
" }).observe(document.body, { childList: true, subtree: true });" + Environment.NewLine +
|
||||
"</script>" + Environment.NewLine;
|
||||
}
|
||||
|
||||
private string CreateScrollPositionScript()
|
||||
{
|
||||
return Environment.NewLine +
|
||||
@ -522,7 +500,7 @@
|
||||
" let currentUrl = window.location.pathname;" + Environment.NewLine +
|
||||
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
|
||||
" let newUrl = window.location.pathname;" + Environment.NewLine +
|
||||
" if (currentUrl != newUrl) {" + Environment.NewLine +
|
||||
" if (currentUrl !== newUrl || window.location.hash === '#top') {" + Environment.NewLine +
|
||||
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
|
||||
" }" + Environment.NewLine +
|
||||
" currentUrl = newUrl;" + Environment.NewLine +
|
||||
@ -534,9 +512,9 @@
|
||||
|
||||
private string ParseScripts(string content)
|
||||
{
|
||||
// iterate scripts
|
||||
var scripts = "";
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
// in interactive render mode, parse scripts from content and inject into page
|
||||
if (_renderMode == RenderModes.Interactive && !string.IsNullOrEmpty(content))
|
||||
{
|
||||
var index = content.IndexOf("<script");
|
||||
while (index >= 0)
|
||||
@ -602,19 +580,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLocalizationCookie(string culture)
|
||||
private void SetLocalizationCookie(string cookieValue)
|
||||
{
|
||||
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
|
||||
{
|
||||
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
|
||||
Secure = true, // Ensure the cookie is only sent over HTTPS
|
||||
HttpOnly = true // Optional: Helps mitigate XSS attacks
|
||||
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
|
||||
};
|
||||
|
||||
Context.Response.Cookies.Append(
|
||||
CookieRequestCultureProvider.DefaultCookieName,
|
||||
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
|
||||
Shared.CookieRequestCultureProvider.DefaultCookieName,
|
||||
cookieValue,
|
||||
cookieOptions
|
||||
);
|
||||
}
|
||||
@ -644,6 +622,16 @@
|
||||
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
|
||||
}
|
||||
}
|
||||
// theme settings components are dynamically loaded within the framework Page Management module
|
||||
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
|
||||
{
|
||||
var settingsType = Type.GetType(theme.ThemeSettingsType);
|
||||
if (settingsType != null)
|
||||
{
|
||||
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
|
||||
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
|
||||
{
|
||||
@ -686,25 +674,49 @@
|
||||
|
||||
// ensure component exists and implements IModuleControl
|
||||
module.ModuleType = "";
|
||||
Type moduletype = Type.GetType(typename, false, true); // case insensitive
|
||||
var moduletype = Type.GetType(typename, false, true); // case insensitive
|
||||
if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
|
||||
{
|
||||
module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
|
||||
}
|
||||
if (moduletype != null && module.ModuleType != "")
|
||||
{
|
||||
var obj = Activator.CreateInstance(moduletype) as IModuleControl;
|
||||
if (obj != null)
|
||||
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
|
||||
if (moduleobject != null)
|
||||
{
|
||||
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
|
||||
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
|
||||
|
||||
// settings components are dynamically loaded within the framework Settings module
|
||||
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
|
||||
{
|
||||
// settings components are embedded within a framework settings module
|
||||
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true);
|
||||
// module settings component
|
||||
var settingsType = "";
|
||||
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
|
||||
{
|
||||
// module settings type explicitly declared in IModule interface
|
||||
settingsType = module.ModuleDefinition.SettingsType;
|
||||
}
|
||||
else
|
||||
{
|
||||
// legacy support - module settings type determined by convention
|
||||
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
|
||||
}
|
||||
moduletype = Type.GetType(settingsType, false, true);
|
||||
if (moduletype != null)
|
||||
{
|
||||
obj = Activator.CreateInstance(moduletype) as IModuleControl;
|
||||
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
|
||||
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
|
||||
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
|
||||
}
|
||||
|
||||
// container settings component
|
||||
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
|
||||
{
|
||||
moduletype = Type.GetType(theme.ContainerSettingsType);
|
||||
if (moduletype != null)
|
||||
{
|
||||
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
|
||||
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,10 @@ using Oqtane.Infrastructure;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Extensions;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using System.IO.Compression;
|
||||
using Oqtane.Services;
|
||||
|
||||
// ReSharper disable StringIndexOfIsCultureSpecific.1
|
||||
|
||||
@ -38,7 +37,9 @@ namespace Oqtane.Controllers
|
||||
private readonly ILogManager _logger;
|
||||
private readonly Alias _alias;
|
||||
private readonly ISettingRepository _settingRepository;
|
||||
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
|
||||
private readonly IImageService _imageService;
|
||||
|
||||
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService)
|
||||
{
|
||||
_environment = environment;
|
||||
_files = files;
|
||||
@ -48,6 +49,7 @@ namespace Oqtane.Controllers
|
||||
_logger = logger;
|
||||
_alias = tenantManager.GetAlias();
|
||||
_settingRepository = settingRepository;
|
||||
_imageService = imageService;
|
||||
}
|
||||
|
||||
// GET: api/<controller>?folder=x
|
||||
@ -425,11 +427,11 @@ namespace Oqtane.Controllers
|
||||
// POST api/<controller>/upload
|
||||
[EnableCors(Constants.MauiCorsPolicy)]
|
||||
[HttpPost("upload")]
|
||||
public async Task UploadFile(string folder, IFormFile formfile)
|
||||
public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
|
||||
{
|
||||
if (formfile == null || formfile.Length <= 0)
|
||||
{
|
||||
return;
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ensure filename is valid
|
||||
@ -437,7 +439,7 @@ namespace Oqtane.Controllers
|
||||
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;
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
string folderPath = "";
|
||||
@ -492,6 +494,8 @@ namespace Oqtane.Controllers
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<string> MergeFile(string folder, string filename)
|
||||
@ -513,7 +517,7 @@ namespace Oqtane.Controllers
|
||||
bool success = true;
|
||||
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
|
||||
{
|
||||
foreach (string filepart in fileparts)
|
||||
foreach (string filepart in fileparts.Order())
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -679,22 +683,18 @@ namespace Oqtane.Controllers
|
||||
var filepath = _files.GetFilePath(file);
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
// validation
|
||||
if (!Enum.TryParse(mode, true, out ResizeMode _)) mode = "crop";
|
||||
if (!Enum.TryParse(position, true, out AnchorPositionMode _)) position = "center";
|
||||
if (!Color.TryParseHex("#" + background, out _)) background = "transparent";
|
||||
if (!int.TryParse(rotate, out _)) rotate = "0";
|
||||
rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate;
|
||||
if (!bool.TryParse(recreate, out _)) recreate = "false";
|
||||
|
||||
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png");
|
||||
string format = "png";
|
||||
|
||||
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
|
||||
if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate))
|
||||
{
|
||||
// user has edit access to folder or folder supports the image size being created
|
||||
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) ||
|
||||
(!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString()))))
|
||||
{
|
||||
imagepath = CreateImage(filepath, width, height, mode, position, background, rotate, imagepath);
|
||||
imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, format, imagepath);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -741,70 +741,6 @@ namespace Oqtane.Controllers
|
||||
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null;
|
||||
}
|
||||
|
||||
private string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
stream.Position = 0;
|
||||
using (var image = Image.Load(stream))
|
||||
{
|
||||
int.TryParse(rotate, out int angle);
|
||||
Enum.TryParse(mode, true, out ResizeMode resizemode);
|
||||
Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode);
|
||||
|
||||
PngEncoder encoder;
|
||||
|
||||
if (background != "transparent")
|
||||
{
|
||||
image.Mutate(x => x
|
||||
.AutoOrient() // auto orient the image
|
||||
.Rotate(angle)
|
||||
.Resize(new ResizeOptions
|
||||
{
|
||||
Mode = resizemode,
|
||||
Position = anchorpositionmode,
|
||||
Size = new Size(width, height),
|
||||
PadColor = Color.ParseHex("#" + background)
|
||||
}));
|
||||
|
||||
encoder = new PngEncoder();
|
||||
}
|
||||
else
|
||||
{
|
||||
image.Mutate(x => x
|
||||
.AutoOrient() // auto orient the image
|
||||
.Rotate(angle)
|
||||
.Resize(new ResizeOptions
|
||||
{
|
||||
Mode = resizemode,
|
||||
Position = anchorpositionmode,
|
||||
Size = new Size(width, height)
|
||||
}));
|
||||
|
||||
encoder = new PngEncoder
|
||||
{
|
||||
ColorType = PngColorType.RgbWithAlpha,
|
||||
TransparentColorMode = PngTransparentColorMode.Preserve,
|
||||
BitDepth = PngBitDepth.Bit8,
|
||||
CompressionLevel = PngCompressionLevel.BestSpeed
|
||||
};
|
||||
}
|
||||
|
||||
image.Save(imagepath, encoder);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message);
|
||||
imagepath = "";
|
||||
}
|
||||
|
||||
return imagepath;
|
||||
}
|
||||
|
||||
private string GetFolderPath(string folder)
|
||||
{
|
||||
return Utilities.PathCombine(_environment.ContentRootPath, folder);
|
||||
|
@ -55,6 +55,10 @@ namespace Oqtane.Controllers
|
||||
else
|
||||
{
|
||||
languages = _languages.GetLanguages(SiteId).ToList();
|
||||
foreach (Language language in languages)
|
||||
{
|
||||
language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(packagename))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))
|
||||
@ -85,6 +89,7 @@ namespace Oqtane.Controllers
|
||||
var language = _languages.GetLanguage(id);
|
||||
if (language != null && language.SiteId == _alias.SiteId)
|
||||
{
|
||||
language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName;
|
||||
return language;
|
||||
}
|
||||
else
|
||||
|
@ -351,9 +351,9 @@ namespace Oqtane.Controllers
|
||||
return new Dictionary<string, object>()
|
||||
{
|
||||
{ "FrameworkVersion", Constants.Version },
|
||||
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Client.dll</HintPath></Reference>" },
|
||||
{ "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Server.dll</HintPath></Reference>" },
|
||||
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Shared.dll</HintPath></Reference>" },
|
||||
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
|
||||
{ "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Server.dll</HintPath></Reference>" },
|
||||
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -8,10 +8,6 @@ using Oqtane.Infrastructure;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Security;
|
||||
using System.Net;
|
||||
using System.Reflection.Metadata;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using System.Linq;
|
||||
|
||||
namespace Oqtane.Controllers
|
||||
{
|
||||
|
@ -189,7 +189,7 @@ namespace Oqtane.Controllers
|
||||
public void Delete(string entityName, int entityId, string settingName)
|
||||
{
|
||||
Setting setting = _settings.GetSetting(entityName, entityId, settingName);
|
||||
if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
|
||||
if (setting != null && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
|
||||
{
|
||||
_settings.DeleteSetting(setting.EntityName, setting.SettingId);
|
||||
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Delete);
|
||||
@ -199,7 +199,7 @@ namespace Oqtane.Controllers
|
||||
{
|
||||
if (entityName != EntityNames.Visitor)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "User Not Authorized To Delete Setting {Setting}", setting);
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For Entity {EntityName} Id {EntityId} Name {SettingName}", entityName, entityId, settingName);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
}
|
||||
|
@ -267,8 +267,8 @@ namespace Oqtane.Controllers
|
||||
return new Dictionary<string, object>()
|
||||
{
|
||||
{ "FrameworkVersion", Constants.Version },
|
||||
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Client.dll</HintPath></Reference>" },
|
||||
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Shared.dll</HintPath></Reference>" },
|
||||
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
|
||||
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Oqtane.Shared;
|
||||
using System;
|
||||
using System.Net;
|
||||
using Oqtane.Enums;
|
||||
using Oqtane.Infrastructure;
|
||||
@ -120,11 +119,15 @@ namespace Oqtane.Controllers
|
||||
filtered = new User();
|
||||
|
||||
// public properties
|
||||
filtered.SiteId = user.SiteId;
|
||||
filtered.UserId = user.UserId;
|
||||
filtered.Username = user.Username;
|
||||
filtered.DisplayName = user.DisplayName;
|
||||
|
||||
// restricted properties
|
||||
filtered.Password = "";
|
||||
filtered.TwoFactorCode = "";
|
||||
filtered.SecurityStamp = "";
|
||||
|
||||
// include private properties if authenticated user is accessing their own user account os is an administrator
|
||||
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId)
|
||||
@ -260,8 +263,24 @@ namespace Oqtane.Controllers
|
||||
[Authorize]
|
||||
public async Task Logout([FromBody] User user)
|
||||
{
|
||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
|
||||
if (_userPermissions.GetUser(User).UserId == user.UserId)
|
||||
{
|
||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
|
||||
}
|
||||
}
|
||||
|
||||
// POST api/<controller>/logout
|
||||
[HttpPost("logouteverywhere")]
|
||||
[Authorize]
|
||||
public async Task LogoutEverywhere([FromBody] User user)
|
||||
{
|
||||
if (_userPermissions.GetUser(User).UserId == user.UserId)
|
||||
{
|
||||
await _userManager.LogoutUserEverywhere(user);
|
||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout Everywhere {Username}", (user != null) ? user.Username : "");
|
||||
}
|
||||
}
|
||||
|
||||
// POST api/<controller>/verify
|
||||
@ -328,6 +347,13 @@ namespace Oqtane.Controllers
|
||||
return user;
|
||||
}
|
||||
|
||||
// GET api/<controller>/validate/x
|
||||
[HttpGet("validateuser")]
|
||||
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
|
||||
{
|
||||
return await _userManager.ValidateUser(username, email, password);
|
||||
}
|
||||
|
||||
// GET api/<controller>/validate/x
|
||||
[HttpGet("validate/{password}")]
|
||||
public async Task<bool> Validate(string password)
|
||||
@ -382,6 +408,7 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
if (roles != "") roles = ";" + roles;
|
||||
user.Roles = roles;
|
||||
user.SecurityStamp = User.SecurityStamp();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Interfaces;
|
||||
|
||||
namespace Oqtane.Repository.Databases.Interfaces
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Extensions
|
||||
@ -41,9 +40,9 @@ namespace Oqtane.Extensions
|
||||
|
||||
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SiteKeyClaimType))
|
||||
{
|
||||
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
|
||||
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SiteKeyClaimType).Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -71,6 +70,18 @@ namespace Oqtane.Extensions
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static string SecurityStamp(this ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SecurityStampClaimType))
|
||||
{
|
||||
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SecurityStampClaimType).Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsOnlyInRole(this ClaimsPrincipal claimsPrincipal, string role)
|
||||
{
|
||||
var identity = claimsPrincipal.Identities.FirstOrDefault(item => item.AuthenticationType == Constants.AuthenticationScheme);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
// ReSharper disable ConvertToUsingDeclaration
|
||||
|
||||
@ -9,7 +10,8 @@ namespace Oqtane.Extensions
|
||||
{
|
||||
public static DbContextOptionsBuilder UseOqtaneDatabase([NotNull] this DbContextOptionsBuilder optionsBuilder, IDatabase database, string connectionString)
|
||||
{
|
||||
database.UseDatabase(optionsBuilder, connectionString);
|
||||
database.UseDatabase(optionsBuilder, connectionString)
|
||||
.ConfigureWarnings(warnings => warnings.Log(RelationalEventId.PendingModelChangesWarning));
|
||||
|
||||
return optionsBuilder;
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
services.AddScoped<ISearchResultsService, SearchResultsService>();
|
||||
services.AddScoped<ISearchService, SearchService>();
|
||||
services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
|
||||
services.AddScoped<IImageService, ImageService>();
|
||||
|
||||
// providers
|
||||
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
|
||||
@ -112,8 +113,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
|
||||
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
|
||||
{
|
||||
// repositories
|
||||
// services
|
||||
services.AddTransient<ISiteService, ServerSiteService>();
|
||||
services.AddTransient<ILocalizationCookieService, ServerLocalizationCookieService>();
|
||||
|
||||
// repositories
|
||||
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
|
||||
services.AddTransient<IThemeRepository, ThemeRepository>();
|
||||
services.AddTransient<IAliasRepository, AliasRepository>();
|
||||
@ -129,7 +133,6 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
services.AddTransient<IPermissionRepository, PermissionRepository>();
|
||||
services.AddTransient<ISettingRepository, SettingRepository>();
|
||||
services.AddTransient<ILogRepository, LogRepository>();
|
||||
services.AddTransient<ILocalizationManager, LocalizationManager>();
|
||||
services.AddTransient<IJobRepository, JobRepository>();
|
||||
services.AddTransient<IJobLogRepository, JobLogRepository>();
|
||||
services.AddTransient<INotificationRepository, NotificationRepository>();
|
||||
@ -152,12 +155,12 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
services.AddTransient<ILogManager, LogManager>();
|
||||
services.AddTransient<IUpgradeManager, UpgradeManager>();
|
||||
services.AddTransient<IUserManager, UserManager>();
|
||||
|
||||
// obsolete - replaced by ITenantManager
|
||||
services.AddTransient<ITenantResolver, TenantResolver>();
|
||||
|
||||
services.AddTransient<ILocalizationManager, LocalizationManager>();
|
||||
services.AddTransient<ITokenReplace, TokenReplace>();
|
||||
|
||||
// obsolete
|
||||
services.AddTransient<ITenantResolver, TenantResolver>(); // replaced by ITenantManager
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -169,6 +172,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.LoginPath = "/login"; // overrides .NET Identity default of /Account/Login
|
||||
options.Events.OnRedirectToLogin = context =>
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
|
@ -527,28 +527,63 @@ namespace Oqtane.Extensions
|
||||
// manage user
|
||||
if (user != null)
|
||||
{
|
||||
// create claims identity
|
||||
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
|
||||
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
|
||||
identity.Label = ExternalLoginStatus.Success;
|
||||
|
||||
// update user
|
||||
user.LastLoginOn = DateTime.UtcNow;
|
||||
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
|
||||
_users.UpdateUser(user);
|
||||
|
||||
// external roles
|
||||
// manage roles
|
||||
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
|
||||
var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
|
||||
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
|
||||
// external roles
|
||||
if (claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
|
||||
var _roles = httpContext.RequestServices.GetRequiredService<IRoleRepository>();
|
||||
var roles = _roles.GetRoles(user.SiteId).ToList(); // global roles excluded ie. host users cannot be added/deleted
|
||||
|
||||
var mappings = httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimMappings", "").Split(',');
|
||||
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
|
||||
{
|
||||
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
|
||||
var rolename = claim.Value;
|
||||
if (mappings.Any(item => item.StartsWith(rolename + ":")))
|
||||
{
|
||||
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
|
||||
rolename = mappings.First(item => item.StartsWith(rolename + ":")).Split(':')[1];
|
||||
}
|
||||
var role = roles.FirstOrDefault(item => item.Name == rolename);
|
||||
if (role != null)
|
||||
{
|
||||
if (!userRoles.Any(item => item.RoleId == role.RoleId && item.UserId == user.UserId))
|
||||
{
|
||||
var userRole = new UserRole();
|
||||
userRole.RoleId = role.RoleId;
|
||||
userRole.UserId = user.UserId;
|
||||
_userRoles.AddUserRole(userRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:SynchronizeRoles", "false")))
|
||||
{
|
||||
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
|
||||
foreach (var userRole in userRoles)
|
||||
{
|
||||
var role = roles.FirstOrDefault(item => item.RoleId == userRole.RoleId);
|
||||
if (role != null)
|
||||
{
|
||||
var rolename = role.Name;
|
||||
if (mappings.Any(item => item.EndsWith(":" + rolename)))
|
||||
{
|
||||
rolename = mappings.First(item => item.EndsWith(":" + rolename)).Split(':')[0];
|
||||
}
|
||||
if (!claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "") && item.Value == rolename))
|
||||
{
|
||||
_userRoles.DeleteUserRole(userRole.UserRoleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -556,6 +591,12 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
// create claims identity
|
||||
identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||
user.SecurityStamp = identityuser.SecurityStamp;
|
||||
identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles);
|
||||
identity.Label = ExternalLoginStatus.Success;
|
||||
|
||||
// user profile claims
|
||||
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
|
||||
{
|
||||
@ -604,13 +645,13 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
|
||||
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
|
||||
}
|
||||
}
|
||||
else // claims invalid
|
||||
{
|
||||
identity.Label = ExternalLoginStatus.MissingClaims;
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
|
||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Satisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
|
||||
}
|
||||
|
||||
return identity;
|
||||
|
@ -155,7 +155,7 @@ namespace Oqtane.Infrastructure
|
||||
// add new site
|
||||
if (install.TenantName != TenantNames.Master && install.ConnectionString.Contains("="))
|
||||
{
|
||||
_configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, false);
|
||||
_configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, true);
|
||||
}
|
||||
if (install.TenantName == TenantNames.Master && !install.ConnectionString.Contains("="))
|
||||
{
|
||||
@ -375,7 +375,6 @@ namespace Oqtane.Infrastructure
|
||||
AddEFMigrationsHistory(sql, _configManager.GetSetting($"{SettingKeys.ConnectionStringsSection}:{tenant.DBConnectionString}", ""), tenant.DBType, tenant.Version, false);
|
||||
// push latest model into database
|
||||
tenantDbContext.Database.Migrate();
|
||||
result.Success = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -384,35 +383,35 @@ namespace Oqtane.Infrastructure
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
|
||||
// execute any version specific upgrade logic
|
||||
var version = tenant.Version;
|
||||
var index = Array.FindIndex(versions, item => item == version);
|
||||
if (index != (versions.Length - 1))
|
||||
if (string.IsNullOrEmpty(result.Message))
|
||||
{
|
||||
try
|
||||
// execute any version specific upgrade logic
|
||||
var version = tenant.Version;
|
||||
var index = Array.FindIndex(versions, item => item == version);
|
||||
if (index != (versions.Length - 1))
|
||||
{
|
||||
for (var i = (index + 1); i < versions.Length; i++)
|
||||
try
|
||||
{
|
||||
upgrades.Upgrade(tenant, versions[i]);
|
||||
for (var i = (index + 1); i < versions.Length; i++)
|
||||
{
|
||||
upgrades.Upgrade(tenant, versions[i]);
|
||||
}
|
||||
tenant.Version = versions[versions.Length - 1];
|
||||
db.Entry(tenant).State = EntityState.Modified;
|
||||
db.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
tenant.Version = versions[versions.Length - 1];
|
||||
db.Entry(tenant).State = EntityState.Modified;
|
||||
db.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
|
||||
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(result.Message))
|
||||
{
|
||||
result.Success = true;
|
||||
}
|
||||
result.Success = string.IsNullOrEmpty(result.Message);
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -588,7 +587,7 @@ namespace Oqtane.Infrastructure
|
||||
|
||||
// add host role
|
||||
var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0;
|
||||
var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null };
|
||||
var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null, IgnoreSecurityStamp = true };
|
||||
userRoles.AddUserRole(userRole);
|
||||
}
|
||||
}
|
||||
|
@ -89,9 +89,9 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
|
||||
// validate recipient
|
||||
if (string.IsNullOrEmpty(notification.ToEmail))
|
||||
if (string.IsNullOrEmpty(notification.ToEmail) || !MailAddress.TryCreate(notification.ToEmail, out _))
|
||||
{
|
||||
log += "Recipient Missing For NotificationId: " + notification.NotificationId + "<br />";
|
||||
log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}<br />";
|
||||
notification.IsDeleted = true;
|
||||
notificationRepository.UpdateNotification(notification);
|
||||
}
|
||||
|
@ -59,8 +59,15 @@ namespace Oqtane.Infrastructure
|
||||
var currentTime = DateTime.UtcNow;
|
||||
var lastIndexedOn = Convert.ToDateTime(siteSettings.GetValue(SearchLastIndexedOnSetting, DateTime.MinValue.ToString()));
|
||||
|
||||
if (lastIndexedOn == DateTime.MinValue)
|
||||
{
|
||||
// reset index
|
||||
log += $"*Site Index Reset*<br />";
|
||||
await searchService.DeleteSearchContentsAsync(site.SiteId);
|
||||
}
|
||||
|
||||
var ignorePages = siteSettings.GetValue(SearchIgnorePagesSetting, "").Split(',');
|
||||
var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "").Split(',');
|
||||
var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "File").Split(',');
|
||||
|
||||
var pages = pageRepository.GetPages(site.SiteId);
|
||||
var pageModules = pageModuleRepository.GetPageModules(site.SiteId);
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oqtane.Enums;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Repository;
|
||||
@ -20,8 +21,9 @@ namespace Oqtane.Infrastructure
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly IUserRoleRepository _userRoles;
|
||||
private readonly INotificationRepository _notifications;
|
||||
private readonly ILogger<LogManager> _filelogger;
|
||||
|
||||
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications)
|
||||
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications, ILogger<LogManager> filelogger)
|
||||
{
|
||||
_logs = logs;
|
||||
_tenantManager = tenantManager;
|
||||
@ -30,24 +32,25 @@ namespace Oqtane.Infrastructure
|
||||
_accessor = accessor;
|
||||
_userRoles = userRoles;
|
||||
_notifications = notifications;
|
||||
_filelogger = filelogger;
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args)
|
||||
public void Log(Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
|
||||
{
|
||||
Log(-1, level, @class, function, null, message, args);
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
|
||||
public void Log(Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
|
||||
{
|
||||
Log(-1, level, @class, function, exception, message, args);
|
||||
}
|
||||
|
||||
public void Log(int siteId, LogLevel level, object @class, LogFunction function, string message, params object[] args)
|
||||
public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
|
||||
{
|
||||
Log(siteId, level, @class, function, null, message, args);
|
||||
}
|
||||
|
||||
public void Log(int siteId, LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
|
||||
public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
|
||||
{
|
||||
Log log = new Log();
|
||||
|
||||
@ -60,7 +63,6 @@ namespace Oqtane.Infrastructure
|
||||
log.SiteId = alias.SiteId;
|
||||
}
|
||||
}
|
||||
if (log.SiteId == -1) return; // logs must be site specific
|
||||
|
||||
log.PageId = null;
|
||||
log.ModuleId = null;
|
||||
@ -92,7 +94,7 @@ namespace Oqtane.Infrastructure
|
||||
log.Feature = log.Category;
|
||||
}
|
||||
log.Function = Enum.GetName(typeof(LogFunction), function);
|
||||
log.Level = Enum.GetName(typeof(LogLevel), level);
|
||||
log.Level = Enum.GetName(typeof(Shared.LogLevel), level);
|
||||
if (exception != null)
|
||||
{
|
||||
log.Exception = exception.ToString();
|
||||
@ -112,14 +114,14 @@ namespace Oqtane.Infrastructure
|
||||
|
||||
public void Log(Log log)
|
||||
{
|
||||
LogLevel minlevel = LogLevel.Information;
|
||||
var minlevel = Shared.LogLevel.Information;
|
||||
var section = _config.GetSection("Logging:LogLevel:Default");
|
||||
if (section.Exists())
|
||||
{
|
||||
minlevel = Enum.Parse<LogLevel>(section.Value);
|
||||
minlevel = Enum.Parse<Shared.LogLevel>(section.Value);
|
||||
}
|
||||
|
||||
if (Enum.Parse<LogLevel>(log.Level) >= minlevel)
|
||||
if (Enum.Parse<Shared.LogLevel>(log.Level) >= minlevel)
|
||||
{
|
||||
log.LogDate = DateTime.UtcNow;
|
||||
log.Server = Environment.MachineName;
|
||||
@ -127,12 +129,19 @@ namespace Oqtane.Infrastructure
|
||||
log = ProcessStructuredLog(log);
|
||||
try
|
||||
{
|
||||
_logs.AddLog(log);
|
||||
SendNotification(log);
|
||||
if (log.SiteId != -1)
|
||||
{
|
||||
_logs.AddLog(log);
|
||||
SendNotification(log);
|
||||
}
|
||||
else // use file logger as fallback when site cannot be determined
|
||||
{
|
||||
_filelogger.Log(GetLogLevel(log.Level), "[" + log.Category + "] " + log.Message);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// an error occurred writing to the database
|
||||
// an error occurred writing the log
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,17 +165,11 @@ namespace Oqtane.Infrastructure
|
||||
names.Add(message.Substring(index + 1, message.IndexOf("}", index) - index - 1));
|
||||
if (values.Length > (names.Count - 1))
|
||||
{
|
||||
if (values[names.Count - 1] == null)
|
||||
{
|
||||
message = message.Replace("{" + names[names.Count - 1] + "}", "null");
|
||||
}
|
||||
else
|
||||
{
|
||||
message = message.Replace("{" + names[names.Count - 1] + "}", values[names.Count - 1].ToString());
|
||||
}
|
||||
var value = (values[names.Count - 1] == null) ? "null" : values[names.Count - 1].ToString();
|
||||
message = message.Replace("{" + names[names.Count - 1] + "}", value);
|
||||
}
|
||||
}
|
||||
index = message.IndexOf("{", index + 1);
|
||||
index = (index < message.Length - 1) ? message.IndexOf("{", index + 1) : -1;
|
||||
}
|
||||
// rebuild properties into dictionary
|
||||
Dictionary<string, object> propertyDictionary = new Dictionary<string, object>();
|
||||
@ -195,13 +198,13 @@ namespace Oqtane.Infrastructure
|
||||
|
||||
private void SendNotification(Log log)
|
||||
{
|
||||
LogLevel notifylevel = LogLevel.Error;
|
||||
Shared.LogLevel notifylevel = Shared.LogLevel.Error;
|
||||
var section = _config.GetSection("Logging:LogLevel:Notify");
|
||||
if (section.Exists())
|
||||
{
|
||||
notifylevel = Enum.Parse<LogLevel>(section.Value);
|
||||
notifylevel = Enum.Parse<Shared.LogLevel>(section.Value);
|
||||
}
|
||||
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel)
|
||||
if (Enum.Parse<Shared.LogLevel>(log.Level) >= notifylevel)
|
||||
{
|
||||
var subject = $"Site {log.Level} Notification";
|
||||
string body = $"Log Message: {log.Message}";
|
||||
@ -220,5 +223,26 @@ namespace Oqtane.Infrastructure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Microsoft.Extensions.Logging.LogLevel GetLogLevel(string level)
|
||||
{
|
||||
switch (Enum.Parse<Shared.LogLevel>(level))
|
||||
{
|
||||
case Shared.LogLevel.Trace:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Trace;
|
||||
case Shared.LogLevel.Debug:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Debug;
|
||||
case Shared.LogLevel.Information:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Information;
|
||||
case Shared.LogLevel.Warning:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Warning;
|
||||
case Shared.LogLevel.Error:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Error;
|
||||
case Shared.LogLevel.Critical:
|
||||
return Microsoft.Extensions.Logging.LogLevel.Critical;
|
||||
default:
|
||||
return Microsoft.Extensions.Logging.LogLevel.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Oqtane.Extensions;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Managers;
|
||||
using Oqtane.Security;
|
||||
using Oqtane.Shared;
|
||||
|
||||
@ -59,19 +58,18 @@ namespace Oqtane.Infrastructure
|
||||
|
||||
if (userid != null && username != null)
|
||||
{
|
||||
// create user identity
|
||||
var user = new User
|
||||
var _users = context.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
|
||||
var user = _users.GetUser(userid, alias.SiteId); // cached
|
||||
if (user != null && !user.IsDeleted)
|
||||
{
|
||||
UserId = int.Parse(userid),
|
||||
Username = username
|
||||
};
|
||||
|
||||
// set claims identity (note jwt already contains the roles - we are reloading to ensure most accurate permissions)
|
||||
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
|
||||
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
|
||||
context.User = new ClaimsPrincipal(claimsidentity);
|
||||
|
||||
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For UserId {UserId} And Username {Username}", user.UserId, user.Username);
|
||||
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user);
|
||||
context.User = new ClaimsPrincipal(claimsidentity);
|
||||
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validated But User {Username} Does Not Exist Or Is Deleted", user.Username);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -13,11 +13,13 @@ namespace Oqtane.Managers
|
||||
Task<User> UpdateUser(User user);
|
||||
Task DeleteUser(int userid, int siteid);
|
||||
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
|
||||
Task LogoutUserEverywhere(User user);
|
||||
Task<User> VerifyEmail(User user, string token);
|
||||
Task ForgotPassword(User user);
|
||||
Task<User> ResetPassword(User user, string token);
|
||||
User VerifyTwoFactor(User user, string token);
|
||||
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
|
||||
Task<UserValidateResult> ValidateUser(string username, string email, string password);
|
||||
Task<bool> ValidatePassword(string password);
|
||||
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ namespace Oqtane.Managers
|
||||
{
|
||||
user.SiteId = siteid;
|
||||
user.Roles = GetUserRoles(user.UserId, user.SiteId);
|
||||
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
|
||||
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
}
|
||||
@ -230,6 +231,7 @@ namespace Oqtane.Managers
|
||||
{
|
||||
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
|
||||
await _identityUserManager.UpdateAsync(identityuser);
|
||||
await _identityUserManager.UpdateSecurityStampAsync(identityuser); // will force user to sign in again
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -240,7 +242,8 @@ namespace Oqtane.Managers
|
||||
|
||||
if (user.Email != identityuser.Email)
|
||||
{
|
||||
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
|
||||
identityuser.Email = user.Email;
|
||||
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
|
||||
|
||||
// if email address changed and it is not confirmed, verification is required for new email address
|
||||
if (!user.EmailConfirmed)
|
||||
@ -262,7 +265,6 @@ namespace Oqtane.Managers
|
||||
user = _users.UpdateUser(user);
|
||||
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
|
||||
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
|
||||
_cache.Remove($"user:{user.UserId}:{alias.SiteKey}");
|
||||
user.Password = ""; // remove sensitive information
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
|
||||
}
|
||||
@ -370,7 +372,7 @@ namespace Oqtane.Managers
|
||||
user.LastLoginOn = DateTime.UtcNow;
|
||||
user.LastIPAddress = LastIPAddress;
|
||||
_users.UpdateUser(user);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress);
|
||||
|
||||
if (setCookie)
|
||||
{
|
||||
@ -417,6 +419,16 @@ namespace Oqtane.Managers
|
||||
|
||||
return user;
|
||||
}
|
||||
public async Task LogoutUserEverywhere(User user)
|
||||
{
|
||||
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||
if (identityuser != null)
|
||||
{
|
||||
await _identityUserManager.UpdateSecurityStampAsync(identityuser);
|
||||
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
|
||||
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<User> VerifyEmail(User user, string token)
|
||||
{
|
||||
@ -528,6 +540,30 @@ namespace Oqtane.Managers
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
|
||||
{
|
||||
var validateResult = new UserValidateResult { Succeeded = true };
|
||||
|
||||
//validate username
|
||||
var allowedChars = _identityUserManager.Options.User.AllowedUserNameCharacters;
|
||||
if (string.IsNullOrWhiteSpace(username) || (!string.IsNullOrEmpty(allowedChars) && username.Any(c => !allowedChars.Contains(c))))
|
||||
{
|
||||
validateResult.Succeeded = false;
|
||||
validateResult.Errors.Add("Message.Username.Invalid", string.Empty);
|
||||
}
|
||||
|
||||
//validate password
|
||||
var passwordValidator = new PasswordValidator<IdentityUser>();
|
||||
var passwordResult = await passwordValidator.ValidateAsync(_identityUserManager, null, password);
|
||||
if (!passwordResult.Succeeded)
|
||||
{
|
||||
validateResult.Succeeded = false;
|
||||
validateResult.Errors.Add("Message.Password.Invalid", string.Empty);
|
||||
}
|
||||
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidatePassword(string password)
|
||||
{
|
||||
var validator = new PasswordValidator<IdentityUser>();
|
||||
|
@ -319,6 +319,19 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||
schema: Schema);
|
||||
}
|
||||
|
||||
public virtual void AddForeignKey(string foreignKeyName, string columnName, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDelete)
|
||||
{
|
||||
_migrationBuilder.AddForeignKey(
|
||||
name: RewriteName(foreignKeyName),
|
||||
table: RewriteName(EntityTableName),
|
||||
column: RewriteName(columnName),
|
||||
principalTable: RewriteName(principalTable),
|
||||
principalColumn: RewriteName(principalColumn),
|
||||
principalSchema: RewriteName(principalSchema),
|
||||
onDelete: onDelete,
|
||||
schema: Schema);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Migration to add an Index to the Entity (table)
|
||||
/// </summary>
|
||||
@ -368,6 +381,7 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||
column: foreignKey.Column,
|
||||
principalTable: RewriteName(foreignKey.PrincipalTable),
|
||||
principalColumn: RewriteName(foreignKey.PrincipalColumn),
|
||||
principalSchema: RewriteName(foreignKey.PrincipalSchema),
|
||||
onDelete: foreignKey.OnDeleteAction);
|
||||
}
|
||||
|
||||
@ -381,6 +395,7 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||
column: RewriteName(foreignKey.ColumnName),
|
||||
principalTable: RewriteName(foreignKey.PrincipalTable),
|
||||
principalColumn: RewriteName(foreignKey.PrincipalColumn),
|
||||
principalSchema: RewriteName(foreignKey.PrincipalSchema),
|
||||
onDelete: foreignKey.OnDeleteAction,
|
||||
schema: Schema);
|
||||
}
|
||||
|
@ -16,6 +16,16 @@ namespace Oqtane.Migrations
|
||||
OnDeleteAction = onDeleteAction;
|
||||
}
|
||||
|
||||
public ForeignKey(string name, Expression<Func<TEntityBuilder, object>> column, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDeleteAction)
|
||||
{
|
||||
Name = name;
|
||||
Column = column;
|
||||
PrincipalTable = principalTable;
|
||||
PrincipalColumn = principalColumn;
|
||||
PrincipalSchema = principalSchema;
|
||||
OnDeleteAction = onDeleteAction;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Expression<Func<TEntityBuilder, object>> Column { get;}
|
||||
@ -34,6 +44,8 @@ namespace Oqtane.Migrations
|
||||
|
||||
public string PrincipalColumn { get; }
|
||||
|
||||
public string PrincipalSchema { get; } = "";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Migrations.EntityBuilders;
|
||||
using Oqtane.Repository;
|
||||
|
||||
namespace Oqtane.Migrations.Tenant
|
||||
{
|
||||
[DbContext(typeof(TenantDBContext))]
|
||||
[Migration("Tenant.05.02.04.01")]
|
||||
public class RemoveLanguageName : MultiDatabaseMigration
|
||||
{
|
||||
public RemoveLanguageName(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
languageEntityBuilder.DropColumn("Name");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// not implemented
|
||||
}
|
||||
}
|
||||
}
|
@ -45,10 +45,25 @@ namespace Oqtane.Modules.Admin.Files.Manager
|
||||
var path = folder.Path + file.Name;
|
||||
|
||||
var body = "";
|
||||
if (DocumentExtensions.Contains(Path.GetExtension(file.Name)))
|
||||
if (System.IO.File.Exists(_fileRepository.GetFilePath(file)))
|
||||
{
|
||||
// get the contents of the file
|
||||
body = System.IO.File.ReadAllText(_fileRepository.GetFilePath(file));
|
||||
// only non-binary files can be indexed
|
||||
if (DocumentExtensions.Contains(Path.GetExtension(file.Name)))
|
||||
{
|
||||
// get the contents of the file
|
||||
try
|
||||
{
|
||||
body = System.IO.File.ReadAllText(_fileRepository.GetFilePath(file));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// could not read the file
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
removed = true; // file does not exist on disk
|
||||
}
|
||||
|
||||
var searchContent = new SearchContent
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Version>5.2.2</Version>
|
||||
<Version>6.0.0</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.2</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</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.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.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-preview2.24304.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<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" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -14,6 +15,7 @@ using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Security;
|
||||
using Oqtane.Services;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Pages
|
||||
@ -28,8 +30,10 @@ namespace Oqtane.Pages
|
||||
private readonly ISyncManager _syncManager;
|
||||
private readonly ILogManager _logger;
|
||||
private readonly Alias _alias;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ISettingRepository _settingRepository;
|
||||
|
||||
public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
|
||||
public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService, ISettingRepository settingRepository)
|
||||
{
|
||||
_environment = environment;
|
||||
_files = files;
|
||||
@ -38,111 +42,228 @@ namespace Oqtane.Pages
|
||||
_syncManager = syncManager;
|
||||
_logger = logger;
|
||||
_alias = tenantManager.GetAlias();
|
||||
_imageService = imageService;
|
||||
_settingRepository = settingRepository;
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = path.Replace("\\", "/");
|
||||
var folderpath = "";
|
||||
var filename = "";
|
||||
|
||||
bool download = false;
|
||||
if (Request.Query.ContainsKey("download"))
|
||||
{
|
||||
download = true;
|
||||
}
|
||||
|
||||
var segments = path.Split('/');
|
||||
if (segments.Length > 0)
|
||||
{
|
||||
filename = segments[segments.Length - 1].ToLower();
|
||||
if (segments.Length > 1)
|
||||
{
|
||||
folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/";
|
||||
}
|
||||
}
|
||||
|
||||
Models.File file;
|
||||
if (folderpath == "id/" && int.TryParse(filename, out int fileid))
|
||||
{
|
||||
file = _files.GetFile(fileid, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
file = _files.GetFile(_alias.SiteId, folderpath, filename);
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
if (file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
|
||||
{
|
||||
// calculate ETag using last modified date and file size
|
||||
var etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16);
|
||||
|
||||
var header = "";
|
||||
if (HttpContext.Request.Headers.ContainsKey(HeaderNames.IfNoneMatch))
|
||||
{
|
||||
header = HttpContext.Request.Headers[HeaderNames.IfNoneMatch].ToString();
|
||||
}
|
||||
|
||||
if (!header.Equals(etag))
|
||||
{
|
||||
var filepath = _files.GetFilePath(file);
|
||||
if (System.IO.File.Exists(filepath))
|
||||
{
|
||||
if (download)
|
||||
{
|
||||
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
|
||||
return PhysicalFile(filepath, file.GetMimeType(), file.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
|
||||
return PhysicalFile(filepath, file.GetMimeType());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
|
||||
return Content(String.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// look for url mapping
|
||||
var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename);
|
||||
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
|
||||
{
|
||||
var url = urlMapping.MappedUrl;
|
||||
if (!url.StartsWith("http"))
|
||||
{
|
||||
var uri = new Uri(HttpContext.Request.GetEncodedUrl());
|
||||
url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url;
|
||||
}
|
||||
return RedirectPermanent(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
path = path.Replace("\\", "/");
|
||||
var folderpath = "";
|
||||
var filename = "";
|
||||
|
||||
bool download = false;
|
||||
if (Request.Query.ContainsKey("download"))
|
||||
{
|
||||
download = true;
|
||||
}
|
||||
|
||||
var segments = path.Split('/');
|
||||
if (segments.Length > 0)
|
||||
{
|
||||
filename = segments[segments.Length - 1].ToLower();
|
||||
if (segments.Length > 1)
|
||||
{
|
||||
folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/";
|
||||
}
|
||||
}
|
||||
|
||||
Models.File file;
|
||||
if (folderpath == "id/" && int.TryParse(filename, out int fileid))
|
||||
{
|
||||
file = _files.GetFile(fileid, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
file = _files.GetFile(_alias.SiteId, folderpath, filename);
|
||||
}
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
// look for url mapping
|
||||
|
||||
var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename);
|
||||
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
|
||||
{
|
||||
var url = urlMapping.MappedUrl;
|
||||
if (!url.StartsWith("http"))
|
||||
{
|
||||
var uri = new Uri(HttpContext.Request.GetEncodedUrl());
|
||||
url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url;
|
||||
}
|
||||
|
||||
// appends the query string to the redirect url
|
||||
if (Request.QueryString.HasValue && !string.IsNullOrWhiteSpace(Request.QueryString.Value))
|
||||
{
|
||||
if (url.Contains('?'))
|
||||
{
|
||||
url += "&";
|
||||
}
|
||||
else
|
||||
{
|
||||
url += "?";
|
||||
}
|
||||
|
||||
url += Request.QueryString.Value.Substring(1);
|
||||
}
|
||||
|
||||
return RedirectPermanent(url);
|
||||
}
|
||||
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
string etag;
|
||||
string downloadName = file.Name;
|
||||
string filepath = _files.GetFilePath(file);
|
||||
|
||||
var etagValue = file.ModifiedOn.Ticks ^ file.Size;
|
||||
|
||||
bool isRequestingImageManipulation = false;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0)
|
||||
{
|
||||
isRequestingImageManipulation = true;
|
||||
etagValue ^= (width * 31);
|
||||
}
|
||||
if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0)
|
||||
{
|
||||
isRequestingImageManipulation = true;
|
||||
etagValue ^= (height * 17);
|
||||
}
|
||||
|
||||
Request.Query.TryGetValue("mode", out var mode);
|
||||
Request.Query.TryGetValue("position", out var position);
|
||||
Request.Query.TryGetValue("background", out var background);
|
||||
|
||||
if (width > 0 || height > 0)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode();
|
||||
if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode();
|
||||
if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode();
|
||||
}
|
||||
|
||||
int rotate;
|
||||
if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0)
|
||||
{
|
||||
isRequestingImageManipulation = true;
|
||||
etagValue ^= (rotate * 13);
|
||||
}
|
||||
|
||||
if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString()))
|
||||
{
|
||||
isRequestingImageManipulation = true;
|
||||
etagValue ^= format.ToString().GetHashCode();
|
||||
}
|
||||
|
||||
etag = Convert.ToString(etagValue, 16);
|
||||
|
||||
var header = "";
|
||||
if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch))
|
||||
{
|
||||
header = ifNoneMatch.ToString();
|
||||
}
|
||||
|
||||
if (header.Equals(etag))
|
||||
{
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
|
||||
return Content(String.Empty);
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(filepath))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
if (isRequestingImageManipulation)
|
||||
{
|
||||
var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue;
|
||||
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles;
|
||||
|
||||
if (!_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Is Not An Image {File}", file);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
Request.Query.TryGetValue("recreate", out var recreate);
|
||||
|
||||
if (!bool.TryParse(recreate, out _)) recreate = "false";
|
||||
if (!_imageService.GetAvailableFormats().Contains(format.ToString())) format = "png";
|
||||
if (width == 0 && height == 0)
|
||||
{
|
||||
width = file.ImageWidth;
|
||||
height = file.ImageHeight;
|
||||
}
|
||||
|
||||
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
|
||||
if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate))
|
||||
{
|
||||
// user has edit access to folder or folder supports the image size being created
|
||||
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) ||
|
||||
(!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString()))))
|
||||
{
|
||||
imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotateStr, format, imagepath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder {Folder} {Width} {Height}", file.Folder, width, height);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return BrokenFile();
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imagepath))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Displaying Image For File {File} {Width} {Height}", file, widthStr, heightStr);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
downloadName = file.Name.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
|
||||
filepath = imagepath;
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(filepath))
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return BrokenFile();
|
||||
}
|
||||
|
||||
if (download)
|
||||
{
|
||||
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
|
||||
return PhysicalFile(filepath, file.GetMimeType(), downloadName);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
|
||||
return PhysicalFile(filepath, file.GetMimeType());
|
||||
}
|
||||
}
|
||||
|
||||
private PhysicalFileResult BrokenFile()
|
||||
{
|
||||
// broken link
|
||||
string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png");
|
||||
return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath));
|
||||
|
@ -23,7 +23,7 @@ namespace Oqtane.Pages
|
||||
_syncManager = syncManager;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnurl)
|
||||
public async Task<IActionResult> OnPostAsync(string returnurl, string everywhere)
|
||||
{
|
||||
if (HttpContext.User != null)
|
||||
{
|
||||
@ -31,6 +31,10 @@ namespace Oqtane.Pages
|
||||
var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId);
|
||||
if (user != null)
|
||||
{
|
||||
if (everywhere == "true")
|
||||
{
|
||||
await _userManager.LogoutUserEverywhere(user);
|
||||
}
|
||||
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, SyncEventActions.Reload);
|
||||
}
|
||||
|
||||
|
@ -235,9 +235,9 @@ namespace Oqtane.Providers
|
||||
return text;
|
||||
}
|
||||
|
||||
public Task ResetIndex()
|
||||
public Task DeleteSearchContent(int siteId)
|
||||
{
|
||||
_searchContentRepository.DeleteAllSearchContent();
|
||||
_searchContentRepository.DeleteAllSearchContent(siteId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace Oqtane.Providers
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
return authState.User.SecurityStamp() == user.SecurityStamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace Oqtane.Repository
|
||||
void DeleteSearchContent(int searchContentId);
|
||||
void DeleteSearchContent(string entityName, string entryId);
|
||||
void DeleteSearchContent(string uniqueKey);
|
||||
void DeleteAllSearchContent();
|
||||
void DeleteAllSearchContent(int siteId);
|
||||
|
||||
SearchWord GetSearchWord(string word);
|
||||
SearchWord AddSearchWord(SearchWord searchWord);
|
||||
|
@ -91,18 +91,29 @@ namespace Oqtane.Repository
|
||||
public void DeletePage(int pageId)
|
||||
{
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
var page = db.Page.Find(pageId);
|
||||
_permissions.DeletePermissions(page.SiteId, EntityNames.Page, pageId);
|
||||
_settings.DeleteSettings(EntityNames.Page, pageId);
|
||||
// remove page modules for page
|
||||
var pageModules = db.PageModule.Where(item => item.PageId == pageId).ToList();
|
||||
foreach (var pageModule in pageModules)
|
||||
{
|
||||
_pageModules.DeletePageModule(pageModule.PageModuleId);
|
||||
var page = db.Page.Find(pageId);
|
||||
_permissions.DeletePermissions(page.SiteId, EntityNames.Page, pageId);
|
||||
_settings.DeleteSettings(EntityNames.Page, pageId);
|
||||
// remove page modules for page
|
||||
var pageModules = db.PageModule.Where(item => item.PageId == pageId).ToList();
|
||||
foreach (var pageModule in pageModules)
|
||||
{
|
||||
_pageModules.DeletePageModule(pageModule.PageModuleId);
|
||||
}
|
||||
|
||||
// At this point the page item is unaware of changes happened in other
|
||||
// contexts (i.e.: the contex opened and closed in each DeletePageModule).
|
||||
// Workin on page item may result in unxpected behaviour:
|
||||
// better close and reopen context to work on a fresh page item.
|
||||
}
|
||||
|
||||
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||
{
|
||||
var page = dbContext.Page.Find(pageId);
|
||||
dbContext.Page.Remove(page);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
// must occur after page modules are deleted because of cascading delete relationship
|
||||
db.Page.Remove(page);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,11 +152,17 @@ namespace Oqtane.Repository
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteAllSearchContent()
|
||||
public void DeleteAllSearchContent(int siteId)
|
||||
{
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
db.SearchContent.RemoveRange(db.SearchContent);
|
||||
db.SaveChanges();
|
||||
// delete in batches of 100 records
|
||||
var searchContents = db.SearchContent.Where(item => item.SiteId == siteId).Take(100).ToList();
|
||||
while (searchContents.Count > 0)
|
||||
{
|
||||
db.SearchContent.RemoveRange(searchContents);
|
||||
db.SaveChanges();
|
||||
searchContents = db.SearchContent.Where(item => item.SiteId == siteId).Take(100).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public SearchWord GetSearchWord(string word)
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
|
@ -441,7 +441,7 @@ namespace Oqtane.Repository
|
||||
pageModule.Module.PermissionList = new List<Permission>();
|
||||
foreach (var permission in pageTemplateModule.PermissionList)
|
||||
{
|
||||
pageModule.Module.PermissionList.Add(permission.Clone(permission));
|
||||
pageModule.Module.PermissionList.Add(permission.Clone());
|
||||
}
|
||||
pageModule.Module.AllPages = false;
|
||||
pageModule.Module.IsDeleted = false;
|
||||
|
@ -5,15 +5,11 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
using Oqtane.Themes;
|
||||
using System.Reflection.Metadata;
|
||||
using Oqtane.Migrations.Master;
|
||||
using Oqtane.Modules;
|
||||
|
||||
namespace Oqtane.Repository
|
||||
{
|
||||
|
@ -75,6 +75,7 @@ namespace Oqtane.Repository
|
||||
userrole.RoleId = role.RoleId;
|
||||
userrole.EffectiveDate = null;
|
||||
userrole.ExpiryDate = null;
|
||||
userrole.IgnoreSecurityStamp = true;
|
||||
_userroles.AddUserRole(userrole);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Infrastructure;
|
||||
@ -14,13 +15,15 @@ namespace Oqtane.Repository
|
||||
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
|
||||
private readonly IRoleRepository _roles;
|
||||
private readonly ITenantManager _tenantManager;
|
||||
private readonly UserManager<IdentityUser> _identityUserManager;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache)
|
||||
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, UserManager<IdentityUser> identityUserManager, IMemoryCache cache)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_roles = roles;
|
||||
_tenantManager = tenantManager;
|
||||
_identityUserManager = identityUserManager;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
@ -69,9 +72,12 @@ namespace Oqtane.Repository
|
||||
DeleteUserRoles(userRole.UserId);
|
||||
}
|
||||
|
||||
var alias = _tenantManager.GetAlias();
|
||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
||||
if (!userRole.IgnoreSecurityStamp)
|
||||
{
|
||||
UpdateSecurityStamp(userRole.UserId);
|
||||
}
|
||||
|
||||
RefreshCache(userRole.UserId);
|
||||
|
||||
return userRole;
|
||||
}
|
||||
@ -82,9 +88,12 @@ namespace Oqtane.Repository
|
||||
db.Entry(userRole).State = EntityState.Modified;
|
||||
db.SaveChanges();
|
||||
|
||||
var alias = _tenantManager.GetAlias();
|
||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
||||
if (!userRole.IgnoreSecurityStamp)
|
||||
{
|
||||
UpdateSecurityStamp(userRole.UserId);
|
||||
}
|
||||
|
||||
RefreshCache(userRole.UserId);
|
||||
|
||||
return userRole;
|
||||
}
|
||||
@ -144,9 +153,8 @@ namespace Oqtane.Repository
|
||||
db.UserRole.Remove(userRole);
|
||||
db.SaveChanges();
|
||||
|
||||
var alias = _tenantManager.GetAlias();
|
||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
||||
UpdateSecurityStamp(userRole.UserId);
|
||||
RefreshCache(userRole.UserId);
|
||||
}
|
||||
|
||||
public void DeleteUserRoles(int userId)
|
||||
@ -158,9 +166,32 @@ namespace Oqtane.Repository
|
||||
}
|
||||
db.SaveChanges();
|
||||
|
||||
UpdateSecurityStamp(userId);
|
||||
RefreshCache(userId);
|
||||
}
|
||||
|
||||
private void UpdateSecurityStamp(int userId)
|
||||
{
|
||||
using var db = _dbContextFactory.CreateDbContext();
|
||||
var user = db.User.Find(userId);
|
||||
if (user != null)
|
||||
{
|
||||
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
|
||||
if (identityuser != null)
|
||||
{
|
||||
_identityUserManager.UpdateSecurityStampAsync(identityuser).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCache(int userId)
|
||||
{
|
||||
var alias = _tenantManager.GetAlias();
|
||||
_cache.Remove($"user:{userId}:{alias.SiteKey}");
|
||||
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
|
||||
if (alias != null)
|
||||
{
|
||||
_cache.Remove($"user:{userId}:{alias.SiteKey}");
|
||||
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,14 +13,17 @@ namespace Oqtane.Security
|
||||
public class ClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : IdentityUser
|
||||
{
|
||||
private readonly ITenantManager _tenants;
|
||||
// cannot utilize IUserManager due to circular references - which is fine as this method is only called on login
|
||||
private readonly IUserRepository _users;
|
||||
private readonly IUserRoleRepository _userRoles;
|
||||
private readonly UserManager<TUser> _userManager;
|
||||
|
||||
public ClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor)
|
||||
{
|
||||
_tenants = tenants;
|
||||
_users = users;
|
||||
_userRoles = userroles;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser identityuser)
|
||||
@ -33,6 +36,7 @@ namespace Oqtane.Security
|
||||
Alias alias = _tenants.GetAlias();
|
||||
if (alias != null)
|
||||
{
|
||||
user.SecurityStamp = await _userManager.GetSecurityStampAsync(identityuser);
|
||||
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList();
|
||||
identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
||||
}
|
||||
|
@ -3,18 +3,17 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Models;
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Extensions;
|
||||
using Oqtane.Shared;
|
||||
using System.IO;
|
||||
using Oqtane.Managers;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Oqtane.Security
|
||||
{
|
||||
public static class PrincipalValidator
|
||||
{
|
||||
public static Task ValidateAsync(CookieValidatePrincipalContext context)
|
||||
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
|
||||
{
|
||||
if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null)
|
||||
{
|
||||
@ -24,60 +23,50 @@ namespace Oqtane.Security
|
||||
// check if framework is installed
|
||||
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
|
||||
{
|
||||
// get current site
|
||||
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
|
||||
|
||||
var alias = context.HttpContext.GetAlias();
|
||||
if (alias != null)
|
||||
{
|
||||
var claims = context.Principal.Claims;
|
||||
var userManager = context.HttpContext.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
|
||||
var user = userManager.GetUser(context.Principal.UserId(), alias.SiteId); // cached
|
||||
|
||||
// check if principal has roles and matches current site
|
||||
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
|
||||
// check if user is valid, not deleted, has roles, and security stamp has not changed
|
||||
if (user != null && !user.IsDeleted && !string.IsNullOrEmpty(user.Roles) && context.Principal.SecurityStamp() == user.SecurityStamp)
|
||||
{
|
||||
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
|
||||
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
|
||||
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
|
||||
|
||||
User user = userRepository.GetUser(context.Principal.Identity.Name);
|
||||
if (user != null)
|
||||
// validate sitekey in case user has changed sites in installation
|
||||
if (context.Principal.SiteKey() != alias.SiteKey || !context.Principal.Roles().Any())
|
||||
{
|
||||
// replace principal with roles for current site
|
||||
List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
|
||||
if (userroles.Any())
|
||||
{
|
||||
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
||||
context.ReplacePrincipal(new ClaimsPrincipal(identity));
|
||||
context.ShouldRenew = true;
|
||||
Log(_logger, alias, "Permissions Updated For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// user has no roles - remove principal
|
||||
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
context.RejectPrincipal();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// user does not exist - remove principal
|
||||
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
context.RejectPrincipal();
|
||||
// refresh principal
|
||||
var identity = UserSecurity.CreateClaimsIdentity(alias, user);
|
||||
context.ReplacePrincipal(new ClaimsPrincipal(identity));
|
||||
context.ShouldRenew = true;
|
||||
Log(_logger, alias, "Permissions Refreshed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// remove principal (ie. log user out)
|
||||
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
context.RejectPrincipal();
|
||||
await context.HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// user is signed in but tenant cannot be determined
|
||||
// user is signed in but site cannot be determined
|
||||
Log(_logger, alias, "Alias Could Not Be Resolved For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void Log (ILogManager logger, Alias alias, string message, string username, string path)
|
||||
{
|
||||
if (!path.StartsWith("/api/")) // reduce log verbosity
|
||||
{
|
||||
logger.Log(alias.SiteId, LogLevel.Information, "LoginValidation", Enums.LogFunction.Security, message, username, path);
|
||||
var siteId = (alias != null) ? alias.SiteId : -1;
|
||||
logger.Log(siteId, LogLevel.Information, "UserValidation", Enums.LogFunction.Security, message, username, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
124
Oqtane.Server/Services/ImageService.cs
Normal file
124
Oqtane.Server/Services/ImageService.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using Oqtane.Enums;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
using System;
|
||||
using SixLabors.ImageSharp;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Shared;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
using System.Linq;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
public class ImageService : IImageService
|
||||
{
|
||||
private readonly ILogManager _logger;
|
||||
private static readonly string[] _formats = ["png", "webp"];
|
||||
|
||||
public ImageService(ILogManager logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string[] GetAvailableFormats()
|
||||
{
|
||||
return _formats;
|
||||
}
|
||||
|
||||
public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string format, string imagepath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// params validation
|
||||
if (!Enum.TryParse(mode, true, out ResizeMode _)) mode = "crop";
|
||||
if (!Enum.TryParse(position, true, out AnchorPositionMode _)) position = "center";
|
||||
if (!Color.TryParseHex("#" + background, out _)) background = "transparent";
|
||||
if (!int.TryParse(rotate, out _)) rotate = "0";
|
||||
rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate;
|
||||
if (!_formats.Contains(format)) format = "png";
|
||||
|
||||
using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
stream.Position = 0;
|
||||
using (var image = Image.Load(stream))
|
||||
{
|
||||
int.TryParse(rotate, out int angle);
|
||||
Enum.TryParse(mode, true, out ResizeMode resizemode);
|
||||
Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode);
|
||||
|
||||
if (width == 0 && height == 0)
|
||||
{
|
||||
width = image.Width;
|
||||
height = image.Height;
|
||||
}
|
||||
|
||||
IImageEncoder encoder;
|
||||
var resizeOptions = new ResizeOptions
|
||||
{
|
||||
Mode = resizemode,
|
||||
Position = anchorpositionmode,
|
||||
Size = new Size(width, height)
|
||||
};
|
||||
|
||||
if (background != "transparent")
|
||||
{
|
||||
resizeOptions.PadColor = Color.ParseHex("#" + background);
|
||||
encoder = GetEncoder(format, transparent: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
encoder = GetEncoder(format, transparent: true);
|
||||
}
|
||||
|
||||
image.Mutate(x => x
|
||||
.AutoOrient() // auto orient the image
|
||||
.Rotate(angle)
|
||||
.Resize(resizeOptions));
|
||||
|
||||
image.Save(imagepath, encoder);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message);
|
||||
imagepath = "";
|
||||
}
|
||||
|
||||
return imagepath;
|
||||
}
|
||||
|
||||
private static IImageEncoder GetEncoder(string format, bool transparent)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
"png" => GetPngEncoder(transparent),
|
||||
"webp" => GetWebpEncoder(transparent),
|
||||
_ => GetPngEncoder(transparent),
|
||||
};
|
||||
}
|
||||
|
||||
private static PngEncoder GetPngEncoder(bool transparent)
|
||||
{
|
||||
return new PngEncoder()
|
||||
{
|
||||
ColorType = transparent ? PngColorType.RgbWithAlpha : PngColorType.Rgb,
|
||||
TransparentColorMode = transparent ? PngTransparentColorMode.Preserve : PngTransparentColorMode.Clear,
|
||||
BitDepth = PngBitDepth.Bit8,
|
||||
CompressionLevel = PngCompressionLevel.BestSpeed
|
||||
};
|
||||
}
|
||||
|
||||
private static WebpEncoder GetWebpEncoder(bool transparent)
|
||||
{
|
||||
return new WebpEncoder()
|
||||
{
|
||||
FileFormat = WebpFileFormatType.Lossy,
|
||||
Quality = 60,
|
||||
TransparentColorMode = transparent ? WebpTransparentColorMode.Preserve : WebpTransparentColorMode.Clear,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
35
Oqtane.Server/Services/LocalizationCookieService.cs
Normal file
35
Oqtane.Server/Services/LocalizationCookieService.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Oqtane.Documentation;
|
||||
|
||||
namespace Oqtane.Services
|
||||
{
|
||||
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
||||
public class ServerLocalizationCookieService : ILocalizationCookieService
|
||||
{
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
|
||||
public ServerLocalizationCookieService(IHttpContextAccessor accessor)
|
||||
{
|
||||
_accessor = accessor;
|
||||
}
|
||||
|
||||
public Task SetLocalizationCookieAsync(string culture)
|
||||
{
|
||||
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
|
||||
|
||||
_accessor.HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions
|
||||
{
|
||||
Path = "/",
|
||||
Expires = DateTimeOffset.UtcNow.AddYears(365),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Secure = true, // Ensure the cookie is only sent over HTTPS
|
||||
HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -149,6 +149,12 @@ namespace Oqtane.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task DeleteSearchContentsAsync(int siteId)
|
||||
{
|
||||
var searchProvider = GetSearchProvider(siteId);
|
||||
await searchProvider.DeleteSearchContent(siteId);
|
||||
}
|
||||
|
||||
private ISearchProvider GetSearchProvider(int siteId)
|
||||
{
|
||||
var providerName = GetSearchProviderSetting(siteId);
|
||||
|
@ -32,6 +32,7 @@ namespace Oqtane.Services
|
||||
private readonly ILogManager _logger;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly string _private = "[PRIVATE]";
|
||||
|
||||
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
|
||||
{
|
||||
@ -69,20 +70,35 @@ namespace Oqtane.Services
|
||||
return GetSite(siteId);
|
||||
});
|
||||
|
||||
// clone object so that cache is not mutated
|
||||
site = site.Clone();
|
||||
|
||||
// trim site settings based on user permissions
|
||||
site.Settings = site.Settings
|
||||
.Where(item => !item.Value.StartsWith(_private) || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
|
||||
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||
|
||||
// trim pages based on user permissions
|
||||
var pages = new List<Page>();
|
||||
foreach (Page page in site.Pages)
|
||||
{
|
||||
if (!page.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList)))
|
||||
{
|
||||
page.Settings = page.Settings
|
||||
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
|
||||
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||
pages.Add(page);
|
||||
}
|
||||
}
|
||||
|
||||
// clone object so that cache is not mutated
|
||||
site = site.Clone(site);
|
||||
site.Pages = pages;
|
||||
|
||||
// get language display name for user
|
||||
foreach (Language language in site.Languages)
|
||||
{
|
||||
language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName;
|
||||
}
|
||||
site.Languages = site.Languages.OrderBy(item => item.Name).ToList();
|
||||
|
||||
return Task.FromResult(site);
|
||||
}
|
||||
|
||||
@ -94,14 +110,13 @@ namespace Oqtane.Services
|
||||
{
|
||||
// site settings
|
||||
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
|
||||
.Where(item => !item.IsPrivate || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
|
||||
|
||||
// populate File Extensions
|
||||
// populate file extensions
|
||||
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
|
||||
? site.Settings["ImageFiles"] : Constants.ImageFiles;
|
||||
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
|
||||
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
|
||||
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
|
||||
|
||||
// pages
|
||||
List<Setting> settings = _settings.GetSettings(EntityNames.Page).ToList();
|
||||
@ -109,21 +124,23 @@ namespace Oqtane.Services
|
||||
foreach (Page page in _pages.GetPages(site.SiteId))
|
||||
{
|
||||
page.Settings = settings.Where(item => item.EntityId == page.PageId)
|
||||
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
|
||||
site.Pages.Add(page);
|
||||
}
|
||||
site.Pages = GetPagesHierarchy(site.Pages);
|
||||
|
||||
// framework modules
|
||||
var modules = GetModules(site.SiteId);
|
||||
var modules = GetPageModules(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());
|
||||
|
||||
// languages
|
||||
site.Languages = _languages.GetLanguages(site.SiteId).ToList();
|
||||
var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture);
|
||||
site.Languages.Add(new Language { Code = defaultCulture.Name, Name = defaultCulture.DisplayName, Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) });
|
||||
if (!site.Languages.Exists(item => item.Code == defaultCulture.Name))
|
||||
{
|
||||
site.Languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) });
|
||||
}
|
||||
|
||||
// themes
|
||||
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList());
|
||||
@ -249,31 +266,28 @@ namespace Oqtane.Services
|
||||
public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
|
||||
{
|
||||
var alias = _tenantManager.GetAlias();
|
||||
var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
||||
{
|
||||
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
||||
return GetModules(siteId);
|
||||
});
|
||||
|
||||
var modules = new List<Module>();
|
||||
foreach (Module module in sitemodules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
|
||||
{
|
||||
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
||||
{
|
||||
modules.Add(module);
|
||||
}
|
||||
}
|
||||
return Task.FromResult(modules);
|
||||
}
|
||||
|
||||
private List<Module> GetModules(int siteId)
|
||||
{
|
||||
var alias = _tenantManager.GetAlias();
|
||||
return _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
||||
var modules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
||||
{
|
||||
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
||||
return GetPageModules(siteId);
|
||||
});
|
||||
|
||||
// clone object so that cache is not mutated
|
||||
modules = modules.ConvertAll(module => module.Clone());
|
||||
|
||||
// trim modules for current page based on user permissions
|
||||
var pagemodules = new List<Module>();
|
||||
foreach (Module module in modules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
|
||||
{
|
||||
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
||||
{
|
||||
module.Settings = module.Settings
|
||||
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
||||
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||
pagemodules.Add(module);
|
||||
}
|
||||
}
|
||||
return Task.FromResult(pagemodules);
|
||||
}
|
||||
|
||||
private List<Module> GetPageModules(int siteId)
|
||||
@ -311,8 +325,7 @@ namespace Oqtane.Services
|
||||
ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)),
|
||||
|
||||
Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId)
|
||||
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, pagemodule.Module.PermissionList))
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue)
|
||||
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue)
|
||||
};
|
||||
|
||||
modules.Add(module);
|
||||
|
@ -159,7 +159,7 @@ namespace Oqtane
|
||||
}
|
||||
}).AddHubOptions(options =>
|
||||
{
|
||||
options.MaximumReceiveMessageSize = null; // no limit (for large amnounts of data ie. textarea components)
|
||||
options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components)
|
||||
})
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
|
@ -9,7 +9,7 @@ namespace [Owner].Module.[Module].Services
|
||||
{
|
||||
public class [Module]Service : ServiceBase, I[Module]Service
|
||||
{
|
||||
public [Module]Service(IHttpClientFactory http, SiteState siteState) : base(http, siteState) { }
|
||||
public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
|
||||
|
||||
private string Apiurl => CreateApiUrl("[Module]");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>[Owner]</Authors>
|
||||
<Company>[Owner]</Company>
|
||||
@ -13,12 +13,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<AccelerateBuildsInVisualStudio>false</AccelerateBuildsInVisualStudio>
|
||||
</PropertyGroup>
|
||||
|
@ -20,12 +20,12 @@
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="..\Client\bin\Release\net8.0\[Owner].Module.[Module].Client.Oqtane.dll" target="lib\net8.0" />
|
||||
<file src="..\Client\bin\Release\net8.0\[Owner].Module.[Module].Client.Oqtane.pdb" target="lib\net8.0" />
|
||||
<file src="..\Server\bin\Release\net8.0\[Owner].Module.[Module].Server.Oqtane.dll" target="lib\net8.0" />
|
||||
<file src="..\Server\bin\Release\net8.0\[Owner].Module.[Module].Server.Oqtane.pdb" target="lib\net8.0" />
|
||||
<file src="..\Shared\bin\Release\net8.0\[Owner].Module.[Module].Shared.Oqtane.dll" target="lib\net8.0" />
|
||||
<file src="..\Shared\bin\Release\net8.0\[Owner].Module.[Module].Shared.Oqtane.pdb" target="lib\net8.0" />
|
||||
<file src="..\Client\bin\Release\net9.0\[Owner].Module.[Module].Client.Oqtane.dll" target="lib\net9.0" />
|
||||
<file src="..\Client\bin\Release\net9.0\[Owner].Module.[Module].Client.Oqtane.pdb" target="lib\net9.0" />
|
||||
<file src="..\Server\bin\Release\net9.0\[Owner].Module.[Module].Server.Oqtane.dll" target="lib\net9.0" />
|
||||
<file src="..\Server\bin\Release\net9.0\[Owner].Module.[Module].Server.Oqtane.pdb" target="lib\net9.0" />
|
||||
<file src="..\Shared\bin\Release\net9.0\[Owner].Module.[Module].Shared.Oqtane.dll" target="lib\net9.0" />
|
||||
<file src="..\Shared\bin\Release\net9.0\[Owner].Module.[Module].Shared.Oqtane.pdb" target="lib\net9.0" />
|
||||
<file src="..\Server\wwwroot\**\*.*" target="wwwroot" />
|
||||
<file src="icon.png" target="" />
|
||||
</files>
|
||||
|
@ -1,7 +1,7 @@
|
||||
XCOPY "..\Client\bin\Debug\net8.0\[Owner].Module.[Module].Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net8.0\[Owner].Module.[Module].Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Server\bin\Debug\net8.0\[Owner].Module.[Module].Server.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Server\bin\Debug\net8.0\[Owner].Module.[Module].Server.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Shared\bin\Debug\net8.0\[Owner].Module.[Module].Shared.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Shared\bin\Debug\net8.0\[Owner].Module.[Module].Shared.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net9.0\[Owner].Module.[Module].Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net9.0\[Owner].Module.[Module].Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Server\bin\Debug\net9.0\[Owner].Module.[Module].Server.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Server\bin\Debug\net9.0\[Owner].Module.[Module].Server.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Shared\bin\Debug\net9.0\[Owner].Module.[Module].Shared.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Shared\bin\Debug\net9.0\[Owner].Module.[Module].Shared.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Server\wwwroot\*" "..\..\[RootFolder]\Oqtane.Server\wwwroot\" /Y /S /I
|
||||
|
@ -1,7 +1,7 @@
|
||||
cp -f "../Client/bin/Debug/net8.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Client/bin/Debug/net8.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Server/bin/Debug/net8.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Server/bin/Debug/net8.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Shared/bin/Debug/net8.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Shared/bin/Debug/net8.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Client/bin/Debug/net9.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Client/bin/Debug/net9.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Server/bin/Debug/net9.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Server/bin/Debug/net9.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Shared/bin/Debug/net9.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Shared/bin/Debug/net9.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<Version>1.0.0</Version>
|
||||
<Product>[Owner].Module.[Module]</Product>
|
||||
@ -19,10 +19,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
<Product>[Owner].Module.[Module]</Product>
|
||||
<Authors>[Owner]</Authors>
|
||||
|
@ -117,6 +117,10 @@
|
||||
margin: .5rem;
|
||||
}
|
||||
|
||||
.app-logo .navbar-brand {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.main .top-row {
|
||||
display: none;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>[Owner]</Authors>
|
||||
<Company>[Owner]</Company>
|
||||
@ -13,9 +13,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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.AspNetCore.Components.WebAssembly" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<AccelerateBuildsInVisualStudio>false</AccelerateBuildsInVisualStudio>
|
||||
</PropertyGroup>
|
||||
|
@ -20,8 +20,8 @@
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="..\Client\bin\Release\net8.0\[Owner].Theme.[Theme].Client.Oqtane.dll" target="lib\net8.0" />
|
||||
<file src="..\Client\bin\Release\net8.0\[Owner].Theme.[Theme].Client.Oqtane.pdb" target="lib\net8.0" />
|
||||
<file src="..\Client\bin\Release\net9.0\[Owner].Theme.[Theme].Client.Oqtane.dll" target="lib\net9.0" />
|
||||
<file src="..\Client\bin\Release\net9.0\[Owner].Theme.[Theme].Client.Oqtane.pdb" target="lib\net9.0" />
|
||||
<file src="..\Client\wwwroot\**\*.*" target="wwwroot" />
|
||||
<file src="icon.png" target="" />
|
||||
</files>
|
||||
|
@ -1,3 +1,3 @@
|
||||
XCOPY "..\Client\bin\Debug\net8.0\[Owner].Theme.[Theme].Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net8.0\[Owner].Theme.[Theme].Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net8.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net9.0\[Owner].Theme.[Theme].Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Client\bin\Debug\net9.0\[Owner].Theme.[Theme].Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\net9.0\" /Y
|
||||
XCOPY "..\Client\wwwroot\*" "..\..\[RootFolder]\Oqtane.Server\wwwroot\" /Y /S /I
|
||||
|
@ -1,3 +1,3 @@
|
||||
cp -f "../Client/bin/Debug/net8.0/[Owner].Theme.[Theme].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Client/bin/Debug/net8.0/[Owner].Theme.[Theme].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net8.0/"
|
||||
cp -f "../Client/bin/Debug/net9.0/[Owner].Theme.[Theme].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -f "../Client/bin/Debug/net9.0/[Owner].Theme.[Theme].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net9.0/"
|
||||
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"
|
||||
|
@ -267,4 +267,8 @@ app {
|
||||
.text-area-editor > textarea {
|
||||
width: 100%;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.app-logo .navbar-brand {
|
||||
padding: 5px 20px 5px 20px;
|
||||
}
|
@ -1,11 +1,18 @@
|
||||
var Oqtane = Oqtane || {};
|
||||
|
||||
Oqtane.Interop = {
|
||||
setCookie: function (name, value, days) {
|
||||
setCookie: function (name, value, days, secure, sameSite) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toUTCString();
|
||||
document.cookie = name + "=" + value + ";" + expires + ";path=/";
|
||||
var cookieString = name + "=" + value + ";" + expires + ";path=/";
|
||||
if (secure) {
|
||||
cookieString += "; secure";
|
||||
}
|
||||
if (sameSite === "Lax" || sameSite === "Strict" || sameSite === "None") {
|
||||
cookieString += "; SameSite=" + sameSite;
|
||||
}
|
||||
document.cookie = cookieString;
|
||||
},
|
||||
getCookie: function (name) {
|
||||
name = name + "=";
|
||||
@ -198,7 +205,9 @@ Oqtane.Interop = {
|
||||
}
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
if (loadjs.isDefined(bundles[b])) {
|
||||
resolve(true);
|
||||
loadjs.ready(bundles[b], () => {
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
else {
|
||||
loadjs(urls, bundles[b], {
|
||||
@ -293,41 +302,49 @@ Oqtane.Interop = {
|
||||
},
|
||||
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
|
||||
var fileinput = document.getElementById('FileInput_' + id);
|
||||
var files = fileinput.files;
|
||||
var progressinfo = document.getElementById('ProgressInfo_' + id);
|
||||
var progressbar = document.getElementById('ProgressBar_' + id);
|
||||
|
||||
if (progressinfo !== null && progressbar !== null) {
|
||||
progressinfo.setAttribute("style", "display: inline;");
|
||||
progressinfo.innerHTML = '';
|
||||
progressbar.setAttribute("style", "width: 100%; display: inline;");
|
||||
progressbar.value = 0;
|
||||
}
|
||||
|
||||
var files = fileinput.files;
|
||||
var totalSize = 0;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var FileChunk = [];
|
||||
var file = files[i];
|
||||
var MaxFileSizeMB = 1;
|
||||
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
|
||||
var FileStreamPos = 0;
|
||||
var EndPos = BufferChunkSize;
|
||||
var Size = file.size;
|
||||
totalSize = totalSize + files[i].size;
|
||||
}
|
||||
|
||||
while (FileStreamPos < Size) {
|
||||
FileChunk.push(file.slice(FileStreamPos, EndPos));
|
||||
FileStreamPos = EndPos;
|
||||
EndPos = FileStreamPos + BufferChunkSize;
|
||||
var maxChunkSizeMB = 1;
|
||||
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
|
||||
var uploadedSize = 0;
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var fileChunk = [];
|
||||
var file = files[i];
|
||||
var fileStreamPos = 0;
|
||||
var endPos = bufferChunkSize;
|
||||
|
||||
while (fileStreamPos < file.size) {
|
||||
fileChunk.push(file.slice(fileStreamPos, endPos));
|
||||
fileStreamPos = endPos;
|
||||
endPos = fileStreamPos + bufferChunkSize;
|
||||
}
|
||||
|
||||
var TotalParts = FileChunk.length;
|
||||
var PartCount = 0;
|
||||
var totalParts = fileChunk.length;
|
||||
var partCount = 0;
|
||||
|
||||
while (Chunk = FileChunk.shift()) {
|
||||
PartCount++;
|
||||
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0');
|
||||
while (chunk = fileChunk.shift()) {
|
||||
partCount++;
|
||||
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
|
||||
|
||||
var data = new FormData();
|
||||
data.append('__RequestVerificationToken', antiforgerytoken);
|
||||
data.append('folder', folder);
|
||||
data.append('formfile', Chunk, FileName);
|
||||
data.append('formfile', chunk, fileName);
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', posturl, true);
|
||||
if (jwt !== "") {
|
||||
@ -335,28 +352,36 @@ Oqtane.Interop = {
|
||||
request.withCredentials = true;
|
||||
}
|
||||
request.upload.onloadstart = function (e) {
|
||||
if (progressinfo !== null && progressbar !== null) {
|
||||
progressinfo.innerHTML = file.name + ' 0%';
|
||||
progressbar.value = 0;
|
||||
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
|
||||
if (files.length === 1) {
|
||||
progressinfo.innerHTML = file.name;
|
||||
}
|
||||
else {
|
||||
progressinfo.innerHTML = file.name + ", ...";
|
||||
}
|
||||
}
|
||||
};
|
||||
request.upload.onprogress = function (e) {
|
||||
if (progressinfo !== null && progressbar !== null) {
|
||||
var percent = Math.ceil((e.loaded / e.total) * 100);
|
||||
progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
|
||||
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
|
||||
progressbar.value = (percent / 100);
|
||||
}
|
||||
};
|
||||
request.upload.onloadend = function (e) {
|
||||
if (progressinfo !== null && progressbar !== null) {
|
||||
progressinfo.innerHTML = file.name + ' 100%';
|
||||
progressbar.value = 1;
|
||||
uploadedSize = uploadedSize + e.total;
|
||||
var percent = Math.ceil((uploadedSize / totalSize) * 100);
|
||||
progressbar.value = (percent / 100);
|
||||
}
|
||||
};
|
||||
request.upload.onerror = function() {
|
||||
if (progressinfo !== null && progressbar !== null) {
|
||||
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
||||
progressbar.value = 0;
|
||||
if (files.length === 1) {
|
||||
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
||||
}
|
||||
else {
|
||||
progressinfo.innerHTML = ' Error: ' + request.statusText;
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send(data);
|
||||
@ -392,11 +417,20 @@ Oqtane.Interop = {
|
||||
}
|
||||
},
|
||||
scrollTo: function (top, left, behavior) {
|
||||
window.scrollTo({
|
||||
top: top,
|
||||
left: left,
|
||||
behavior: behavior
|
||||
});
|
||||
const modal = document.querySelector('.modal');
|
||||
if (modal) {
|
||||
modal.scrollTo({
|
||||
top: top,
|
||||
left: left,
|
||||
behavior: behavior
|
||||
});
|
||||
} else {
|
||||
window.scrollTo({
|
||||
top: top,
|
||||
left: left,
|
||||
behavior: behavior
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToId: function (id) {
|
||||
var element = document.getElementById(id);
|
||||
|
Reference in New Issue
Block a user