Implements Image Manipulation in Files Page via QueryString

- Extracts the image creation into a service
- Refactors Files Page GET action for better readability and cyclomatic complexity
This commit is contained in:
David Montesinos 2024-10-13 12:38:43 +02:00
parent 1047058676
commit aa5b84a214
5 changed files with 296 additions and 171 deletions

View File

@ -17,11 +17,10 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Extensions; using Oqtane.Extensions;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Png;
using System.Net.Http; using System.Net.Http;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using System.IO.Compression; using System.IO.Compression;
using Oqtane.Services;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@ -38,7 +37,9 @@ namespace Oqtane.Controllers
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly ISettingRepository _settingRepository; 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; _environment = environment;
_files = files; _files = files;
@ -48,6 +49,7 @@ namespace Oqtane.Controllers
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_settingRepository = settingRepository; _settingRepository = settingRepository;
_imageService = imageService;
} }
// GET: api/<controller>?folder=x // GET: api/<controller>?folder=x
@ -681,12 +683,6 @@ namespace Oqtane.Controllers
var filepath = _files.GetFilePath(file); var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath)) 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"; if (!bool.TryParse(recreate, out _)) recreate = "false";
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png");
@ -696,7 +692,7 @@ namespace Oqtane.Controllers
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || 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())))) (!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, imagepath);
} }
else else
{ {
@ -743,70 +739,6 @@ namespace Oqtane.Controllers
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; 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) private string GetFolderPath(string folder)
{ {
return Utilities.PathCombine(_environment.ContentRootPath, folder); return Utilities.PathCombine(_environment.ContentRootPath, folder);

View File

@ -102,6 +102,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISearchResultsService, SearchResultsService>(); services.AddScoped<ISearchResultsService, SearchResultsService>();
services.AddScoped<ISearchService, SearchService>(); services.AddScoped<ISearchService, SearchService>();
services.AddScoped<ISearchProvider, DatabaseSearchProvider>(); services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
services.AddScoped<IImageService, ImageService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -14,6 +15,7 @@ using Oqtane.Infrastructure;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Pages namespace Oqtane.Pages
@ -28,8 +30,10 @@ namespace Oqtane.Pages
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; 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; _environment = environment;
_files = files; _files = files;
@ -38,12 +42,19 @@ namespace Oqtane.Pages
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_imageService = imageService;
_settingRepository = settingRepository;
} }
public IActionResult OnGet(string path) public IActionResult OnGet(string path)
{ {
if (!string.IsNullOrEmpty(path)) 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("\\", "/"); path = path.Replace("\\", "/");
var folderpath = ""; var folderpath = "";
var filename = ""; var filename = "";
@ -74,56 +85,10 @@ namespace Oqtane.Pages
file = _files.GetFile(_alias.SiteId, folderpath, filename); file = _files.GetFile(_alias.SiteId, folderpath, filename);
} }
if (file != null) 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 // look for url mapping
var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename); var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{ {
@ -133,16 +98,137 @@ namespace Oqtane.Pages
var uri = new Uri(HttpContext.Request.GetEncodedUrl()); var uri = new Uri(HttpContext.Request.GetEncodedUrl());
url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url; 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))
{
url += Request.QueryString.Value;
}
return RedirectPermanent(url); 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);
bool hasWidthParam = Request.Query.TryGetValue("width", out var widthStr);
bool hasHeightParam = Request.Query.TryGetValue("height", out var heightStr);
int width = 0;
int height = 0;
bool isRequestingImageResize =
hasWidthParam && int.TryParse(widthStr, out width) && width > 0 &&
hasHeightParam && int.TryParse(heightStr, out height) && height > 0;
if (isRequestingImageResize)
{
etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size ^ (width * 31) ^ (height * 17), 16);
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId); etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
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 (isRequestingImageResize)
{
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("mode", out var mode);
Request.Query.TryGetValue("position", out var position);
Request.Query.TryGetValue("background", out var background);
Request.Query.TryGetValue("rotate", out var rotate);
Request.Query.TryGetValue("recreate", out var recreate);
if (!bool.TryParse(recreate, out _)) recreate = "false";
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png");
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, rotate, 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() + ".png");
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 // broken link
string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png"); string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png");
return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)); return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath));

View File

@ -0,0 +1,93 @@
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.Interfaces;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class ImageService : IImageService
{
private readonly ILogManager _logger;
public ImageService(ILogManager logger)
{
_logger = logger;
}
public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, 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;
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;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Oqtane.Services
{
public interface IImageService
{
public string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath);
}
}