From 3a6810ec6749f333580d12ca0a6223131dd46682 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Wed, 17 Apr 2024 11:52:51 +0200 Subject: [PATCH 1/9] 8225: Adding a checkbox to StringFilterForm to control whether an empty value should cause the filter to be skipped (#8781) * Adding a checkbox to StringFilterForm to control whether an empty value should cause the filter to be skipped * Removing StringOperator.ContainsAnyIfProvided as its now obsolete due to the IgnoreFilterIfValueIsEmpty checkbox setting * Code styling in StringFilterForm * Adding missing T-string * Adding migration step to upgrade from using the ContainsAnyIfProvided operator in StringFilterForm --- .../FilterEditors/Forms/StringFilterForm.cs | 94 +++++++++---------- .../Modules/Orchard.Projections/Migrations.cs | 29 ++++-- 2 files changed, 65 insertions(+), 58 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs b/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs index 59656c4b6d5..4b88d245c2e 100644 --- a/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs +++ b/src/Orchard.Web/Modules/Orchard.Projections/FilterEditors/Forms/StringFilterForm.cs @@ -7,7 +7,6 @@ using Orchard.Localization; namespace Orchard.Projections.FilterEditors.Forms { - public class StringFilterForm : IFormProvider { public const string FormName = "StringFilter"; @@ -20,44 +19,44 @@ public StringFilterForm(IShapeFactory shapeFactory) { } public void Describe(DescribeContext context) { - Func form = - shape => { - - var f = Shape.Form( - Id: "StringFilter", - _Operator: Shape.SelectList( - Id: "operator", Name: "Operator", - Title: T("Operator"), - Size: 1, - Multiple: false - ), - _Value: Shape.TextBox( - Id: "value", Name: "Value", - Title: T("Value"), - Classes: new[] { "text medium", "tokenized" }, - Description: T("Enter the value the string should be.") - ) - ); + object form(IShapeFactory shape) { + var f = Shape.Form( + Id: "StringFilter", + _Operator: Shape.SelectList( + Id: "operator", Name: "Operator", + Title: T("Operator"), + Size: 1, + Multiple: false + ), + _Value: Shape.TextBox( + Id: "value", Name: "Value", + Title: T("Value"), + Classes: new[] { "text medium", "tokenized" }, + Description: T("Enter the value the string should be.") + ), + _IgnoreIfEmptyValue: Shape.Checkbox( + Id: "IgnoreFilterIfValueIsEmpty", + Name: "IgnoreFilterIfValueIsEmpty", + Title: T("Ignore filter if value is empty"), + Description: T("When enabled, the filter will not be applied if the provided value is or evaluates to empty."), + Value: "true" + )); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Equals), Text = T("Is equal to").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEquals), Text = T("Is not equal to").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Contains), Text = T("Contains").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAny), Text = T("Contains any word").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAll), Text = T("Contains all words").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Starts), Text = T("Starts with").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotStarts), Text = T("Does not start with").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Ends), Text = T("Ends with").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEnds), Text = T("Does not end with").Text }); - f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotContains), Text = T("Does not contain").Text }); - f._Operator.Add(new SelectListItem { - Value = Convert.ToString(StringOperator.ContainsAnyIfProvided), - Text = T("Contains any word (if any is provided)").Text - }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Equals), Text = T("Is equal to").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEquals), Text = T("Is not equal to").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Contains), Text = T("Contains").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAny), Text = T("Contains any word").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.ContainsAll), Text = T("Contains all words").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Starts), Text = T("Starts with").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotStarts), Text = T("Does not start with").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.Ends), Text = T("Ends with").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotEnds), Text = T("Does not end with").Text }); + f._Operator.Add(new SelectListItem { Value = Convert.ToString(StringOperator.NotContains), Text = T("Does not contain").Text }); - return f; - }; + return f; + } - context.Form(FormName, form); + context.Form(FormName, (Func)form); } @@ -65,6 +64,11 @@ public static Action GetFilterPredicate(dynamic formState var op = (StringOperator)Enum.Parse(typeof(StringOperator), Convert.ToString(formState.Operator)); object value = Convert.ToString(formState.Value); + if (bool.TryParse(formState.IgnoreFilterIfValueIsEmpty?.ToString() ?? "", out bool ignoreIfEmpty) + && ignoreIfEmpty + && string.IsNullOrWhiteSpace(value as string)) + return (ex) => { }; + switch (op) { case StringOperator.Equals: return x => x.Eq(property, value); @@ -92,14 +96,6 @@ public static Action GetFilterPredicate(dynamic formState return y => y.Not(x => x.Like(property, Convert.ToString(value), HqlMatchMode.End)); case StringOperator.NotContains: return y => y.Not(x => x.Like(property, Convert.ToString(value), HqlMatchMode.Anywhere)); - case StringOperator.ContainsAnyIfProvided: - if (string.IsNullOrWhiteSpace((string)value)) - return x => x.IsNotEmpty("Id"); // basically, return every possible ContentItem - var values3 = Convert.ToString(value) - .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - var predicates3 = values3.Skip(1) - .Select>(x => y => y.Like(property, x, HqlMatchMode.Anywhere)).ToArray(); - return x => x.Disjunction(y => y.Like(property, values3[0], HqlMatchMode.Anywhere), predicates3); default: throw new ArgumentOutOfRangeException(); } @@ -130,11 +126,6 @@ public static LocalizedString DisplayFilter(string fieldName, dynamic formState, return T("{0} does not end with '{1}'", fieldName, value); case StringOperator.NotContains: return T("{0} does not contain '{1}'", fieldName, value); - case StringOperator.ContainsAnyIfProvided: - return T("{0} contains any of '{1}' (or '{1}' is empty)", - fieldName, - new LocalizedString(string.Join("', '", - value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)))); default: throw new ArgumentOutOfRangeException(); } @@ -151,7 +142,6 @@ public enum StringOperator { NotStarts, Ends, NotEnds, - NotContains, - ContainsAnyIfProvided + NotContains } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs index 2c751846a05..cf0b6f57e16 100644 --- a/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Projections/Migrations.cs @@ -1,7 +1,6 @@ using System; using System.Data; using System.Linq; -using Orchard.ContentManagement; using Orchard.ContentManagement.MetaData; using Orchard.Core.Common.Models; using Orchard.Core.Contents.Extensions; @@ -15,13 +14,16 @@ namespace Orchard.Projections { public class Migrations : DataMigrationImpl { private readonly IRepository _memberBindingRepository; private readonly IRepository _layoutRepository; - + private readonly IRepository _filterRepository; public Migrations( IRepository memberBindingRepository, - IRepository layoutRepository) { + IRepository layoutRepository, + IRepository filterRepository) { _memberBindingRepository = memberBindingRepository; _layoutRepository = layoutRepository; + _filterRepository = filterRepository; + T = NullLocalizer.Instance; } @@ -359,15 +361,30 @@ public int UpdateFrom4() { } public int UpdateFrom5() { - SchemaBuilder.AlterTable("LayoutRecord", t => t.AddColumn("GUIdentifier", - column => column.WithLength(68))); + SchemaBuilder.AlterTable("LayoutRecord", t => t + .AddColumn("GUIdentifier", column => column.WithLength(68))); var layoutRecords = _layoutRepository.Table.Where(l => l.GUIdentifier == null || l.GUIdentifier == "").ToList(); foreach (var layout in layoutRecords) { - layout.GUIdentifier = Guid.NewGuid().ToString(); + layout.GUIdentifier = Guid.NewGuid().ToString(); } return 6; } + + public int UpdateFrom6() { + // This casts a somewhat wide net, but filters can't be queried by the form they are using and different + // types of filters can (and do) use StringFilterForm. However, the "Operator" parameter's value being + // "ContainsAnyIfProvided" is very specific. + var formStateToReplace = "ContainsAnyIfProvided"; + var filterRecordsToUpdate = _filterRepository.Table.Where(f => f.State.Contains(formStateToReplace)).ToList(); + foreach (var filter in filterRecordsToUpdate) { + filter.State = filter.State.Replace( + formStateToReplace, + "ContainsAnytrue"); + } + + return 7; + } } } \ No newline at end of file From 9b0d78b4f65188cebcb1462b1d73a0d1c28ef2ec Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 18 Apr 2024 21:35:39 +0200 Subject: [PATCH 2/9] #8786: Updating Microsoft.IdentityModel.* packages from 5.2.4 to 5.7.0 (#8787) * Updating Microsoft.IdentityModel.* packages from 5.2.4 to 5.7.0 * Updating OpenId packages from 4.0.0 to 4.2.2 to mach the version of Microsoft.Owin * Fixing assembly binding redirects in Orchard.Web/Web.config * OpenId: Updating Microsoft.IdentityModel.Tokens.* packages to version 5.7.0.0 to match Microsoft.IdentityModel.Tokens * OpenId: Updating Microsoft.IdentityModel.Protocols packages to 5.7.0.0 too * The return of System.Net.Http assembly binding redirects --- .../Orchard.Azure.MediaServices.csproj | 16 ++--- .../packages.config | 8 +-- .../Modules/Orchard.Glimpse/Web.config | 4 -- .../Orchard.OpenId/Orchard.OpenId.csproj | 61 +++++++++++-------- .../Modules/Orchard.OpenId/Web.config | 24 ++++++-- .../Modules/Orchard.OpenId/packages.config | 29 +++++---- src/Orchard.Web/Web.config | 8 +-- 7 files changed, 87 insertions(+), 63 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Orchard.Azure.MediaServices.csproj b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Orchard.Azure.MediaServices.csproj index 7d1c324bb43..ab5b25bdeea 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Orchard.Azure.MediaServices.csproj +++ b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Orchard.Azure.MediaServices.csproj @@ -79,14 +79,14 @@ ..\..\..\packages\Microsoft.Data.Services.Client.5.8.4\lib\net40\Microsoft.Data.Services.Client.dll - - ..\..\..\packages\Microsoft.IdentityModel.JsonWebTokens.5.2.4\lib\net451\Microsoft.IdentityModel.JsonWebTokens.dll + + ..\..\..\packages\Microsoft.IdentityModel.JsonWebTokens.5.7.0\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll - - ..\..\..\packages\Microsoft.IdentityModel.Logging.5.2.4\lib\net451\Microsoft.IdentityModel.Logging.dll + + ..\..\..\packages\Microsoft.IdentityModel.Logging.5.7.0\lib\net461\Microsoft.IdentityModel.Logging.dll - - ..\..\..\packages\Microsoft.IdentityModel.Tokens.5.2.4\lib\net451\Microsoft.IdentityModel.Tokens.dll + + ..\..\..\packages\Microsoft.IdentityModel.Tokens.5.7.0\lib\net461\Microsoft.IdentityModel.Tokens.dll ..\..\..\packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4\Microsoft.Practices.TransientFaultHandling.Core.dll @@ -130,8 +130,8 @@ 3.5 - - ..\..\..\packages\System.IdentityModel.Tokens.Jwt.5.2.4\lib\net451\System.IdentityModel.Tokens.Jwt.dll + + ..\..\..\packages\System.IdentityModel.Tokens.Jwt.5.7.0\lib\net461\System.IdentityModel.Tokens.Jwt.dll diff --git a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/packages.config b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/packages.config index 670d7e211cd..670821743e1 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/packages.config +++ b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/packages.config @@ -12,9 +12,9 @@ - - - + + + @@ -22,7 +22,7 @@ - + diff --git a/src/Orchard.Web/Modules/Orchard.Glimpse/Web.config b/src/Orchard.Web/Modules/Orchard.Glimpse/Web.config index d4c2217570d..0ad5a744213 100644 --- a/src/Orchard.Web/Modules/Orchard.Glimpse/Web.config +++ b/src/Orchard.Web/Modules/Orchard.Glimpse/Web.config @@ -45,10 +45,6 @@ - - - - diff --git a/src/Orchard.Web/Modules/Orchard.OpenId/Orchard.OpenId.csproj b/src/Orchard.Web/Modules/Orchard.OpenId/Orchard.OpenId.csproj index 109495d160b..5eb6ee7f5fa 100644 --- a/src/Orchard.Web/Modules/Orchard.OpenId/Orchard.OpenId.csproj +++ b/src/Orchard.Web/Modules/Orchard.OpenId/Orchard.OpenId.csproj @@ -84,23 +84,32 @@ ..\..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll - - ..\..\..\packages\Microsoft.IdentityModel.JsonWebTokens.5.2.4\lib\net451\Microsoft.IdentityModel.JsonWebTokens.dll + + ..\..\..\packages\Microsoft.IdentityModel.JsonWebTokens.5.7.0\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll - - ..\..\..\packages\Microsoft.IdentityModel.Logging.5.2.4\lib\net451\Microsoft.IdentityModel.Logging.dll + + ..\..\..\packages\Microsoft.IdentityModel.Logging.5.7.0\lib\net461\Microsoft.IdentityModel.Logging.dll ..\..\..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.4.403061554\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll - - ..\..\..\packages\Microsoft.IdentityModel.Protocols.5.2.4\lib\net451\Microsoft.IdentityModel.Protocols.dll + + ..\..\..\packages\Microsoft.IdentityModel.Protocols.5.7.0\lib\net461\Microsoft.IdentityModel.Protocols.dll - - ..\..\..\packages\Microsoft.IdentityModel.Protocols.OpenIdConnect.5.2.4\lib\net451\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll + + ..\..\..\packages\Microsoft.IdentityModel.Protocols.OpenIdConnect.5.7.0\lib\net461\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll - - ..\..\..\packages\Microsoft.IdentityModel.Tokens.5.2.4\lib\net451\Microsoft.IdentityModel.Tokens.dll + + ..\..\..\packages\Microsoft.IdentityModel.Protocols.WsFederation.5.7.0\lib\net461\Microsoft.IdentityModel.Protocols.WsFederation.dll + + + ..\..\..\packages\Microsoft.IdentityModel.Tokens.5.7.0\lib\net461\Microsoft.IdentityModel.Tokens.dll + + + ..\..\..\packages\Microsoft.IdentityModel.Tokens.Saml.5.7.0\lib\net461\Microsoft.IdentityModel.Tokens.Saml.dll + + + ..\..\..\packages\Microsoft.IdentityModel.Xml.5.7.0\lib\net461\Microsoft.IdentityModel.Xml.dll ..\..\..\packages\Microsoft.Owin.4.2.2\lib\net45\Microsoft.Owin.dll @@ -111,29 +120,29 @@ ..\..\..\packages\Microsoft.Owin.Security.4.2.2\lib\net45\Microsoft.Owin.Security.dll - - ..\..\..\packages\Microsoft.Owin.Security.ActiveDirectory.4.0.0\lib\net451\Microsoft.Owin.Security.ActiveDirectory.dll + + ..\..\..\packages\Microsoft.Owin.Security.ActiveDirectory.4.2.2\lib\net45\Microsoft.Owin.Security.ActiveDirectory.dll ..\..\..\packages\Microsoft.Owin.Security.Cookies.4.2.2\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\..\..\packages\Microsoft.Owin.Security.Facebook.4.0.0\lib\net451\Microsoft.Owin.Security.Facebook.dll + + ..\..\..\packages\Microsoft.Owin.Security.Facebook.4.2.2\lib\net45\Microsoft.Owin.Security.Facebook.dll - - ..\..\..\packages\Microsoft.Owin.Security.Google.4.0.0\lib\net451\Microsoft.Owin.Security.Google.dll + + ..\..\..\packages\Microsoft.Owin.Security.Google.4.2.2\lib\net45\Microsoft.Owin.Security.Google.dll - - ..\..\..\packages\Microsoft.Owin.Security.Jwt.4.0.0\lib\net451\Microsoft.Owin.Security.Jwt.dll + + ..\..\..\packages\Microsoft.Owin.Security.Jwt.4.2.2\lib\net45\Microsoft.Owin.Security.Jwt.dll - - ..\..\..\packages\Microsoft.Owin.Security.OAuth.4.0.0\lib\net451\Microsoft.Owin.Security.OAuth.dll + + ..\..\..\packages\Microsoft.Owin.Security.OAuth.4.2.2\lib\net45\Microsoft.Owin.Security.OAuth.dll - - ..\..\..\packages\Microsoft.Owin.Security.OpenIdConnect.4.0.0\lib\net451\Microsoft.Owin.Security.OpenIdConnect.dll + + ..\..\..\packages\Microsoft.Owin.Security.OpenIdConnect.4.2.2\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll - - ..\..\..\packages\Microsoft.Owin.Security.Twitter.4.0.0\lib\net451\Microsoft.Owin.Security.Twitter.dll + + ..\..\..\packages\Microsoft.Owin.Security.Twitter.4.2.2\lib\net45\Microsoft.Owin.Security.Twitter.dll ..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -162,8 +171,8 @@ - - ..\..\..\packages\System.IdentityModel.Tokens.Jwt.5.2.4\lib\net451\System.IdentityModel.Tokens.Jwt.dll + + ..\..\..\packages\System.IdentityModel.Tokens.Jwt.5.7.0\lib\net461\System.IdentityModel.Tokens.Jwt.dll diff --git a/src/Orchard.Web/Modules/Orchard.OpenId/Web.config b/src/Orchard.Web/Modules/Orchard.OpenId/Web.config index 2e752185629..9e9001652e2 100644 --- a/src/Orchard.Web/Modules/Orchard.OpenId/Web.config +++ b/src/Orchard.Web/Modules/Orchard.OpenId/Web.config @@ -76,7 +76,7 @@ - + @@ -88,15 +88,15 @@ - + - + - + @@ -114,6 +114,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.OpenId/packages.config b/src/Orchard.Web/Modules/Orchard.OpenId/packages.config index d4e71c49d6c..c31b3098702 100644 --- a/src/Orchard.Web/Modules/Orchard.OpenId/packages.config +++ b/src/Orchard.Web/Modules/Orchard.OpenId/packages.config @@ -12,29 +12,32 @@ - - + + - - - + + + + + + - + - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index d8fa011b2bb..ef54236949d 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -247,7 +247,7 @@ - + @@ -255,15 +255,15 @@ - + - + - + From 0b86413e6045be10f5f0c4986672547b6d5d001f Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 18 Apr 2024 21:37:48 +0200 Subject: [PATCH 3/9] 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 | 28 +++++---- .../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 | 59 +++++++++++++++---- .../FileSystems/Media/IStorageProvider.cs | 2 +- .../Media/InvalidNameCharacterException.cs | 7 +++ .../Media/StorageProviderExtensions.cs | 4 -- src/Orchard/Orchard.Framework.csproj | 2 +- 12 files changed, 148 insertions(+), 91 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 a0470c2b83a..314b984d4bc 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 @@ private static string ConvertToRelativeUriPath(string path) { 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 @@ public IEnumerable ListFiles(string path) { } 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 @@ public bool TryCreateFolder(string path) { 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 @@ public void RenameFolder(string path, string newPath) { 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 @@ public void RenameFile(string path, string newPath) { 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 @@ public void CopyFile(string path, string newPath) { 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 @@ public AzureBlobFolderStorage(CloudBlobDirectory blob, string rootPath) { _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 @@ private static long GetDirectorySize(CloudBlobDirectory directoryBlob) { 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 8130d5e5183..49c35432332 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.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 @@ public ActionResult Upload(string folderPath, string type) { url = mediaPart.FileName, }); } + catch (InvalidNameCharacterException) { + statuses.Add(new { + error = T("The file name contains invalid character(s)").Text, + progress = 1.0, + }); + } 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, + error = ex.Message, progress = 1.0, }); } @@ -130,7 +135,7 @@ public ActionResult Replace(int replaceId, string type) { 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 @@ public ActionResult Replace(int replaceId, string type) { 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 @@ public ActionResult Replace(int replaceId, string type) { 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 @@ public ActionResult Replace(int replaceId, string type) { }); } 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 b79ede9e561..9b567b1c503 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 @@ IMediaLibraryService mediaManagerService 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 @@ public ActionResult Create() { 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 @@ public ActionResult Edit() { 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 @@ public ActionResult Edit() { } 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 @@ public ActionResult Move(string folderPath, int[] mediaItemIds) { // 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 f9e58b547ee..f165d5955b2 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 @@ protected override DriverResult Editor(MediaPart part, IUpdateModel updater, dyn 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 @@ protected override DriverResult Editor(MediaPart part, IUpdateModel updater, dyn _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 (InvalidNameCharacterException) { + updater.AddModelError(fieldName, T("The file name contains invalid character(s).")); } - catch (Exception) { - updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file")); + 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 349469205d5..abadaff3ac8 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 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 @@ public class MediaLibraryService : IMediaLibraryService { 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 @@ public MediaPart ImportMedia(Stream stream, string relativePath, string filename } 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 @@ public MediaPart ImportMedia(string relativePath, string filename, string conten 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 @@ public bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p 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 @@ public bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p 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 db6de355985..08e7466e749 100644 --- a/src/Orchard/Exceptions/DefaultExceptionPolicy.cs +++ b/src/Orchard/Exceptions/DefaultExceptionPolicy.cs @@ -34,7 +34,7 @@ public bool HandleException(object sender, Exception exception) { return false; } - if (sender is IEventBus && exception is OrchardFatalException) { + if (sender is IEventBus && exception is OrchardFatalException) { return false; } @@ -49,7 +49,7 @@ public bool HandleException(object sender, Exception exception) { } 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 a66ba1a8b9d..534c774065e 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 ab0abfd2652..34a7b505518 100644 --- a/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs @@ -4,15 +4,22 @@ using System.Linq; using System.Web.Hosting; using Orchard.Environment.Configuration; +using Orchard.Exceptions; using Orchard.Localization; +using Orchard.Utility.Extensions; using Orchard.Validation; -using Orchard.Exceptions; 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 @@ public FileSystemStorageProvider(ShellSettings settings) { appPath = HostingEnvironment.ApplicationVirtualPath; } if (!appPath.EndsWith("/")) - appPath = appPath + '/'; + appPath += '/'; if (!appPath.StartsWith("/")) appPath = '/' + appPath; @@ -39,21 +46,21 @@ public FileSystemStorageProvider(ShellSettings settings) { 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 @@ public bool TryCreateFolder(string path) { /// 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 @@ public void RenameFolder(string oldPath, string newPath) { 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 @@ public void RenameFile(string oldPath, string newPath) { 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 @@ public void CopyFile(string originalPath, string duplicatePath) { /// 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 @@ private static bool IsHidden(FileSystemInfo di) { 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 39501cdaa7d..b7d771e6b3f 100644 --- a/src/Orchard/FileSystems/Media/IStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/IStorageProvider.cs @@ -128,7 +128,7 @@ public interface IStorageProvider : IDependency { 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 00000000000..53fcff0c550 --- /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 8380b7645fb..1a5e4bf96ea 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 a24d2a5fa73..57312d77f90 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 4/9] #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 cdd14b43d54..00000000000 --- 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 3954c9a070b..1b17c9c0718 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 7c602486905..603f5099d7e 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 @@ private static bool IsFormElementType(IElementValidator validator, Type elementT 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 00000000000..2a4543c289b --- /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 f9fd36172ca..26cf3fe4285 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 3718e30d9ed..771a933d610 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 d39abbb5b67..00c82464f87 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 @@ public class CookieCultureSelector : ICultureSelector, ICultureStorageProvider { 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 @@ public void SetCulture(string culture) { 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 @@ private string GetCookiePath(HttpContextBase httpContext) { 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 f2ace3a9cb5..04cda8d60b6 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 @@ public interface ILocalizationService : IDependency { 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 e8baceda04d..97bcb0ae80b 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 @@ public void SetContentCulture(IContent content, string culture) { 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 @@ public IEnumerable GetLocalizations(IContent content, VersionO 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 00000000000..c69c4e5df33 --- /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 00000000000..71f4f80844e --- /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 57312d77f90..56e9fbc43bf 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 00000000000..f4606979042 --- /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 5/9] #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 fb9e64649ae..d1b93fe6e31 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.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 a81d6829e0a..ee572b66bd4 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 @@ public class EnumerationField : ContentField { 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 0d7b16dd8c7..0f3d1003600 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 cf4335e5ba7613cae62cbd0db9b88781f7b99f3d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 19 Apr 2024 11:20:32 +0200 Subject: [PATCH 6/9] #6193: IHtmlFilter and TokenFilter improvements and bugfixes (#6938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * IHtmlFilter and TokenFilter improvements and bugfixes. This changeset unifies the two separate TokenFilter implementations (one for BodyPart, TextField, etc and another one for certain elements such as Html). It also fixes a bug with the TokenFilter when executing for HtmlWidget, where the wrong content item is being recorded by the handler (the original implementation). Thirdly, it removes awkward code repetition by moving filter execution into a dedicated HtmlFilterRunner service. * Renaming IHtmlFilterRunner to IHtmlFilterProcessor (and its references) * Applying IHtmlFilterProcessor to HtmlMenuItems too + code styling in BodyPartDriver * Fixing FeedControllerTests.CorePartValuesAreExtracted * Code styling --------- Co-authored-by: Jean-Thierry Kéchichian Co-authored-by: Sipke Schoorstra Co-authored-by: Benedek Farkas --- .../Feeds/Controllers/FeedControllerTests.cs | 2 + .../Core/Common/Drivers/BodyPartDriver.cs | 45 +++++++------------ .../Core/Common/Drivers/TextFieldDriver.cs | 9 ++-- .../Core/Common/Models/BodyPart.cs | 8 ++++ .../Core/Common/Services/BbcodeFilter.cs | 21 +++++---- .../Core/Common/Services/TextFieldFilter.cs | 6 +-- .../CorePartsFeedItemBuilder.cs | 10 ++--- .../Feeds/StandardBuilders/ItemInspector.cs | 12 ++--- .../StandardQueries/ContainerFeedQuery.cs | 13 +++--- .../Views/MenuItemLink-HtmlMenuItem.cshtml | 11 ++++- .../Markdown/Services/MarkdownFilter.cs | 13 +++--- .../Services/Rendering/CloudVideoFilter.cs | 6 +-- .../Drivers/HeadingElementDriver.cs | 10 ++--- .../Drivers/HtmlElementDriver.cs | 13 +++--- .../Drivers/MarkdownElementDriver.cs | 13 +++--- .../Drivers/ParagraphElementDriver.cs | 10 ++--- .../Drivers/TextElementDriver.cs | 13 +++--- .../Orchard.Layouts/Filters/TokensFilter.cs | 34 -------------- .../Modules/Orchard.Layouts/Module.txt | 2 +- .../Orchard.Layouts/Orchard.Layouts.csproj | 4 -- .../Services/ElementFilterProcessor.cs | 19 -------- .../Services/IElementFilter.cs | 8 ---- .../Services/IElementFilterProcessor.cs | 8 ---- .../StandardQueries/QueryFeedQuery.cs | 13 +++--- .../Orchard.Tags/Feeds/TagFeedQuery.cs | 31 ++++--------- .../Orchard.Tokens/Filters/TokensFilter.cs | 36 +++++---------- .../Modules/Orchard.Tokens/Module.txt | 1 + src/Orchard/Orchard.Framework.csproj | 4 ++ src/Orchard/Services/HtmlFilter.cs | 5 +++ src/Orchard/Services/HtmlFilterContext.cs | 8 ++++ src/Orchard/Services/HtmlFilterProcessor.cs | 16 +++++++ src/Orchard/Services/IHtmlFilter.cs | 4 +- src/Orchard/Services/IHtmlFilterProcessor.cs | 28 ++++++++++++ 33 files changed, 200 insertions(+), 236 deletions(-) delete mode 100644 src/Orchard.Web/Modules/Orchard.Layouts/Filters/TokensFilter.cs delete mode 100644 src/Orchard.Web/Modules/Orchard.Layouts/Services/ElementFilterProcessor.cs delete mode 100644 src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilter.cs delete mode 100644 src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilterProcessor.cs create mode 100644 src/Orchard/Services/HtmlFilter.cs create mode 100644 src/Orchard/Services/HtmlFilterContext.cs create mode 100644 src/Orchard/Services/HtmlFilterProcessor.cs create mode 100644 src/Orchard/Services/IHtmlFilterProcessor.cs diff --git a/src/Orchard.Core.Tests/Feeds/Controllers/FeedControllerTests.cs b/src/Orchard.Core.Tests/Feeds/Controllers/FeedControllerTests.cs index 56c07dc076f..151d9773a27 100644 --- a/src/Orchard.Core.Tests/Feeds/Controllers/FeedControllerTests.cs +++ b/src/Orchard.Core.Tests/Feeds/Controllers/FeedControllerTests.cs @@ -20,6 +20,7 @@ using Orchard.Tests.Modules; using Orchard.Tests.Stubs; using Orchard.Core.Title.Models; +using Orchard.Services; namespace Orchard.Core.Tests.Feeds.Controllers { [TestFixture] @@ -177,6 +178,7 @@ public void CorePartValuesAreExtracted() { builder.RegisterInstance(mockContentManager.Object).As(); builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterInstance(query).As(); var container = builder.Build(); diff --git a/src/Orchard.Web/Core/Common/Drivers/BodyPartDriver.cs b/src/Orchard.Web/Core/Common/Drivers/BodyPartDriver.cs index 064dee05f1e..55c46eac227 100644 --- a/src/Orchard.Web/Core/Common/Drivers/BodyPartDriver.cs +++ b/src/Orchard.Web/Core/Common/Drivers/BodyPartDriver.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web; +using System.Web; using Orchard.Mvc.Html; using Orchard.ContentManagement; using Orchard.ContentManagement.Aspects; using Orchard.ContentManagement.Drivers; using Orchard.Core.Common.Models; -using Orchard.Core.Common.Settings; using Orchard.Core.Common.ViewModels; using Orchard.Services; using System.Web.Mvc; @@ -15,13 +12,13 @@ namespace Orchard.Core.Common.Drivers { public class BodyPartDriver : ContentPartDriver { - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; private readonly RequestContext _requestContext; private const string TemplateName = "Parts.Common.Body"; - public BodyPartDriver(IOrchardServices services, IEnumerable htmlFilters, RequestContext requestContext) { - _htmlFilters = htmlFilters; + public BodyPartDriver(IOrchardServices services, IHtmlFilterProcessor htmlFilterProcessor, RequestContext requestContext) { + _htmlFilterProcessor = htmlFilterProcessor; Services = services; _requestContext = requestContext; } @@ -33,32 +30,27 @@ protected override string Prefix { } protected override DriverResult Display(BodyPart part, string displayType, dynamic shapeHelper) { + string GetProcessedBodyText() => _htmlFilterProcessor.ProcessFilters(part.Text, part.GetFlavor(), part); + return Combined( - ContentShape("Parts_Common_Body", - () => { - var bodyText = _htmlFilters.Aggregate(part.Text, (text, filter) => filter.ProcessContent(text, GetFlavor(part))); - return shapeHelper.Parts_Common_Body(Html: new HtmlString(bodyText)); - }), - ContentShape("Parts_Common_Body_Summary", - () => { - var bodyText = _htmlFilters.Aggregate(part.Text, (text, filter) => filter.ProcessContent(text, GetFlavor(part))); - return shapeHelper.Parts_Common_Body_Summary(Html: new HtmlString(bodyText)); - }) - ); + ContentShape("Parts_Common_Body", () => + shapeHelper.Parts_Common_Body(Html: new HtmlString(GetProcessedBodyText()))), + ContentShape("Parts_Common_Body_Summary", () => + shapeHelper.Parts_Common_Body_Summary(Html: new HtmlString(GetProcessedBodyText())))); } protected override DriverResult Editor(BodyPart part, dynamic shapeHelper) { var model = BuildEditorViewModel(part,_requestContext); - return ContentShape("Parts_Common_Body_Edit", - () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix)); + return ContentShape("Parts_Common_Body_Edit", () => + shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix)); } protected override DriverResult Editor(BodyPart part, IUpdateModel updater, dynamic shapeHelper) { var model = BuildEditorViewModel(part, _requestContext); updater.TryUpdateModel(model, Prefix, null, null); - return ContentShape("Parts_Common_Body_Edit", - () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix)); + return ContentShape("Parts_Common_Body_Edit", () => + shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix)); } protected override void Importing(BodyPart part, ContentManagement.Handlers.ImportContentContext context) { @@ -83,18 +75,11 @@ protected override void Cloning(BodyPart originalPart, BodyPart clonePart, Clone private static BodyEditorViewModel BuildEditorViewModel(BodyPart part,RequestContext requestContext) { return new BodyEditorViewModel { BodyPart = part, - EditorFlavor = GetFlavor(part), + EditorFlavor = part.GetFlavor(), AddMediaPath = new PathBuilder(part,requestContext).AddContentType().AddContainerSlug().ToString() }; } - private static string GetFlavor(BodyPart part) { - var typePartSettings = part.Settings.GetModel(); - return (typePartSettings != null && !string.IsNullOrWhiteSpace(typePartSettings.Flavor)) - ? typePartSettings.Flavor - : part.PartDefinition.Settings.GetModel().FlavorDefault; - } - class PathBuilder { private readonly IContent _content; private string _path; diff --git a/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs b/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs index b29b3cb4417..8154ad25edb 100644 --- a/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs +++ b/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs @@ -14,10 +14,10 @@ namespace Orchard.Core.Common.Drivers { public class TextFieldDriver : ContentFieldDriver { - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public TextFieldDriver(IOrchardServices services, IEnumerable htmlFilters) { - _htmlFilters = htmlFilters; + public TextFieldDriver(IOrchardServices services, IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; Services = services; T = NullLocalizer.Instance; } @@ -37,8 +37,7 @@ protected override DriverResult Display(ContentPart part, TextField field, strin return ContentShape("Fields_Common_Text", GetDifferentiator(field, part), () => { var settings = field.PartFieldDefinition.Settings.GetModel(); - - object fieldValue = new HtmlString(_htmlFilters.Aggregate(field.Value, (text, filter) => filter.ProcessContent(text, settings.Flavor))); + var fieldValue = new HtmlString(_htmlFilterProcessor.ProcessFilters(field.Value, settings.Flavor, part)); return shapeHelper.Fields_Common_Text(Name: field.Name, Value: fieldValue); }); } diff --git a/src/Orchard.Web/Core/Common/Models/BodyPart.cs b/src/Orchard.Web/Core/Common/Models/BodyPart.cs index 7d191cce2c5..c797fb68638 100644 --- a/src/Orchard.Web/Core/Common/Models/BodyPart.cs +++ b/src/Orchard.Web/Core/Common/Models/BodyPart.cs @@ -1,4 +1,5 @@ using Orchard.ContentManagement; +using Orchard.Core.Common.Settings; namespace Orchard.Core.Common.Models { public class BodyPart : ContentPart { @@ -11,5 +12,12 @@ public string Format { get { return Retrieve(x => x.Format); } set { Store(x => x.Format, value); } } + + public string GetFlavor() { + var typePartSettings = Settings.GetModel(); + return string.IsNullOrWhiteSpace(typePartSettings?.Flavor) + ? PartDefinition.Settings.GetModel().FlavorDefault + : typePartSettings.Flavor; + } } } diff --git a/src/Orchard.Web/Core/Common/Services/BbcodeFilter.cs b/src/Orchard.Web/Core/Common/Services/BbcodeFilter.cs index 33c406cfded..78a2e4f89b7 100644 --- a/src/Orchard.Web/Core/Common/Services/BbcodeFilter.cs +++ b/src/Orchard.Web/Core/Common/Services/BbcodeFilter.cs @@ -5,27 +5,26 @@ using Orchard.Services; namespace Orchard.Core.Common.Services { - public class BbcodeFilter : IHtmlFilter { - public string ProcessContent(string text, string flavor) { - return BbcodeReplace(text); + public class BbcodeFilter : HtmlFilter { + public override string ProcessContent(string text, HtmlFilterContext context) { + return BbcodeReplace(text, context); } // Can be moved somewhere else once we have IoC enabled body text filters. - private static string BbcodeReplace(string text) { - if (string.IsNullOrEmpty(text)) - return string.Empty; + private static string BbcodeReplace(string text, HtmlFilterContext context) { + if (String.IsNullOrEmpty(text)) + return String.Empty; - // optimize code path if nothing to do + // Optimize code path if nothing to do. if (!text.Contains("[url]") && !text.Contains("[img]") && !text.Contains("[url=")) { return text; } var sb = new StringBuilder(text); - var index = -1; var allIndexes = new List(); - // process all [url] + // Process all [url]. while (-1 != (index = text.IndexOf("[url]", index + 1, StringComparison.Ordinal))) { allIndexes.Add(index); } @@ -63,7 +62,7 @@ private static string BbcodeReplace(string text) { var url = text.Substring(start + 5, urlEnd - start - 5); var title = text.Substring(urlEnd + 1, end - urlEnd - 1); - // substitue [url] by + // Substitute [url] by . sb.Remove(start, end - start + 6); sb.Insert(start, String.Format("{1}", url, title)); } @@ -85,7 +84,7 @@ private static string BbcodeReplace(string text) { var url = text.Substring(start + 5, end - start - 5); - // substitue [url] by + // Substitue [url] by . sb.Remove(start, end - start + 6); if (!string.IsNullOrEmpty(url)) { diff --git a/src/Orchard.Web/Core/Common/Services/TextFieldFilter.cs b/src/Orchard.Web/Core/Common/Services/TextFieldFilter.cs index 263d17af5bd..75713da339e 100644 --- a/src/Orchard.Web/Core/Common/Services/TextFieldFilter.cs +++ b/src/Orchard.Web/Core/Common/Services/TextFieldFilter.cs @@ -4,10 +4,10 @@ using Orchard.Utility.Extensions; namespace Orchard.Core.Common.Services { - public class TextFieldFilter : IHtmlFilter { - public string ProcessContent(string text, string flavor) { + public class TextFieldFilter : HtmlFilter { + public override string ProcessContent(string text, HtmlFilterContext context) { // Flavor is null for a normal input/text field - return flavor == null || string.Equals(flavor, "textarea", StringComparison.OrdinalIgnoreCase) ? ReplaceNewLines(text) : text; + return context.Flavor == null || String.Equals(context.Flavor, "textarea", StringComparison.OrdinalIgnoreCase) ? ReplaceNewLines(text) : text; } private static string ReplaceNewLines(string text) { diff --git a/src/Orchard.Web/Core/Feeds/StandardBuilders/CorePartsFeedItemBuilder.cs b/src/Orchard.Web/Core/Feeds/StandardBuilders/CorePartsFeedItemBuilder.cs index 79f36b8abce..84a9957ad8e 100644 --- a/src/Orchard.Web/Core/Feeds/StandardBuilders/CorePartsFeedItemBuilder.cs +++ b/src/Orchard.Web/Core/Feeds/StandardBuilders/CorePartsFeedItemBuilder.cs @@ -13,15 +13,15 @@ namespace Orchard.Core.Feeds.StandardBuilders { public class CorePartsFeedItemBuilder : IFeedItemBuilder { private readonly IContentManager _contentManager; private readonly RouteCollection _routes; - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; public CorePartsFeedItemBuilder( IContentManager contentManager, RouteCollection routes, - IEnumerable htmlFilters) { + IHtmlFilterProcessor htmlFilterProcessor) { _contentManager = contentManager; _routes = routes; - _htmlFilters = htmlFilters; + _htmlFilterProcessor = htmlFilterProcessor; } public void Populate(FeedContext context) { @@ -29,8 +29,8 @@ public void Populate(FeedContext context) { var inspector = new ItemInspector( feedItem.Item, - _contentManager.GetItemMetadata(feedItem.Item), - _htmlFilters); + _contentManager.GetItemMetadata(feedItem.Item), + _htmlFilterProcessor); // author is intentionally left empty as it could result in unwanted spam diff --git a/src/Orchard.Web/Core/Feeds/StandardBuilders/ItemInspector.cs b/src/Orchard.Web/Core/Feeds/StandardBuilders/ItemInspector.cs index a382c0fe849..b05c5c8d042 100644 --- a/src/Orchard.Web/Core/Feeds/StandardBuilders/ItemInspector.cs +++ b/src/Orchard.Web/Core/Feeds/StandardBuilders/ItemInspector.cs @@ -12,17 +12,17 @@ namespace Orchard.Core.Feeds.StandardBuilders { public class ItemInspector { private readonly IContent _item; private readonly ContentItemMetadata _metadata; - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; private readonly ICommonPart _common; private readonly ITitleAspect _titleAspect; private readonly BodyPart _body; - public ItemInspector(IContent item, ContentItemMetadata metadata) : this(item, metadata, Enumerable.Empty()) {} + public ItemInspector(IContent item, ContentItemMetadata metadata) : this(item, metadata, null) {} - public ItemInspector(IContent item, ContentItemMetadata metadata, IEnumerable htmlFilters) { + public ItemInspector(IContent item, ContentItemMetadata metadata, IHtmlFilterProcessor htmlFilterProcessor) { _item = item; _metadata = metadata; - _htmlFilters = htmlFilters; + _htmlFilterProcessor = htmlFilterProcessor; _common = item.Get(); _titleAspect = item.Get(); _body = item.Get(); @@ -49,8 +49,8 @@ public RouteValueDictionary Link { public string Description { get { - if (_body != null && !string.IsNullOrEmpty(_body.Text)) { - return _htmlFilters.Aggregate(_body.Text, (text, filter) => filter.ProcessContent(text, GetFlavor(_body))); + if (_htmlFilterProcessor != null && _body != null && !string.IsNullOrEmpty(_body.Text)) { + return _htmlFilterProcessor.ProcessFilters(_body.Text, GetFlavor(_body), _body); } return Title; } diff --git a/src/Orchard.Web/Core/Feeds/StandardQueries/ContainerFeedQuery.cs b/src/Orchard.Web/Core/Feeds/StandardQueries/ContainerFeedQuery.cs index c1550e7beb8..694831a1b72 100644 --- a/src/Orchard.Web/Core/Feeds/StandardQueries/ContainerFeedQuery.cs +++ b/src/Orchard.Web/Core/Feeds/StandardQueries/ContainerFeedQuery.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Web.Mvc; using System.Xml.Linq; using Orchard.ContentManagement; @@ -8,16 +7,16 @@ using Orchard.Core.Feeds.StandardBuilders; using Orchard.Mvc.Extensions; using Orchard.Services; -using Orchard.Utility.Extensions; -namespace Orchard.Core.Feeds.StandardQueries { +namespace Orchard.Core.Feeds.StandardQueries +{ public class ContainerFeedQuery : IFeedQueryProvider, IFeedQuery { private readonly IContentManager _contentManager; - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public ContainerFeedQuery(IContentManager contentManager, IEnumerable htmlFilters) { + public ContainerFeedQuery(IContentManager contentManager, IHtmlFilterProcessor htmlFilterProcessor) { _contentManager = contentManager; - _htmlFilters = htmlFilters; + _htmlFilterProcessor = htmlFilterProcessor; } public FeedQueryMatch Match(FeedContext context) { @@ -55,7 +54,7 @@ public void Execute(FeedContext context) { return; } - var inspector = new ItemInspector(container, _contentManager.GetItemMetadata(container), _htmlFilters); + var inspector = new ItemInspector(container, _contentManager.GetItemMetadata(container), _htmlFilterProcessor); if (context.Format == "rss") { var link = new XElement("link"); context.Response.Element.SetElementValue("title", inspector.Title); diff --git a/src/Orchard.Web/Core/Navigation/Views/MenuItemLink-HtmlMenuItem.cshtml b/src/Orchard.Web/Core/Navigation/Views/MenuItemLink-HtmlMenuItem.cshtml index 6ac21f40418..12a799df244 100644 --- a/src/Orchard.Web/Core/Navigation/Views/MenuItemLink-HtmlMenuItem.cshtml +++ b/src/Orchard.Web/Core/Navigation/Views/MenuItemLink-HtmlMenuItem.cshtml @@ -1 +1,10 @@ -@Html.Raw(Model.Content.BodyPart.Text) +@using Orchard.Core.Common.Models +@using Orchard.Services + +@{ + var htmlFilterProcessor = WorkContext.Resolve(); + var bodyPart = Model.Content.BodyPart as BodyPart; + var bodyText = htmlFilterProcessor.ProcessFilters(bodyPart.Text, bodyPart.GetFlavor(), bodyPart); +} + +@Html.Raw(bodyText) diff --git a/src/Orchard.Web/Modules/Markdown/Services/MarkdownFilter.cs b/src/Orchard.Web/Modules/Markdown/Services/MarkdownFilter.cs index cf5d1db3e5d..ec66bb30e28 100644 --- a/src/Orchard.Web/Modules/Markdown/Services/MarkdownFilter.cs +++ b/src/Orchard.Web/Modules/Markdown/Services/MarkdownFilter.cs @@ -1,15 +1,16 @@ using System; using Orchard.Services; -namespace Markdown.Services { - public class MarkdownFilter : IHtmlFilter { - public string ProcessContent(string text, string flavor) { - return String.Equals(flavor, "markdown", StringComparison.OrdinalIgnoreCase) ? MarkdownReplace(text) : text; +namespace Markdown.Services +{ + public class MarkdownFilter : HtmlFilter { + public override string ProcessContent(string text, HtmlFilterContext context) { + return String.Equals(context.Flavor, "markdown", StringComparison.OrdinalIgnoreCase) ? MarkdownReplace(text) : text; } private static string MarkdownReplace(string text) { - if (string.IsNullOrEmpty(text)) - return string.Empty; + if (String.IsNullOrEmpty(text)) + return String.Empty; return Markdig.Markdown.ToHtml(text); } diff --git a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Services/Rendering/CloudVideoFilter.cs b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Services/Rendering/CloudVideoFilter.cs index 56d475d85a4..b98ee97279a 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Services/Rendering/CloudVideoFilter.cs +++ b/src/Orchard.Web/Modules/Orchard.Azure.MediaServices/Services/Rendering/CloudVideoFilter.cs @@ -12,7 +12,7 @@ using Orchard.Services; namespace Orchard.Azure.MediaServices.Services.Rendering { - public class CloudVideoFilter : IHtmlFilter { + public class CloudVideoFilter : HtmlFilter { private readonly IShapeFactory _shapeFactory; private readonly IContentManager _contentManager; private readonly IShapeDisplay _shapeDisplay; @@ -23,8 +23,8 @@ public CloudVideoFilter(IShapeFactory shapeFactory, IContentManager contentManag _shapeDisplay = shapeDisplay; } - public string ProcessContent(string text, string flavor) { - return String.Equals(flavor, "html", StringComparison.OrdinalIgnoreCase) ? ReplaceVideoPlaceholder(text) : text; + public override string ProcessContent(string text, HtmlFilterContext context) { + return String.Equals(context.Flavor, "html", StringComparison.OrdinalIgnoreCase) ? ReplaceVideoPlaceholder(text) : text; } private string ReplaceVideoPlaceholder(string text) { diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HeadingElementDriver.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HeadingElementDriver.cs index 0bc14c531e9..d122e6eb57a 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HeadingElementDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HeadingElementDriver.cs @@ -2,15 +2,15 @@ using Orchard.Layouts.Framework.Display; using Orchard.Layouts.Framework.Drivers; using Orchard.Layouts.Helpers; -using Orchard.Layouts.Services; using Orchard.Layouts.ViewModels; +using Orchard.Services; namespace Orchard.Layouts.Drivers { public class HeadingElementDriver : ElementDriver { - private readonly IElementFilterProcessor _processor; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public HeadingElementDriver(IElementFilterProcessor processor) { - _processor = processor; + public HeadingElementDriver(IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; } protected override EditorResult OnBuildEditor(Heading element, ElementEditorContext context) { @@ -30,7 +30,7 @@ protected override EditorResult OnBuildEditor(Heading element, ElementEditorCont } protected override void OnDisplaying(Heading element, ElementDisplayingContext context) { - context.ElementShape.ProcessedContent = _processor.ProcessContent(element.Content, "html", context.GetTokenData()); + context.ElementShape.ProcessedContent = _htmlFilterProcessor.ProcessFilters(element.Content, new HtmlFilterContext { Flavor = "html", Data = context.GetTokenData() }); context.ElementShape.Level = element.Level; } } diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HtmlElementDriver.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HtmlElementDriver.cs index eb62d7616a8..8e7c050b6ca 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HtmlElementDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/HtmlElementDriver.cs @@ -2,15 +2,16 @@ using Orchard.Layouts.Framework.Display; using Orchard.Layouts.Framework.Drivers; using Orchard.Layouts.Helpers; -using Orchard.Layouts.Services; using Orchard.Layouts.ViewModels; +using Orchard.Services; -namespace Orchard.Layouts.Drivers { +namespace Orchard.Layouts.Drivers +{ public class HtmlElementDriver : ElementDriver { - private readonly IElementFilterProcessor _processor; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public HtmlElementDriver(IElementFilterProcessor processor) { - _processor = processor; + public HtmlElementDriver(IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; } protected override EditorResult OnBuildEditor(Html element, ElementEditorContext context) { @@ -29,7 +30,7 @@ protected override EditorResult OnBuildEditor(Html element, ElementEditorContext } protected override void OnDisplaying(Html element, ElementDisplayingContext context) { - context.ElementShape.ProcessedContent = _processor.ProcessContent(element.Content, "html", context.GetTokenData()); + context.ElementShape.ProcessedContent = _htmlFilterProcessor.ProcessFilters(element.Content, new HtmlFilterContext { Flavor = "html", Data = context.GetTokenData() }); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/MarkdownElementDriver.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/MarkdownElementDriver.cs index e988c89a17d..8e7f8d612e9 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/MarkdownElementDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/MarkdownElementDriver.cs @@ -2,16 +2,17 @@ using Orchard.Layouts.Framework.Display; using Orchard.Layouts.Framework.Drivers; using Orchard.Layouts.Helpers; -using Orchard.Layouts.Services; using Orchard.Layouts.ViewModels; +using Orchard.Services; using MarkdownElement = Orchard.Layouts.Elements.Markdown; -namespace Orchard.Layouts.Drivers { +namespace Orchard.Layouts.Drivers +{ [OrchardFeature("Orchard.Layouts.Markdown")] public class MarkdownElementDriver : ElementDriver { - private readonly IElementFilterProcessor _processor; - public MarkdownElementDriver(IElementFilterProcessor processor) { - _processor = processor; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; + public MarkdownElementDriver(IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; } protected override EditorResult OnBuildEditor(MarkdownElement element, ElementEditorContext context) { @@ -29,7 +30,7 @@ protected override EditorResult OnBuildEditor(MarkdownElement element, ElementEd } protected override void OnDisplaying(MarkdownElement element, ElementDisplayingContext context) { - context.ElementShape.ProcessedContent = _processor.ProcessContent(element.Content, "markdown", context.GetTokenData()); + context.ElementShape.ProcessedContent = _htmlFilterProcessor.ProcessFilters(element.Content, new HtmlFilterContext { Flavor = "markdown", Data = context.GetTokenData() }); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/ParagraphElementDriver.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/ParagraphElementDriver.cs index 0d285207ad5..1aca94f2a77 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/ParagraphElementDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/ParagraphElementDriver.cs @@ -2,15 +2,15 @@ using Orchard.Layouts.Framework.Display; using Orchard.Layouts.Framework.Drivers; using Orchard.Layouts.Helpers; -using Orchard.Layouts.Services; using Orchard.Layouts.ViewModels; +using Orchard.Services; namespace Orchard.Layouts.Drivers { public class ParagraphElementDriver : ElementDriver { - private readonly IElementFilterProcessor _processor; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public ParagraphElementDriver(IElementFilterProcessor processor) { - _processor = processor; + public ParagraphElementDriver(IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; } protected override EditorResult OnBuildEditor(Paragraph element, ElementEditorContext context) { @@ -28,7 +28,7 @@ protected override EditorResult OnBuildEditor(Paragraph element, ElementEditorCo } protected override void OnDisplaying(Paragraph element, ElementDisplayingContext context) { - context.ElementShape.ProcessedContent = _processor.ProcessContent(element.Content, "html", context.GetTokenData()); + context.ElementShape.ProcessedContent = _htmlFilterProcessor.ProcessFilters(element.Content, new HtmlFilterContext { Flavor = "html", Data = context.GetTokenData() }); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/TextElementDriver.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/TextElementDriver.cs index e85dad82c9c..87083dd7d6b 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/TextElementDriver.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Drivers/TextElementDriver.cs @@ -2,15 +2,16 @@ using Orchard.Layouts.Framework.Display; using Orchard.Layouts.Framework.Drivers; using Orchard.Layouts.Helpers; -using Orchard.Layouts.Services; using Orchard.Layouts.ViewModels; +using Orchard.Services; -namespace Orchard.Layouts.Drivers { +namespace Orchard.Layouts.Drivers +{ public class TextElementDriver : ElementDriver { - private readonly IElementFilterProcessor _processor; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; - public TextElementDriver(IElementFilterProcessor processor) { - _processor = processor; + public TextElementDriver(IHtmlFilterProcessor htmlFilterProcessor) { + _htmlFilterProcessor = htmlFilterProcessor; } protected override EditorResult OnBuildEditor(Text element, ElementEditorContext context) { @@ -28,7 +29,7 @@ protected override EditorResult OnBuildEditor(Text element, ElementEditorContext } protected override void OnDisplaying(Text element, ElementDisplayingContext context) { - context.ElementShape.ProcessedContent = _processor.ProcessContent(element.Content, "textarea", context.GetTokenData()); + context.ElementShape.ProcessedContent = _htmlFilterProcessor.ProcessFilters(element.Content, new HtmlFilterContext { Flavor = "textarea", Data = context.GetTokenData() }); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Filters/TokensFilter.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Filters/TokensFilter.cs deleted file mode 100644 index 1dc8a27a5c5..00000000000 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Filters/TokensFilter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Orchard.Environment.Extensions; -using Orchard.Layouts.Services; -using Orchard.Tokens; -using System.Collections.Generic; - -namespace Orchard.Layouts.Filters { - [OrchardFeature("Orchard.Layouts.Tokens")] - public class TokensFilter : IElementFilter { - - private readonly ITokenizer _tokenizer; - - public TokensFilter(ITokenizer tokenizer) { - _tokenizer = tokenizer; - } - - public string ProcessContent(string text, string flavor) { - return ProcessContent(text, flavor, new Dictionary()); - } - - public string ProcessContent(string text, string flavor, IDictionary context) { - if (String.IsNullOrEmpty(text)) - return ""; - - if (!text.Contains("#{")) { - return text; - } - - text = _tokenizer.Replace(text, context, new ReplaceOptions { Encoding = ReplaceOptions.NoEncode }); - - return text; - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Module.txt b/src/Orchard.Web/Modules/Orchard.Layouts/Module.txt index 7dec211a0f2..7a42cf991a1 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Module.txt @@ -29,7 +29,7 @@ Features: Dependencies: Orchard.Layouts, Orchard.Projections Orchard.Layouts.Tokens: Name: Element Tokens - Description: Provides an element token provider that enables elements to be rendered using a token and enables tokens to be used inside of various elements such as Html, Text and Paragraph. + Description: Provides an element token provider that enables elements to be rendered using a token. Category: Layout Dependencies: Orchard.Layouts, Orchard.Tokens Orchard.Layouts.UI: diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Orchard.Layouts.csproj b/src/Orchard.Web/Modules/Orchard.Layouts/Orchard.Layouts.csproj index ff45ec93087..e207903e8a2 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Orchard.Layouts.csproj +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Orchard.Layouts.csproj @@ -319,7 +319,6 @@ - @@ -375,9 +374,6 @@ - - - diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Services/ElementFilterProcessor.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Services/ElementFilterProcessor.cs deleted file mode 100644 index 2dd58b24fea..00000000000 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Services/ElementFilterProcessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using Orchard.Services; - -namespace Orchard.Layouts.Services { - public class ElementFilterProcessor : IElementFilterProcessor { - private readonly IEnumerable _filters; - public ElementFilterProcessor(IEnumerable filters) { - _filters = filters; - } - - public string ProcessContent(string text, string flavor, IDictionary context) { - foreach (var htmlFilter in _filters) { - var elementFilter = htmlFilter as IElementFilter; - text = elementFilter != null ? elementFilter.ProcessContent(text, flavor, context) : htmlFilter.ProcessContent(text, flavor); - } - return text; - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilter.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilter.cs deleted file mode 100644 index 23d474c7359..00000000000 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -using Orchard.Services; - -namespace Orchard.Layouts.Services { - public interface IElementFilter : IHtmlFilter { - string ProcessContent(string text, string flavor, IDictionary context); - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilterProcessor.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilterProcessor.cs deleted file mode 100644 index f66dbf07a3d..00000000000 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Services/IElementFilterProcessor.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -using Orchard.Services; - -namespace Orchard.Layouts.Services { - public interface IElementFilterProcessor : IDependency { - string ProcessContent(string text, string flavor, IDictionary context); - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Projections/StandardQueries/QueryFeedQuery.cs b/src/Orchard.Web/Modules/Orchard.Projections/StandardQueries/QueryFeedQuery.cs index d53d7228b5c..07b0d629ef7 100644 --- a/src/Orchard.Web/Modules/Orchard.Projections/StandardQueries/QueryFeedQuery.cs +++ b/src/Orchard.Web/Modules/Orchard.Projections/StandardQueries/QueryFeedQuery.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using System.Xml.Linq; @@ -11,22 +10,22 @@ using Orchard.Projections.Models; using Orchard.Projections.Services; using Orchard.Services; -using Orchard.Utility.Extensions; -namespace Orchard.Projections.StandardQueries { +namespace Orchard.Projections.StandardQueries +{ public class QueryFeedQuery : IFeedQueryProvider, IFeedQuery { private readonly IContentManager _contentManager; private readonly IProjectionManager _projectionManager; - private readonly IEnumerable _htmlFilters; + private readonly IHtmlFilterProcessor _htmlFilterProcessor; public QueryFeedQuery( IContentManager contentManager, IProjectionManager projectionManager, - IEnumerable htmlFilters) + IHtmlFilterProcessor htmlFilterProcessor) { _contentManager = contentManager; _projectionManager = projectionManager; - _htmlFilters = htmlFilters; + _htmlFilterProcessor = htmlFilterProcessor; } public FeedQueryMatch Match(FeedContext context) { @@ -55,7 +54,7 @@ public void Execute(FeedContext context) { return; } - var inspector = new ItemInspector(container, _contentManager.GetItemMetadata(container), _htmlFilters); + var inspector = new ItemInspector(container, _contentManager.GetItemMetadata(container), _htmlFilterProcessor); if (context.Format == "rss") { var link = new XElement("link"); context.Response.Element.SetElementValue("title", inspector.Title); diff --git a/src/Orchard.Web/Modules/Orchard.Tags/Feeds/TagFeedQuery.cs b/src/Orchard.Web/Modules/Orchard.Tags/Feeds/TagFeedQuery.cs index b5fd415c350..2e30e643ef8 100644 --- a/src/Orchard.Web/Modules/Orchard.Tags/Feeds/TagFeedQuery.cs +++ b/src/Orchard.Web/Modules/Orchard.Tags/Feeds/TagFeedQuery.cs @@ -1,34 +1,21 @@ using System; -using System.Collections.Generic; using System.Web.Mvc; +using System.Web.Routing; using System.Xml.Linq; -using Orchard.ContentManagement; -using Orchard.Core.Common.Models; +using Orchard.Core.Feeds; using Orchard.Core.Feeds.Models; -using Orchard.Core.Feeds.StandardBuilders; +using Orchard.Environment.Extensions; +using Orchard.Localization; using Orchard.Mvc.Extensions; -using Orchard.Services; -using Orchard.Utility.Extensions; -using Orchard.Core.Feeds; using Orchard.Tags.Services; -using Orchard.Localization; -using System.Web.Routing; -using Orchard.Environment.Extensions; namespace Orchard.Tags.Feeds { [OrchardFeature("Orchard.Tags.Feeds")] public class TagFeedQuery : IFeedQueryProvider, IFeedQuery { - private readonly IContentManager _contentManager; - private readonly IEnumerable _htmlFilters; private readonly ITagService _tagService; - public TagFeedQuery( - IContentManager contentManager, - IEnumerable htmlFilters, - ITagService tagService) { - _contentManager = contentManager; + public TagFeedQuery(ITagService tagService) { _tagService = tagService; - _htmlFilters = htmlFilters; T = NullLocalizer.Instance; } @@ -42,11 +29,11 @@ public FeedQueryMatch Match(FeedContext context) { var tagName = (string)tagIdValue.ConvertTo(typeof(string)); var tag = _tagService.GetTagByName(tagName); - + if (tag == null) { return null; } - + return new FeedQueryMatch { FeedQuery = this, Priority = -5 }; } @@ -57,10 +44,10 @@ public void Execute(FeedContext context) { var limitValue = context.ValueProvider.GetValue("limit"); var limit = 20; - if (limitValue != null) { + if (limitValue != null) { Int32.TryParse(Convert.ToString(limitValue), out limit); } - + limit = Math.Min(limit, 100); var tagName = (string)tagIdValue.ConvertTo(typeof(string)); diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Filters/TokensFilter.cs b/src/Orchard.Web/Modules/Orchard.Tokens/Filters/TokensFilter.cs index cab5c586b4e..fa301871a4c 100644 --- a/src/Orchard.Web/Modules/Orchard.Tokens/Filters/TokensFilter.cs +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Filters/TokensFilter.cs @@ -1,49 +1,33 @@ using System; -using System.Collections.Generic; -using Orchard.ContentManagement; -using Orchard.ContentManagement.Handlers; using Orchard.Environment.Extensions; using Orchard.Services; namespace Orchard.Tokens.Filters { - [OrchardFeature("Orchard.Tokens.HtmlFilter")] - public class TokensFilter : ContentHandler, IHtmlFilter { + public class TokensFilter : HtmlFilter { private readonly ITokenizer _tokenizer; - private ContentItem _displayed; public TokensFilter(ITokenizer tokenizer) { _tokenizer = tokenizer; } - - protected override void BuildDisplayShape(BuildDisplayContext context) { - _displayed = context.ContentItem; - } - - public string ProcessContent(string text, string flavor) { - return TokensReplace(text); + + public override string ProcessContent(string text, HtmlFilterContext context) { + return TokensReplace(text, context); } - private string TokensReplace(string text) { + private string TokensReplace(string text, HtmlFilterContext context) { if (String.IsNullOrEmpty(text)) return String.Empty; // Optimize code path if nothing to do. - if (!text.Contains("#{")) { + if (!text.Contains("#{")) return text; - } - - var data = new Dictionary(); - - if (_displayed != null) - data["Content"] = _displayed; - - text = _tokenizer.Replace(text, data); - - _displayed = null; - return text; + return _tokenizer.Replace(text, context.Data, + String.Equals(context.Flavor, "html", StringComparison.OrdinalIgnoreCase) + ? new ReplaceOptions { Encoding = ReplaceOptions.HtmlEncode } + : new ReplaceOptions { Encoding = ReplaceOptions.NoEncode }); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt b/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt index 2be37cd1b70..d90c598cd2b 100644 --- a/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Tokens/Module.txt @@ -20,3 +20,4 @@ Features: Description: Evaluates tokens in a body. Category: Content Dependencies: Orchard.Tokens + Priority: -1 diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 94eb529a18f..d12aa8d0599 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -432,7 +432,11 @@ + + + + diff --git a/src/Orchard/Services/HtmlFilter.cs b/src/Orchard/Services/HtmlFilter.cs new file mode 100644 index 00000000000..09b729bb78b --- /dev/null +++ b/src/Orchard/Services/HtmlFilter.cs @@ -0,0 +1,5 @@ +namespace Orchard.Services { + public abstract class HtmlFilter : Component, IHtmlFilter { + public abstract string ProcessContent(string text, HtmlFilterContext context); + } +} diff --git a/src/Orchard/Services/HtmlFilterContext.cs b/src/Orchard/Services/HtmlFilterContext.cs new file mode 100644 index 00000000000..cb821de97d3 --- /dev/null +++ b/src/Orchard/Services/HtmlFilterContext.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Orchard.Services { + public class HtmlFilterContext { + public string Flavor { get; set; } + public IDictionary Data { get; set; } = new Dictionary(); + } +} diff --git a/src/Orchard/Services/HtmlFilterProcessor.cs b/src/Orchard/Services/HtmlFilterProcessor.cs new file mode 100644 index 00000000000..f1d2cfe9eb2 --- /dev/null +++ b/src/Orchard/Services/HtmlFilterProcessor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Orchard.Services { + public class HtmlFilterProcessor : IHtmlFilterProcessor { + private readonly IEnumerable _filters; + + public HtmlFilterProcessor(IEnumerable filters) { + _filters = filters; + } + + public string ProcessFilters(string text, HtmlFilterContext context) { + return _filters.Aggregate(text, (current, htmlFilter) => htmlFilter.ProcessContent(current, context)); + } + } +} diff --git a/src/Orchard/Services/IHtmlFilter.cs b/src/Orchard/Services/IHtmlFilter.cs index 6114dc3fb17..0e206dbb0b1 100644 --- a/src/Orchard/Services/IHtmlFilter.cs +++ b/src/Orchard/Services/IHtmlFilter.cs @@ -1,5 +1,5 @@ namespace Orchard.Services { public interface IHtmlFilter : IDependency { - string ProcessContent(string text, string flavor); + string ProcessContent(string text, HtmlFilterContext context); } -} \ No newline at end of file +} diff --git a/src/Orchard/Services/IHtmlFilterProcessor.cs b/src/Orchard/Services/IHtmlFilterProcessor.cs new file mode 100644 index 00000000000..aea5defa3b9 --- /dev/null +++ b/src/Orchard/Services/IHtmlFilterProcessor.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Orchard.ContentManagement; + +namespace Orchard.Services { + public interface IHtmlFilterProcessor : IDependency { + string ProcessFilters(string text, HtmlFilterContext context); + } + + public static class HtmlFilterProcessorExtensions { + public static string ProcessFilters( + this IHtmlFilterProcessor processor, + string text, + string flavor, + IDictionary data) => + processor.ProcessFilters(text, new HtmlFilterContext { Flavor = flavor, Data = data }); + + public static string ProcessFilters(this IHtmlFilterProcessor processor, string text, string flavor) => + processor.ProcessFilters(text, new HtmlFilterContext { Flavor = flavor }); + + public static string ProcessFilters(this IHtmlFilterProcessor processor, string text, string flavor, IContent content) => + processor.ProcessFilters( + text, + new HtmlFilterContext { + Flavor = flavor, + Data = new Dictionary { { "Content", content.ContentItem } } + }); + } +} From 0faf196f744257262ab407298a94bdb500028881 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 19 Apr 2024 11:59:21 +0200 Subject: [PATCH 7/9] #6193: IHtmlFilter and TokenFilter improvements and bugfixes (fixing auto-merge) (#8790) --- src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs b/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs index 8154ad25edb..4761bb94867 100644 --- a/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs +++ b/src/Orchard.Web/Core/Common/Drivers/TextFieldDriver.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Web; using Orchard.ContentManagement; using Orchard.ContentManagement.Drivers; @@ -74,12 +72,12 @@ protected override DriverResult Editor(ContentPart part, TextField field, IUpdat if (settings.MaxLength > 0) { - var value = new HtmlString(_htmlFilters.Aggregate(field.Value, (text, filter) => filter.ProcessContent(text, settings.Flavor))) + var value = new HtmlString(_htmlFilterProcessor.ProcessFilters(field.Value, settings.Flavor, part)) .ToString().RemoveTags(); if (value.Length > settings.MaxLength) { updater.AddModelError("Text", T("The maximum allowed length for the field {0} is {1}", T(field.DisplayName), settings.MaxLength)); - } + } } } From 23fb9dff922059e800b8363005cc7f90c7d2b5b6 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 19 Apr 2024 15:34:59 +0200 Subject: [PATCH 8/9] 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 2a88f828f4f..80b4bd1c9f5 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 7d8c75c76b4..b0e1fccb526 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 @@ public LocalizationPart GetLocalizedContentItem(IContent content, string culture 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 @@ public IEnumerable GetLocalizations(IContent content, VersionO 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 @@ public bool TryFindLocalizedRoute(ContentItem routableContent, string cultureNam 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 23df7a2a79a..fc61e3c6348 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.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 @@ public ActionResult Replace(int replaceId, string type) { 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; } From e60a84535985139c05e34ff0a83598745c9d5708 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Fri, 19 Apr 2024 15:48:17 +0200 Subject: [PATCH 9/9] #8791: Merge 1.10.x into dev (#8792) * 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 * #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 * #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 * Fixing merge --------- Co-authored-by: Lombiq Co-authored-by: Sergio Navarro Co-authored-by: psp589 Co-authored-by: Matteo Piovanelli --- .../Services/FileSystems/AzureFileSystem.cs | 45 ++++--- .../Helpers/NameValueCollectionExtensions.cs | 12 -- .../Orchard.DynamicForms.csproj | 1 - .../Services/FormService.cs | 2 +- .../Drivers/EnumerationFieldDriver.cs | 35 +++--- .../Orchard.Fields/Fields/EnumerationField.cs | 26 ++-- .../Fields/Enumeration.Edit.cshtml | 13 +- .../UserCultureSelectorController.cs | 48 ++++++++ .../Modules/Orchard.Localization/Module.txt | 4 +- .../Orchard.Localization.csproj | 9 +- .../Selectors/CookieCultureSelector.cs | 11 +- .../Services/ILocalizationService.cs | 3 + .../Services/LocalizationService.cs | 116 ++++++++++++++---- .../Orchard.Localization/Services/Utils.cs | 31 +++++ .../Views/UserCultureSelector.cshtml | 34 +++++ .../Controllers/ClientStorageController.cs | 25 ++-- .../Controllers/FolderController.cs | 44 +++---- .../MediaFileName/MediaFileNameDriver.cs | 15 ++- .../Services/MediaLibraryService.cs | 21 ++-- .../Exceptions/DefaultExceptionPolicy.cs | 4 +- src/Orchard/Exceptions/ExceptionExtensions.cs | 9 +- .../Media/FileSystemStorageProvider.cs | 59 +++++++-- .../FileSystems/Media/IStorageProvider.cs | 2 +- .../Media/InvalidNameCharacterException.cs | 7 ++ .../Media/StorageProviderExtensions.cs | 4 - src/Orchard/Orchard.Framework.csproj | 3 +- .../NameValueCollectionExtensions.cs | 12 ++ 27 files changed, 410 insertions(+), 185 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/FileSystems/Media/InvalidNameCharacterException.cs create mode 100644 src/Orchard/Utility/Extensions/NameValueCollectionExtensions.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 12b2030290e..f4d586b2471 100644 --- a/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs +++ b/src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs @@ -96,6 +96,8 @@ private static string ConvertToRelativeUriPath(string path) { 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"); @@ -148,10 +150,10 @@ public IEnumerable ListFiles(string path) { } 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) { @@ -201,6 +203,11 @@ public bool TryCreateFolder(string path) { 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 @@ -232,7 +239,11 @@ public void RenameFolder(string path, string newPath) { path = ConvertToRelativeUriPath(path); newPath = ConvertToRelativeUriPath(newPath); - // Workaround for https://github.com/Azure/azure-storage-net/issues/892 + if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(newPath))) { + throw new InvalidNameCharacterException("The new directory name contains invalid character(s)"); + } + + // Workaround for https://github.com/Azure/azure-storage-net/issues/892. // Renaming a folder by only changing the casing corrupts all the files in the folder. if (path.Equals(newPath, StringComparison.OrdinalIgnoreCase)) { var tempPath = Guid.NewGuid().ToString() + "/"; @@ -277,6 +288,10 @@ public void RenameFile(string path, string newPath) { 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)); @@ -301,6 +316,10 @@ public void CopyFile(string path, string newPath) { 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"); } @@ -388,10 +407,7 @@ public AzureBlobFolderStorage(CloudBlobDirectory blob, string rootPath) { _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('/'); @@ -416,11 +432,12 @@ private static long GetDirectorySize(CloudBlobDirectory directoryBlob) { 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.DynamicForms/Helpers/NameValueCollectionExtensions.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs deleted file mode 100644 index cdd14b43d54..00000000000 --- 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 e1e332f1efc..c2cc01b7bcb 100644 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj +++ b/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj @@ -339,7 +339,6 @@ - diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs index 7c602486905..603f5099d7e 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 @@ private static bool IsFormElementType(IElementValidator validator, Type elementT return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType); } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs b/src/Orchard.Web/Modules/Orchard.Fields/Drivers/EnumerationFieldDriver.cs index 2bbd1a90379..3240df55d17 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.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 a81d6829e0a..ee572b66bd4 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 @@ public class EnumerationField : ContentField { 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 0d7b16dd8c7..0f3d1003600 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; 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 00000000000..2a4543c289b --- /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 9f6e499f81d..2acd2c4b75a 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,7 +30,7 @@ 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 Orchard.Localization.CultureNeutralPartsAndFields: Description: Enables the synchronization among localizations of parts and fields specifically marked as "Culture Neutral". Category: Content diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj index 9baa994b890..80b4bd1c9f5 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 @@ + - + @@ -120,6 +121,7 @@ + @@ -196,8 +198,7 @@ - - + @@ -233,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 d39abbb5b67..00c82464f87 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 @@ public class CookieCultureSelector : ICultureSelector, ICultureStorageProvider { 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 @@ public void SetCulture(string culture) { 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 @@ private string GetCookiePath(HttpContextBase httpContext) { 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 f2ace3a9cb5..04cda8d60b6 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 @@ public interface ILocalizationService : IDependency { 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 8255ef292de..b0e1fccb526 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs @@ -1,47 +1,56 @@ -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) { - 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; + } - if (localized?.Culture.Culture == culture) return localized; + if (localized.Culture?.Culture == culture) return localized; - // Warning: Returns only the first of same culture localizations. return GetLocalizationsQuery(localized, versionOptions) - .Where(l => l.CultureId == cultureRecord.Id) + .Where(localization => 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(); @@ -51,30 +60,83 @@ public void SetContentCulture(IContent content, string culture) { localized.Culture = _cultureManager.GetCultureByName(culture); } - public IEnumerable GetLocalizations(IContent content) { - 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(); 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(); } + 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; + } + + /// + /// 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 masterId = localizationPart.HasTranslationGroup + ? localizationPart.Record.MasterContentItemId + : localizationPart.Id; + + var query = _contentManager.Query(localizationPart.ContentItem.ContentType); - var query = versionOptions == null ? - _contentManager.Query() : _contentManager.Query(versionOptions); + if (versionOptions == null) { + query = query.ForVersion(versionOptions); + } - // Warning: May contain more than one localization of the same culture. - return query.Where(l => l.Id == masterId || l.MasterContentItemId == masterId); + return query + .Where(localization => localization.Id == masterId || localization.MasterContentItemId == masterId); } } } 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 00000000000..c69c4e5df33 --- /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 00000000000..71f4f80844e --- /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(); +} + + diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs index 00d7aacf655..fc61e3c6348 100644 --- a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs +++ b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs @@ -111,10 +111,16 @@ public ActionResult Upload(string folderPath, string type) { url = mediaPart.FileName, }); } + catch (InvalidNameCharacterException) { + statuses.Add(new { + error = T("The file name contains invalid character(s)").Text, + progress = 1.0, + }); + } 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, + error = ex.Message, progress = 1.0, }); } @@ -134,7 +140,7 @@ public ActionResult Replace(int replaceId, string type) { 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(); } @@ -142,7 +148,7 @@ public ActionResult Replace(int replaceId, string type) { 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 @@ -150,7 +156,8 @@ public ActionResult Replace(int replaceId, string type) { 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"; } @@ -180,13 +187,15 @@ public ActionResult Replace(int replaceId, string type) { 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; } @@ -210,7 +219,7 @@ public ActionResult Replace(int replaceId, string type) { }); } 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 cf039106aaf..22036beb67d 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 @@ IMediaLibraryService mediaManagerService 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 @@ public ActionResult Create() { 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 @@ public ActionResult Edit() { 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 @@ public ActionResult Edit() { } 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 @@ public ActionResult Move(string folderPath, int[] mediaItemIds) { // 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 e99e26318aa..42353deeb9d 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 @@ protected override DriverResult Editor(MediaPart part, IUpdateModel updater, dyn 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,10 +67,13 @@ protected override DriverResult Editor(MediaPart part, IUpdateModel updater, dyn _notifier.Add(NotifyType.Success, 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 (InvalidNameCharacterException) { + updater.AddModelError(fieldName, T("The file name contains invalid character(s).")); } - catch (Exception) { - updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file")); + catch (Exception exception) { + updater.AddModelError(fieldName, T("Unable to rename file: {0}", exception.Message)); } } } diff --git a/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs b/src/Orchard.Web/Modules/Orchard.MediaLibrary/Services/MediaLibraryService.cs index bbf77b8447d..e5b3bf7c1c1 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 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 @@ public class MediaLibraryService : IMediaLibraryService { private readonly IStorageProvider _storageProvider; private readonly IEnumerable _mediaFactorySelectors; private readonly IMediaFolderProvider _mediaFolderProvider; - private static char[] HttpUnallowed = new char[] { '<', '>', '*', '%', '&', ':', '\\', '?', '#' }; public MediaLibraryService( IOrchardServices orchardServices, @@ -146,12 +145,6 @@ public MediaPart ImportMedia(Stream stream, string relativePath, string filename } 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; @@ -178,9 +171,9 @@ public MediaPart ImportMedia(string relativePath, string filename, string conten 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; @@ -257,7 +250,7 @@ public bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p 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) ?? ""; @@ -269,7 +262,7 @@ public bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p 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 db6de355985..08e7466e749 100644 --- a/src/Orchard/Exceptions/DefaultExceptionPolicy.cs +++ b/src/Orchard/Exceptions/DefaultExceptionPolicy.cs @@ -34,7 +34,7 @@ public bool HandleException(object sender, Exception exception) { return false; } - if (sender is IEventBus && exception is OrchardFatalException) { + if (sender is IEventBus && exception is OrchardFatalException) { return false; } @@ -49,7 +49,7 @@ public bool HandleException(object sender, Exception exception) { } 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 a66ba1a8b9d..534c774065e 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 ab0abfd2652..34a7b505518 100644 --- a/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/FileSystemStorageProvider.cs @@ -4,15 +4,22 @@ using System.Linq; using System.Web.Hosting; using Orchard.Environment.Configuration; +using Orchard.Exceptions; using Orchard.Localization; +using Orchard.Utility.Extensions; using Orchard.Validation; -using Orchard.Exceptions; 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 @@ public FileSystemStorageProvider(ShellSettings settings) { appPath = HostingEnvironment.ApplicationVirtualPath; } if (!appPath.EndsWith("/")) - appPath = appPath + '/'; + appPath += '/'; if (!appPath.StartsWith("/")) appPath = '/' + appPath; @@ -39,21 +46,21 @@ public FileSystemStorageProvider(ShellSettings settings) { 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 @@ public bool TryCreateFolder(string path) { /// 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 @@ public void RenameFolder(string oldPath, string newPath) { 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 @@ public void RenameFile(string oldPath, string newPath) { 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 @@ public void CopyFile(string originalPath, string duplicatePath) { /// 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 @@ private static bool IsHidden(FileSystemInfo di) { 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 39501cdaa7d..b7d771e6b3f 100644 --- a/src/Orchard/FileSystems/Media/IStorageProvider.cs +++ b/src/Orchard/FileSystems/Media/IStorageProvider.cs @@ -128,7 +128,7 @@ public interface IStorageProvider : IDependency { 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 00000000000..53fcff0c550 --- /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 8380b7645fb..1a5e4bf96ea 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 d12aa8d0599..7058ad861d0 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -45,7 +45,6 @@ ..\OrchardBasicCorrectness.ruleset false false - pdbonly @@ -169,6 +168,7 @@ + @@ -718,6 +718,7 @@ + diff --git a/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs b/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs new file mode 100644 index 00000000000..f4606979042 --- /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()); + } +}