From 0b86413e6045be10f5f0c4986672547b6d5d001f Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 18 Apr 2024 21:37:48 +0200 Subject: [PATCH 1/4] 6748: Stricter file and folder name validation (#6792) * Media Library: More strict file and folder name validation, fixes #6748 * Resetting MediaLibraryService changes to 1.10.x * Code styling in FileSystemStorageProvider * Adding string file and folder name validation to FileSystemStorageProvider, so that MediaLibrary components don't need to do it separately * Applying the same file and folder name validation to AzureFileSystem too * Code styling and fixes in AzureFileSystem, MediaLibrary and IStorageProvider * Simplifying invalid character detection * Code styling * Adding InvalidNameCharacterException to be able to handle invalid characters precisely at various user-facing components * Updating MediaLibrary not to log an error when a file can't be uploaded due to invalid characters --------- Co-authored-by: Lombiq --- .../Services/FileSystems/AzureFileSystem.cs | 43 +++++++++---- .../Controllers/ClientStorageController.cs | 30 +++++---- .../Controllers/FolderController.cs | 44 ++++++------- .../MediaFileName/MediaFileNameDriver.cs | 16 +++-- .../Services/MediaLibraryService.cs | 21 +++---- .../Exceptions/DefaultExceptionPolicy.cs | 4 +- src/Orchard/Exceptions/ExceptionExtensions.cs | 9 +-- .../Media/FileSystemStorageProvider.cs | 61 ++++++++++++++----- .../FileSystems/Media/IStorageProvider.cs | 2 +- .../Media/InvalidNameCharacterException.cs | 7 +++ .../Media/StorageProviderExtensions.cs | 4 -- src/Orchard/Orchard.Framework.csproj | 2 +- 12 files changed, 150 insertions(+), 93 deletions(-) create mode 100644 src/Orchard/FileSystems/Media/InvalidNameCharacterException.cs diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs b/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs index a0470c2b8..314b984d4 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs +++ b/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs @@ -89,6 +89,8 @@ namespace Orchard.Azure.Services.FileSystems { return newPath; } + private static string GetFolderName(string path) => path.Substring(path.LastIndexOf('/') + 1); + public string Combine(string path1, string path2) { if (path1 == null) { throw new ArgumentNullException("path1"); @@ -141,10 +143,10 @@ namespace Orchard.Azure.Services.FileSystems { } return BlobClient.ListBlobs(prefix) - .OfType() - .Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry)) - .Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot)) - .ToArray(); + .OfType() + .Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry)) + .Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot)) + .ToArray(); } public IEnumerable ListFolders(string path) { @@ -194,6 +196,11 @@ namespace Orchard.Azure.Services.FileSystems { public void CreateFolder(string path) { path = ConvertToRelativeUriPath(path); + + if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(path))) { + throw new InvalidNameCharacterException("The directory name contains invalid character(s)"); + } + Container.EnsureDirectoryDoesNotExist(String.Concat(_root, path)); // Creating a virtually hidden file to make the directory an existing concept @@ -225,6 +232,10 @@ namespace Orchard.Azure.Services.FileSystems { path = ConvertToRelativeUriPath(path); newPath = ConvertToRelativeUriPath(newPath); + if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(newPath))) { + throw new InvalidNameCharacterException("The new directory name contains invalid character(s)"); + } + if (!path.EndsWith("/")) path += "/"; @@ -260,6 +271,10 @@ namespace Orchard.Azure.Services.FileSystems { path = ConvertToRelativeUriPath(path); newPath = ConvertToRelativeUriPath(newPath); + if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(newPath))) { + throw new InvalidNameCharacterException("The new file name contains invalid character(s)"); + } + Container.EnsureBlobExists(String.Concat(_root, path)); Container.EnsureBlobDoesNotExist(String.Concat(_root, newPath)); @@ -284,6 +299,10 @@ namespace Orchard.Azure.Services.FileSystems { public IStorageFile CreateFile(string path) { path = ConvertToRelativeUriPath(path); + if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(path))) { + throw new InvalidNameCharacterException("The file name contains invalid character(s)"); + } + if (Container.BlobExists(String.Concat(_root, path))) { throw new ArgumentException("File " + path + " already exists"); } @@ -371,10 +390,7 @@ namespace Orchard.Azure.Services.FileSystems { _rootPath = rootPath; } - public string GetName() { - var path = GetPath(); - return path.Substring(path.LastIndexOf('/') + 1); - } + public string GetName() => GetFolderName(GetPath()); public string GetPath() { return _blob.Uri.ToString().Substring(_rootPath.Length).Trim('/'); @@ -399,11 +415,12 @@ namespace Orchard.Azure.Services.FileSystems { long size = 0; foreach (var blobItem in directoryBlob.ListBlobs()) { - if (blobItem is CloudBlockBlob) - size += ((CloudBlockBlob)blobItem).Properties.Length; - - if (blobItem is CloudBlobDirectory) - size += GetDirectorySize((CloudBlobDirectory)blobItem); + if (blobItem is CloudBlockBlob blob) { + size += blob.Properties.Length; + } + else if (blobItem is CloudBlobDirectory directory) { + size += GetDirectorySize(directory); + } } return size; diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs index 8130d5e51..49c354323 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs @@ -3,15 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Web.Mvc; using Orchard.ContentManagement; +using Orchard.FileSystems.Media; +using Orchard.Localization; +using Orchard.Logging; +using Orchard.MediaLibrary.Models; using Orchard.MediaLibrary.Services; using Orchard.MediaLibrary.ViewModels; using Orchard.Themes; using Orchard.UI.Admin; -using Orchard.MediaLibrary.Models; -using Orchard.Localization; -using System.Linq; -using Orchard.FileSystems.Media; -using Orchard.Logging; namespace Orchard.MediaLibrary.Controllers { [Admin, Themed(false)] @@ -107,10 +106,16 @@ namespace Orchard.MediaLibrary.Controllers { url = mediaPart.FileName, }); } - catch (Exception ex) { - Logger.Error(ex, "Unexpected exception when uploading a media."); + catch (InvalidNameCharacterException) { statuses.Add(new { - error = T(ex.Message).Text, + error = T("The file name contains invalid character(s)").Text, + progress = 1.0, + }); + } + catch (Exception ex) { + Logger.Error(ex, T("Unexpected exception when uploading a media.").Text); + statuses.Add(new { + error = ex.Message, progress = 1.0, }); } @@ -130,7 +135,7 @@ namespace Orchard.MediaLibrary.Controllers { return HttpNotFound(); // Check permission - if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath)) + if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath)) && !_mediaLibraryService.CanManageMediaFolder(replaceMedia.FolderPath)) { return new HttpUnauthorizedResult(); } @@ -138,7 +143,7 @@ namespace Orchard.MediaLibrary.Controllers { var statuses = new List(); var settings = Services.WorkContext.CurrentSite.As(); - + // Loop through each file in the request for (int i = 0; i < HttpContext.Request.Files.Count; i++) { // Pointer to file @@ -146,7 +151,8 @@ namespace Orchard.MediaLibrary.Controllers { var filename = Path.GetFileName(file.FileName); // if the file has been pasted, provide a default name - if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase) && !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) { + if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase) + && !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) { filename = "clipboard.png"; } @@ -184,7 +190,7 @@ namespace Orchard.MediaLibrary.Controllers { }); } catch (Exception ex) { - Logger.Error(ex, "Unexpected exception when uploading a media."); + Logger.Error(ex, T("Unexpected exception when uploading a media.").Text); statuses.Add(new { error = T(ex.Message).Text, diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/FolderController.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/FolderController.cs index b79ede9e5..9b567b1c5 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/FolderController.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/FolderController.cs @@ -1,9 +1,9 @@ using System; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Web.Mvc; using Orchard.ContentManagement; +using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.Logging; using Orchard.MediaLibrary.Models; @@ -36,7 +36,7 @@ namespace Orchard.MediaLibrary.Controllers { public ActionResult Create(string folderPath) { if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) { Services.Notifier.Error(T("Couldn't create media folder")); - return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath }); + return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath }); } // If the user is trying to access a folder above his boundaries, redirect him to his home folder @@ -68,28 +68,32 @@ namespace Orchard.MediaLibrary.Controllers { return new HttpUnauthorizedResult(); } + var failed = false; try { - bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$"); - if (!valid) { - throw new ArgumentException(T("Folder contains invalid characters").ToString()); - } - else { - _mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name); - Services.Notifier.Information(T("Media folder created")); - } + _mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name); + Services.Notifier.Information(T("Media folder created")); + } + catch (InvalidNameCharacterException) { + Services.Notifier.Error(T("The folder name contains invalid character(s).")); + failed = true; } catch (ArgumentException argumentException) { Services.Notifier.Error(T("Creating Folder failed: {0}", argumentException.Message)); + failed = true; + } + + if (failed) { Services.TransactionManager.Cancel(); return View(viewModel); } + return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary" }); } public ActionResult Edit(string folderPath) { if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) { Services.Notifier.Error(T("Couldn't edit media folder")); - return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath }); + return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath }); } if (!_mediaLibraryService.CanManageMediaFolder(folderPath)) { @@ -125,7 +129,7 @@ namespace Orchard.MediaLibrary.Controllers { var viewModel = new MediaManagerFolderEditViewModel(); UpdateModel(viewModel); - if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath) + if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, viewModel.FolderPath))) { return new HttpUnauthorizedResult(); } @@ -136,14 +140,12 @@ namespace Orchard.MediaLibrary.Controllers { } try { - bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$"); - if (!valid) { - throw new ArgumentException(T("Folder contains invalid characters").ToString()); - } - else { - _mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name); - Services.Notifier.Information(T("Media folder renamed")); - } + _mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name); + Services.Notifier.Information(T("Media folder renamed")); + } + catch (InvalidNameCharacterException) { + Services.Notifier.Error(T("The folder name contains invalid character(s).")); + return View(viewModel); } catch (Exception exception) { Services.Notifier.Error(T("Editing Folder failed: {0}", exception.Message)); @@ -198,7 +200,7 @@ namespace Orchard.MediaLibrary.Controllers { // don't try to rename the file if there is no associated media file if (!string.IsNullOrEmpty(media.FileName)) { // check permission on source folder - if(!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) { + if (!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) { return new HttpUnauthorizedResult(); } diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/MediaFileName/MediaFileNameDriver.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/MediaFileName/MediaFileNameDriver.cs index f9e58b547..f165d5955 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/MediaFileName/MediaFileNameDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/MediaFileName/MediaFileNameDriver.cs @@ -1,14 +1,14 @@ using System; using Orchard.ContentManagement; using Orchard.ContentManagement.Drivers; +using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.MediaLibrary.Models; using Orchard.MediaLibrary.Services; using Orchard.Security; using Orchard.UI.Notify; -namespace Orchard.MediaLibrary.MediaFileName -{ +namespace Orchard.MediaLibrary.MediaFileName { public class MediaFileNameDriver : ContentPartDriver { private readonly IAuthenticationService _authenticationService; private readonly IAuthorizationService _authorizationService; @@ -58,6 +58,8 @@ namespace Orchard.MediaLibrary.MediaFileName var priorFileName = model.FileName; if (updater.TryUpdateModel(model, Prefix, null, null)) { if (model.FileName != null && !model.FileName.Equals(priorFileName, StringComparison.OrdinalIgnoreCase)) { + var fieldName = "MediaFileNameEditorSettings.FileName"; + try { _mediaLibraryService.RenameFile(part.FolderPath, priorFileName, model.FileName); part.FileName = model.FileName; @@ -65,14 +67,18 @@ namespace Orchard.MediaLibrary.MediaFileName _notifier.Add(NotifyType.Information, T("File '{0}' was renamed to '{1}'", priorFileName, model.FileName)); } catch (OrchardException) { - updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file. Invalid Windows file path.")); + updater.AddModelError(fieldName, T("Unable to rename file. Invalid Windows file path.")); } - catch (Exception) { - updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file")); + catch (InvalidNameCharacterException) { + updater.AddModelError(fieldName, T("The file name contains invalid character(s).")); + } + catch (Exception exception) { + updater.AddModelError(fieldName, T("Unable to rename file: {0}", exception.Message)); } } } } + return model; }); } diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs index 349469205..abadaff3a 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs @@ -6,13 +6,13 @@ using System.Web; using Orchard.ContentManagement; using Orchard.ContentManagement.MetaData.Models; using Orchard.Core.Common.Models; +using Orchard.Core.Title.Models; using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.MediaLibrary.Factories; using Orchard.MediaLibrary.Models; -using Orchard.Core.Title.Models; -using Orchard.Validation; using Orchard.MediaLibrary.Providers; +using Orchard.Validation; namespace Orchard.MediaLibrary.Services { public class MediaLibraryService : IMediaLibraryService { @@ -21,7 +21,6 @@ namespace Orchard.MediaLibrary.Services { private readonly IStorageProvider _storageProvider; private readonly IEnumerable _mediaFactorySelectors; private readonly IMediaFolderProvider _mediaFolderProvider; - private static char[] HttpUnallowed = new char[] { '<', '>', '*', '%', '&', ':', '\\', '?', '#' }; public MediaLibraryService( IOrchardServices orchardServices, @@ -145,12 +144,6 @@ namespace Orchard.MediaLibrary.Services { } public string GetUniqueFilename(string folderPath, string filename) { - - // remove any char which is unallowed in an HTTP request - foreach (var unallowedChar in HttpUnallowed) { - filename = filename.Replace(unallowedChar.ToString(), ""); - } - // compute a unique filename var uniqueFilename = filename; var index = 1; @@ -177,9 +170,9 @@ namespace Orchard.MediaLibrary.Services { var mediaFile = BuildMediaFile(relativePath, storageFile); using (var stream = storageFile.OpenRead()) { - var mediaFactory = GetMediaFactory(stream, mimeType, contentType); - if (mediaFactory == null) - throw new Exception(T("No media factory available to handle this resource.").Text); + var mediaFactory = GetMediaFactory(stream, mimeType, contentType) + ?? throw new Exception(T("No media factory available to handle this resource.").Text); + var mediaPart = mediaFactory.CreateMedia(stream, mediaFile.Name, mimeType, contentType); if (mediaPart != null) { mediaPart.FolderPath = relativePath; @@ -256,7 +249,7 @@ namespace Orchard.MediaLibrary.Services { if (_orchardServices.Authorizer.Authorize(Permissions.ManageMediaContent)) { return true; } - if (_orchardServices.WorkContext.CurrentUser==null) + if (_orchardServices.WorkContext.CurrentUser == null) return _orchardServices.Authorizer.Authorize(permission); // determines the folder type: public, user own folder (my), folder of another user (private) var rootedFolderPath = this.GetRootedFolderPath(folderPath) ?? ""; @@ -268,7 +261,7 @@ namespace Orchard.MediaLibrary.Services { isMyfolder = true; } - if(isMyfolder) { + if (isMyfolder) { return _orchardServices.Authorizer.Authorize(Permissions.ManageOwnMedia); } else { // other diff --git a/src/Orchard/Exceptions/DefaultExceptionPolicy.cs b/src/Orchard/Exceptions/DefaultExceptionPolicy.cs index db6de3559..08e7466e7 100644 --- a/src/Orchard/Exceptions/DefaultExceptionPolicy.cs +++ b/src/Orchard/Exceptions/DefaultExceptionPolicy.cs @@ -34,7 +34,7 @@ namespace Orchard.Exceptions { return false; } - if (sender is IEventBus && exception is OrchardFatalException) { + if (sender is IEventBus && exception is OrchardFatalException) { return false; } @@ -49,7 +49,7 @@ namespace Orchard.Exceptions { } private static bool IsFatal(Exception exception) { - return + return exception is OrchardSecurityException || exception is StackOverflowException || exception is AccessViolationException || diff --git a/src/Orchard/Exceptions/ExceptionExtensions.cs b/src/Orchard/Exceptions/ExceptionExtensions.cs index a66ba1a8b..534c77406 100644 --- a/src/Orchard/Exceptions/ExceptionExtensions.cs +++ b/src/Orchard/Exceptions/ExceptionExtensions.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Orchard.Security; -using System.Threading; -using System.Security; using System.Runtime.InteropServices; +using System.Security; +using System.Threading; +using Orchard.Security; namespace Orchard.Exceptions { public static class ExceptionExtensions { diff --git a/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs b/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs index ab0abfd26..34a7b5055 100644 --- a/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs @@ -4,15 +4,22 @@ using System.IO; using System.Linq; using System.Web.Hosting; using Orchard.Environment.Configuration; -using Orchard.Localization; -using Orchard.Validation; using Orchard.Exceptions; +using Orchard.Localization; +using Orchard.Utility.Extensions; +using Orchard.Validation; namespace Orchard.FileSystems.Media { public class FileSystemStorageProvider : IStorageProvider { private readonly string _storagePath; // c:\orchard\media\default private readonly string _virtualPath; // ~/Media/Default/ private readonly string _publicPath; // /Orchard/Media/Default/ + public static readonly char[] HttpUnallowedCharacters = + new char[] { '<', '>', '*', '%', '&', ':', '\\', '/', '?', '#', '"', '{', '}', '|', '^', '[', ']', '`' }; + public static readonly char[] InvalidFolderNameCharacters = + Path.GetInvalidPathChars().Union(HttpUnallowedCharacters).ToArray(); + public static readonly char[] InvalidFileNameCharacters = + Path.GetInvalidFileNameChars().Union(HttpUnallowedCharacters).ToArray(); public FileSystemStorageProvider(ShellSettings settings) { var mediaPath = HostingEnvironment.IsHosted @@ -27,7 +34,7 @@ namespace Orchard.FileSystems.Media { appPath = HostingEnvironment.ApplicationVirtualPath; } if (!appPath.EndsWith("/")) - appPath = appPath + '/'; + appPath += '/'; if (!appPath.StartsWith("/")) appPath = '/' + appPath; @@ -39,21 +46,21 @@ namespace Orchard.FileSystems.Media { public Localizer T { get; set; } - public int MaxPathLength { - get; set; - // The public setter allows injecting this from Sites.MyTenant.Config or Sites.config, by using - // an AutoFac component: - /* - + /// + /// The public setter allows injecting this from Sites.MyTenant.Config or Sites.config, by using an AutoFac + /// component. See the example below. + /// + /* + - - */ - } + */ + public int MaxPathLength { get; set; } /// /// Maps a relative path into the storage path. @@ -215,6 +222,12 @@ namespace Orchard.FileSystems.Media { /// The relative path to the folder to be created. /// If the folder already exists. public void CreateFolder(string path) { + // We are dealing with a folder here, but GetFileName returns the last path segment, which in this case is + // the folder name. + if (FolderNameContainsInvalidCharacters(Path.GetFileName(path))) { + throw new InvalidNameCharacterException(T("The directory name contains invalid character(s)").ToString()); + } + DirectoryInfo directoryInfo = new DirectoryInfo(MapStorage(path)); if (directoryInfo.Exists) { throw new ArgumentException(T("Directory {0} already exists", path).ToString()); @@ -248,6 +261,12 @@ namespace Orchard.FileSystems.Media { throw new ArgumentException(T("Directory {0} does not exist", oldPath).ToString()); } + // We are dealing with a folder here, but GetFileName returns the last path segment, which in this case is + // the folder name. + if (FolderNameContainsInvalidCharacters(Path.GetFileName(newPath))) { + throw new InvalidNameCharacterException(T("The new directory name contains invalid character(s)").ToString()); + } + DirectoryInfo targetDirectory = new DirectoryInfo(MapStorage(newPath)); if (targetDirectory.Exists) { throw new ArgumentException(T("Directory {0} already exists", newPath).ToString()); @@ -313,6 +332,10 @@ namespace Orchard.FileSystems.Media { throw new ArgumentException(T("File {0} does not exist", oldPath).ToString()); } + if (FileNameContainsInvalidCharacters(Path.GetFileName(newPath))) { + throw new InvalidNameCharacterException(T("The new file name contains invalid character(s)").ToString()); + } + FileInfo targetFileInfo = new FileInfo(MapStorage(newPath)); if (targetFileInfo.Exists) { throw new ArgumentException(T("File {0} already exists", newPath).ToString()); @@ -342,6 +365,10 @@ namespace Orchard.FileSystems.Media { /// If the file already exists. /// The created file. public IStorageFile CreateFile(string path) { + if (FileNameContainsInvalidCharacters(Path.GetFileName(path))) { + throw new InvalidNameCharacterException(T("The file name contains invalid character(s)").ToString()); + } + FileInfo fileInfo = new FileInfo(MapStorage(path)); if (fileInfo.Exists) { throw new ArgumentException(T("File {0} already exists", fileInfo.Name).ToString()); @@ -427,6 +454,12 @@ namespace Orchard.FileSystems.Media { return (di.Attributes & FileAttributes.Hidden) != 0; } + public static bool FolderNameContainsInvalidCharacters(string folderName) => + folderName.IndexOfAny(InvalidFolderNameCharacters) > -1; + + public static bool FileNameContainsInvalidCharacters(string fileName) => + fileName.IndexOfAny(InvalidFileNameCharacters) > -1; + #endregion private class FileSystemStorageFile : IStorageFile { diff --git a/src/Orchard/FileSystems/Media/IStorageProvider.cs b/src/Orchard/FileSystems/Media/IStorageProvider.cs index 39501cdaa..b7d771e6b 100644 --- a/src/Orchard/FileSystems/Media/IStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/IStorageProvider.cs @@ -128,7 +128,7 @@ namespace Orchard.FileSystems.Media { void SaveStream(string path, Stream inputStream); /// - /// Combines to paths. + /// Combines two paths. /// /// The parent path. /// The child path. diff --git a/src/Orchard/FileSystems/Media/InvalidNameCharacterException.cs b/src/Orchard/FileSystems/Media/InvalidNameCharacterException.cs new file mode 100644 index 000000000..53fcff0c5 --- /dev/null +++ b/src/Orchard/FileSystems/Media/InvalidNameCharacterException.cs @@ -0,0 +1,7 @@ +using System; + +namespace Orchard.FileSystems.Media { + public class InvalidNameCharacterException : ArgumentException { + public InvalidNameCharacterException(string message) : base(message) { } + } +} diff --git a/src/Orchard/FileSystems/Media/StorageProviderExtensions.cs b/src/Orchard/FileSystems/Media/StorageProviderExtensions.cs index 8380b7645..1a5e4bf96 100644 --- a/src/Orchard/FileSystems/Media/StorageProviderExtensions.cs +++ b/src/Orchard/FileSystems/Media/StorageProviderExtensions.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Orchard.FileSystems.Media { public static class StorageProviderExtensions { diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index a24d2a5fa..57312d77f 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -45,7 +45,6 @@ ..\OrchardBasicCorrectness.ruleset false false - pdbonly @@ -159,6 +158,7 @@ + From 15cad85d1e0008d01bea84c68736a4e0324c7627 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 18 Apr 2024 23:35:48 +0200 Subject: [PATCH 2/4] #6793: Adding a content-independent culture selector shape for the front-end (#8784) * Adds a new CultureSelector shape for front-end * fixed query string culture change * Moving NameValueCollectionExtensions from Orchard.DynamicForms and Orchard.Localization to Orchard.Framework * Code styling * Simplifying UserCultureSelectorController and removing the addition of the culture to the query string * EOF empty lines and code styling * Fixing that the main Orchard.Localization should depend on Orchard.Autoroute * Code styling in LocalizationService * Updating LocalizationService to not have to use IEnumerable.Single * Matching culture name matching in LocalizationService culture- and casing-invariant --------- Co-authored-by: Sergio Navarro Co-authored-by: psp589 --- .../Helpers/NameValueCollectionExtensions.cs | 12 -- .../Orchard.DynamicForms.csproj | 1 - .../Services/FormService.cs | 2 +- .../UserCultureSelectorController.cs | 48 ++++++++ .../Modules/Orchard.Localization/Module.txt | 4 +- .../Orchard.Localization.csproj | 9 +- .../Selectors/CookieCultureSelector.cs | 11 +- .../Services/ILocalizationService.cs | 3 + .../Services/LocalizationService.cs | 113 +++++++++++++----- .../Orchard.Localization/Services/Utils.cs | 31 +++++ .../Views/UserCultureSelector.cshtml | 34 ++++++ src/Orchard/Orchard.Framework.csproj | 1 + .../NameValueCollectionExtensions.cs | 12 ++ 13 files changed, 226 insertions(+), 55 deletions(-) delete mode 100644 src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml create mode 100644 src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs deleted file mode 100644 index cdd14b43d..000000000 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using System.Web; - -namespace Orchard.DynamicForms.Helpers { - public static class NameValueCollectionExtensions { - public static string ToQueryString(this NameValueCollection nameValues) { - return String.Join("&", (from string name in nameValues select String.Concat(name, "=", HttpUtility.UrlEncode(nameValues[name]))).ToArray()); - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj b/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj index 3954c9a07..1b17c9c07 100644 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj +++ b/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj @@ -340,7 +340,6 @@ - diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs index 7c6024869..603f5099d 100644 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs +++ b/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs @@ -467,4 +467,4 @@ namespace Orchard.DynamicForms.Services { return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType); } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs b/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs new file mode 100644 index 000000000..2a4543c28 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs @@ -0,0 +1,48 @@ +using System; +using System.Web.Mvc; +using Orchard.Autoroute.Models; +using Orchard.CulturePicker.Services; +using Orchard.Environment.Extensions; +using Orchard.Localization.Providers; +using Orchard.Localization.Services; +using Orchard.Mvc.Extensions; + +namespace Orchard.Localization.Controllers { + [OrchardFeature("Orchard.Localization.CultureSelector")] + public class UserCultureSelectorController : Controller { + private readonly ILocalizationService _localizationService; + private readonly ICultureStorageProvider _cultureStorageProvider; + public IOrchardServices Services { get; set; } + + public UserCultureSelectorController( + IOrchardServices services, + ILocalizationService localizationService, + ICultureStorageProvider cultureStorageProvider) { + Services = services; + _localizationService = localizationService; + _cultureStorageProvider = cultureStorageProvider; + } + + public ActionResult ChangeCulture(string culture) { + if (string.IsNullOrEmpty(culture)) { + throw new ArgumentNullException(culture); + } + + var returnUrl = Utils.GetReturnUrl(Services.WorkContext.HttpContext.Request); + if (string.IsNullOrEmpty(returnUrl)) + returnUrl = ""; + + if (_localizationService.TryGetRouteForUrl(returnUrl, out AutoroutePart currentRoutePart) + && _localizationService.TryFindLocalizedRoute(currentRoutePart.ContentItem, culture, out AutoroutePart localizedRoutePart)) { + returnUrl = localizedRoutePart.Path; + } + + _cultureStorageProvider.SetCulture(culture); + if (!returnUrl.StartsWith("~/")) { + returnUrl = "~/" + returnUrl; + } + + return this.RedirectLocal(returnUrl); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Module.txt b/src/Orchard.Web/Modules/Orchard.Localization/Module.txt index f9fd36172..26cf3fe42 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Localization/Module.txt @@ -9,7 +9,7 @@ Features: Orchard.Localization: Description: Enables localization of content items. Category: Content - Dependencies: Settings + Dependencies: Settings, Orchard.Autoroute Name: Content Localization Orchard.Localization.DateTimeFormat: Description: Enables PO-based translation of date/time formats and names of days and months. @@ -30,4 +30,4 @@ Features: Description: Enables transliteration of the autoroute slug when creating a piece of content. Category: Content Name: URL Transliteration - Dependencies: Orchard.Localization.Transliteration, Orchard.Autoroute + Dependencies: Orchard.Localization.Transliteration diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj index 3718e30d9..771a933d6 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj +++ b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj @@ -89,10 +89,11 @@ + - + @@ -118,6 +119,7 @@ + @@ -196,6 +198,9 @@ + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) @@ -229,4 +234,4 @@ - + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs b/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs index d39abbb5b..00c82464f 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs @@ -1,4 +1,3 @@ -using System; using System.Web; using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; @@ -19,7 +18,8 @@ namespace Orchard.Localization.Selectors { private const string AdminCookieName = "OrchardCurrentCulture-Admin"; private const int DefaultExpireTimeYear = 1; - public CookieCultureSelector(IHttpContextAccessor httpContextAccessor, + public CookieCultureSelector( + IHttpContextAccessor httpContextAccessor, IClock clock, ShellSettings shellSettings) { _httpContextAccessor = httpContextAccessor; @@ -36,11 +36,10 @@ namespace Orchard.Localization.Selectors { var cookie = new HttpCookie(cookieName, culture) { Expires = _clock.UtcNow.AddYears(DefaultExpireTimeYear), + Domain = httpContext.Request.IsLocal ? null : httpContext.Request.Url.Host }; - cookie.Domain = !httpContext.Request.IsLocal ? httpContext.Request.Url.Host : null; - - if (!String.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) { + if (!string.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) { cookie.Path = GetCookiePath(httpContext); } @@ -73,4 +72,4 @@ namespace Orchard.Localization.Selectors { return cookiePath; } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs index f2ace3a9c..04cda8d60 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Orchard.Autoroute.Models; using Orchard.ContentManagement; using Orchard.Localization.Models; @@ -10,5 +11,7 @@ namespace Orchard.Localization.Services { void SetContentCulture(IContent content, string culture); IEnumerable GetLocalizations(IContent content); IEnumerable GetLocalizations(IContent content, VersionOptions versionOptions); + bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute); + bool TryGetRouteForUrl(string url, out AutoroutePart route); } } diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs index e8baceda0..97bcb0ae8 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs @@ -1,53 +1,59 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Orchard.Autoroute.Models; +using Orchard.Autoroute.Services; using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; using Orchard.Localization.Models; namespace Orchard.Localization.Services { public class LocalizationService : ILocalizationService { private readonly IContentManager _contentManager; private readonly ICultureManager _cultureManager; + private readonly IHomeAliasService _homeAliasService; - - public LocalizationService(IContentManager contentManager, ICultureManager cultureManager) { + public LocalizationService(IContentManager contentManager, ICultureManager cultureManager, IHomeAliasService homeAliasService) { _contentManager = contentManager; _cultureManager = cultureManager; + _homeAliasService = homeAliasService; } + /// + /// Warning: Returns only the first item of same culture localizations. + /// + public LocalizationPart GetLocalizedContentItem(IContent content, string culture) => + GetLocalizedContentItem(content, culture, null); - public LocalizationPart GetLocalizedContentItem(IContent content, string culture) { - // Warning: Returns only the first of same culture localizations. - return GetLocalizedContentItem(content, culture, null); - } - + /// + /// Warning: Returns only the first item of same culture localizations. + /// public LocalizationPart GetLocalizedContentItem(IContent content, string culture, VersionOptions versionOptions) { var cultureRecord = _cultureManager.GetCultureByName(culture); - if (cultureRecord == null) return null; + if (cultureRecord == null) { + return null; + } var localized = content.As(); - if (localized == null) return null; + if (localized == null) { + return null; + } var masterContentItemId = localized.HasTranslationGroup ? localized.Record.MasterContentItemId : localized.Id; - // Warning: Returns only the first of same culture localizations. return _contentManager .Query(versionOptions, content.ContentItem.ContentType) - .Where(l => - (l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId) && - l.CultureId == cultureRecord.Id) + .Where(localization => + (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId) + && localization.CultureId == cultureRecord.Id) .Slice(1) .FirstOrDefault(); } - public string GetContentCulture(IContent content) { - var localized = content.As(); - - return localized?.Culture == null ? - _cultureManager.GetSiteCulture() : - localized.Culture.Culture; - } + public string GetContentCulture(IContent content) => + content.As()?.Culture?.Culture ?? _cultureManager.GetSiteCulture(); public void SetContentCulture(IContent content, string culture) { var localized = content.As(); @@ -57,11 +63,14 @@ namespace Orchard.Localization.Services { localized.Culture = _cultureManager.GetCultureByName(culture); } - public IEnumerable GetLocalizations(IContent content) { - // Warning: May contain more than one localization of the same culture. - return GetLocalizations(content, null); - } + /// + /// Warning: May contain more than one localization of the same culture. + /// + public IEnumerable GetLocalizations(IContent content) => GetLocalizations(content, null); + /// + /// Warning: May contain more than one localization of the same culture. + /// public IEnumerable GetLocalizations(IContent content, VersionOptions versionOptions) { if (content.ContentItem.Id == 0) return Enumerable.Empty(); @@ -76,16 +85,58 @@ namespace Orchard.Localization.Services { if (localized.HasTranslationGroup) { int masterContentItemId = localized.MasterContentItem.ContentItem.Id; - query = query.Where(l => - l.Id != contentItemId && // Exclude the content - (l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId)); + query = query.Where(localization => + localization.Id != contentItemId && // Exclude the content + (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId)); } else { - query = query.Where(l => l.MasterContentItemId == contentItemId); + query = query.Where(localization => localization.MasterContentItemId == contentItemId); } - // Warning: May contain more than one localization of the same culture. return query.List().ToList(); } + + public bool TryGetRouteForUrl(string url, out AutoroutePart route) { + route = _contentManager.Query() + .ForVersion(VersionOptions.Published) + .Where(r => r.DisplayAlias == url) + .List() + .FirstOrDefault(); + + route = route ?? _homeAliasService.GetHomePage(VersionOptions.Latest).As(); + + return route != null; + } + + public bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute) { + if (!routableContent.Parts.Any(p => p.Is())) { + localizedRoute = null; + + return false; + } + + IEnumerable localizations = GetLocalizations(routableContent, VersionOptions.Published); + + ILocalizableAspect localizationPart = null, siteCultureLocalizationPart = null; + foreach (var localization in localizations) { + if (localization.Culture.Culture.Equals(cultureName, StringComparison.InvariantCultureIgnoreCase)) { + localizationPart = localization; + + break; + } + + if (localization.Culture == null && siteCultureLocalizationPart == null) { + siteCultureLocalizationPart = localization; + } + } + + if (localizationPart == null) { + localizationPart = siteCultureLocalizationPart; + } + + localizedRoute = localizationPart?.As(); + + return localizedRoute != null; + } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs new file mode 100644 index 000000000..c69c4e5df --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs @@ -0,0 +1,31 @@ +using System.Web; + +namespace Orchard.CulturePicker.Services { + public static class Utils { + public static string GetReturnUrl(HttpRequestBase request) { + if (request.UrlReferrer == null) { + return ""; + } + + string localUrl = GetAppRelativePath(request.UrlReferrer.AbsolutePath, request); + return HttpUtility.UrlDecode(localUrl); + } + + public static string GetAppRelativePath(string logicalPath, HttpRequestBase request) { + if (request.ApplicationPath == null) { + return ""; + } + + logicalPath = logicalPath.ToLower(); + string appPath = request.ApplicationPath.ToLower(); + if (appPath != "/") { + appPath += "/"; + } + else { + return logicalPath.Substring(1); + } + + return logicalPath.Replace(appPath, ""); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml b/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml new file mode 100644 index 000000000..71f4f8084 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml @@ -0,0 +1,34 @@ +@using Orchard.Localization.Services + +@{ + var currentCulture = WorkContext.CurrentCulture; + var supportedCultures = WorkContext.Resolve().ListCultures().ToList(); +} + +
+
    + @foreach (var supportedCulture in supportedCultures) + { + var url = Url.Action( + "ChangeCulture", + "UserCultureSelector", + new + { + area = "Orchard.Localization", + culture = supportedCulture, + returnUrl = Html.ViewContext.HttpContext.Request.RawUrl + }); + +
  • + @if (supportedCulture.Equals(currentCulture)) + { + @T("{0} (current)", supportedCulture) + } + else + { + @supportedCulture + } +
  • + } +
+
diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 57312d77f..56e9fbc43 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -685,6 +685,7 @@ + diff --git a/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs b/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs new file mode 100644 index 000000000..f46069790 --- /dev/null +++ b/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs @@ -0,0 +1,12 @@ +using System.Collections.Specialized; +using System.Linq; +using System.Web; + +namespace Orchard.Utility.Extensions { + public static class NameValueCollectionExtensions { + public static string ToQueryString(this NameValueCollection nameValues) => + string.Join( + "&", + (from string name in nameValues select string.Concat(name, "=", HttpUtility.UrlEncode(nameValues[name]))).ToArray()); + } +} From fdbb06ba8dc641be9f46dabd7f94086d507e9537 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 19 Apr 2024 11:11:47 +0200 Subject: [PATCH 3/4] #8640: Fixing consistency between different Enumeration Field flavors' data storage (#8789) * Reworking EnumerationField's logic to store/retrieve its (selected) values * Fixing exception when creating new item with CheckboxList flavor, adding more nullchecks and compactness * Code styling in EnumerationFieldDriver * Code styling in EnumerationField editor template * Fixing that EnumerationFieldDriver and the EnumerationField editor template should read SelectedValues instead of Values directly --------- Co-authored-by: Matteo Piovanelli --- .../Drivers/EnumerationFieldDriver.cs | 35 ++++++++----------- .../Orchard.Fields/Fields/EnumerationField.cs | 26 ++++---------- .../Fields/Enumeration.Edit.cshtml | 13 ++++--- 3 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs b/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs index fb9e64649..d1b93fe6e 100644 --- a/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs @@ -4,46 +4,41 @@ using Orchard.ContentManagement.Handlers; using Orchard.Fields.Fields; using Orchard.Fields.Settings; using Orchard.Localization; -using System; -using System.Collections.Generic; -using System.Linq; namespace Orchard.Fields.Drivers { public class EnumerationFieldDriver : ContentFieldDriver { public IOrchardServices Services { get; set; } + private const string TemplateName = "Fields/Enumeration.Edit"; public EnumerationFieldDriver(IOrchardServices services) { Services = services; + T = NullLocalizer.Instance; } public Localizer T { get; set; } - private static string GetPrefix(ContentField field, ContentPart part) { - return part.PartDefinition.Name + "." + field.Name; - } + private static string GetPrefix(ContentField field, ContentPart part) => + part.PartDefinition.Name + "." + field.Name; - private static string GetDifferentiator(EnumerationField field, ContentPart part) { - return field.Name; - } + private static string GetDifferentiator(EnumerationField field) => field.Name; protected override DriverResult Display(ContentPart part, EnumerationField field, string displayType, dynamic shapeHelper) { - return ContentShape("Fields_Enumeration", GetDifferentiator(field, part), - () => shapeHelper.Fields_Enumeration()); + return ContentShape("Fields_Enumeration", GetDifferentiator(field), () => shapeHelper.Fields_Enumeration()); } protected override DriverResult Editor(ContentPart part, EnumerationField field, dynamic shapeHelper) { - return ContentShape("Fields_Enumeration_Edit", GetDifferentiator(field, part), - () => { - if (part.IsNew() && String.IsNullOrEmpty(field.Value)) { - var settings = field.PartFieldDefinition.Settings.GetModel(); - if (!String.IsNullOrWhiteSpace(settings.DefaultValue)) { - field.Value = settings.DefaultValue; - } + return ContentShape("Fields_Enumeration_Edit", GetDifferentiator(field), () => { + if (part.IsNew() && string.IsNullOrEmpty(field.Value)) { + var settings = field.PartFieldDefinition.Settings.GetModel(); + if (!string.IsNullOrWhiteSpace(settings.DefaultValue)) { + field.SelectedValues = new string[] { settings.DefaultValue }; } - return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: field, Prefix: GetPrefix(field, part)); - }); + } + + return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: field, Prefix: GetPrefix(field, part)); + }); } protected override DriverResult Editor(ContentPart part, EnumerationField field, IUpdateModel updater, dynamic shapeHelper) { diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Fields/EnumerationField.cs b/src/Orchard.Web/Modules/Orchard.Fields/Fields/EnumerationField.cs index a81d6829e..ee572b66b 100644 --- a/src/Orchard.Web/Modules/Orchard.Fields/Fields/EnumerationField.cs +++ b/src/Orchard.Web/Modules/Orchard.Fields/Fields/EnumerationField.cs @@ -7,28 +7,16 @@ namespace Orchard.Fields.Fields { private const char Separator = ';'; public string Value { - get { return Storage.Get(); } - set { Storage.Set(value ?? String.Empty); } + get => Storage.Get()?.Trim(Separator) ?? ""; + set => Storage.Set(string.IsNullOrWhiteSpace(value) + ? string.Empty + // It is now the responsibility of this field to (re-)add the separators. + : Separator + value.Trim(Separator) + Separator); } public string[] SelectedValues { - get { - var value = Value; - if(string.IsNullOrWhiteSpace(value)) { - return new string[0]; - } - - return value.Split(new [] { Separator }, StringSplitOptions.RemoveEmptyEntries); - } - - set { - if (value == null || value.Length == 0) { - Value = String.Empty; - } - else { - Value = Separator + string.Join(Separator.ToString(), value) + Separator; - } - } + get => Value?.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0]; + set => Value = value?.Length > 0 ? string.Join(Separator.ToString(), value) : ""; } } } diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Views/EditorTemplates/Fields/Enumeration.Edit.cshtml b/src/Orchard.Web/Modules/Orchard.Fields/Views/EditorTemplates/Fields/Enumeration.Edit.cshtml index 0d7b16dd8..0f3d10036 100644 --- a/src/Orchard.Web/Modules/Orchard.Fields/Views/EditorTemplates/Fields/Enumeration.Edit.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Fields/Views/EditorTemplates/Fields/Enumeration.Edit.cshtml @@ -1,22 +1,27 @@ @model Orchard.Fields.Fields.EnumerationField + @using Orchard.Fields.Settings; + @{ var settings = Model.PartFieldDefinition.Settings.GetModel(); string[] options = (!String.IsNullOrWhiteSpace(settings.Options)) ? settings.Options.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.None) : new string[] { T("Select an option").ToString() }; } +
- + @switch (settings.ListMode) { case ListMode.Dropdown: - @Html.DropDownListFor(m => m.Value, new SelectList(options, Model.Value), settings.Required ? new { required = "required" } : null) + @Html.DropDownListFor(m => m.Value, new SelectList(options, Model.SelectedValues.FirstOrDefault()), settings.Required ? new { required = "required" } : null) break; case ListMode.Radiobutton: foreach (var option in options) { if (string.IsNullOrWhiteSpace(option)) { - } + + } else { - } + + } } break; From 23fb9dff922059e800b8363005cc7f90c7d2b5b6 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 19 Apr 2024 15:34:59 +0200 Subject: [PATCH 4/4] Fixing merge --- .../Orchard.Localization.csproj | 5 ++ .../Services/LocalizationService.cs | 47 ++++++++----------- .../Controllers/ClientStorageController.cs | 7 ++- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj index 2a88f828f..80b4bd1c9 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj +++ b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj @@ -121,6 +121,9 @@ + + + @@ -195,6 +198,8 @@ + + 10.0 diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs index 7d8c75c76..b0e1fccb5 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs @@ -41,7 +41,7 @@ namespace Orchard.Localization.Services { return null; } - if (localized?.Culture.Culture == culture) return localized; + if (localized.Culture?.Culture == culture) return localized; return GetLocalizationsQuery(localized, versionOptions) .Where(localization => localization.CultureId == cultureRecord.Id) @@ -74,35 +74,10 @@ namespace Orchard.Localization.Services { var localized = content.As(); return GetLocalizationsQuery(localized, versionOptions) - .Where(l => l.Id != localized.Id) // Exclude the current content. + .Where(localization => localization.Id != localized.Id) // Exclude the current content. .List(); } - - private IContentQuery GetLocalizationsQuery(LocalizationPart localizationPart, VersionOptions versionOptions) { - var masterId = localizationPart.HasTranslationGroup ? - localizationPart.Record.MasterContentItemId : localizationPart.Id; - - var query = versionOptions == null ? - _contentManager.Query(localized.ContentItem.ContentType) : - _contentManager.Query(versionOptions, localized.ContentItem.ContentType); - - int contentItemId = localized.ContentItem.Id; - - if (localized.HasTranslationGroup) { - int masterContentItemId = localized.MasterContentItem.ContentItem.Id; - - query = query.Where(localization => - localization.Id != contentItemId && // Exclude the content - (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId)); - } - else { - query = query.Where(localization => localization.MasterContentItemId == contentItemId); - } - - return query.List().ToList(); - } - public bool TryGetRouteForUrl(string url, out AutoroutePart route) { route = _contentManager.Query() .ForVersion(VersionOptions.Published) @@ -145,5 +120,23 @@ namespace Orchard.Localization.Services { return localizedRoute != null; } + + /// + /// Warning: May contain more than one localization of the same culture. + /// + private IContentQuery GetLocalizationsQuery(LocalizationPart localizationPart, VersionOptions versionOptions) { + var masterId = localizationPart.HasTranslationGroup + ? localizationPart.Record.MasterContentItemId + : localizationPart.Id; + + var query = _contentManager.Query(localizationPart.ContentItem.ContentType); + + if (versionOptions == null) { + query = query.ForVersion(versionOptions); + } + + return query + .Where(localization => localization.Id == masterId || localization.MasterContentItemId == masterId); + } } } diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs index 23df7a2a7..fc61e3c63 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Web.Mvc; using Orchard.ContentManagement; +using Orchard.ContentManagement.Handlers; using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.Logging; @@ -186,13 +187,15 @@ namespace Orchard.MediaLibrary.Controllers { if (mediaItemsUsingTheFile == 1) { // if the file is referenced only by the deleted media content, the file too can be removed. try { _mediaLibraryService.DeleteFile(replaceMedia.FolderPath, replaceMedia.FileName); - } catch (ArgumentException) { // File not found by FileSystemStorageProvider is thrown as ArgumentException. + } + catch (ArgumentException) { // File not found by FileSystemStorageProvider is thrown as ArgumentException. statuses.Add(new { error = T("Error when deleting file to replace: file {0} does not exist in folder {1}. Media has been updated anyway.", replaceMedia.FileName, replaceMedia.FolderPath).Text, progress = 1.0 }); } - } else { + } + else { // it changes the media file name replaceMedia.FileName = filename; }