Compare commits

...

31 Commits

Author SHA1 Message Date
Benedek Farkas
3ca230dbd6 Simplifying attribute value cache 2024-12-07 11:51:14 +01:00
Benedek Farkas
5185ed0c87 Caching attribute values of img tags 2024-12-03 15:27:13 +01:00
Benedek Farkas
03cef87ef3 Caching attribute regexes 2024-12-03 14:25:45 +01:00
Benedek Farkas
bc753d2178 Optimizing image tag regex 2024-12-03 13:34:34 +01:00
Benedek Farkas
67b1214da3 Fixing that detecting the extension works regardless of casing
but it still works with Azure Blob Storage (which is case-sensitive) too
2024-12-02 17:48:56 +01:00
Benedek Farkas
318f9edd6f Code styling 2024-11-27 22:14:33 +01:00
Benedek Farkas
cfe62f4c21 Fixing that GetAttributeRegex should find attributes with empty value 2024-11-27 22:07:29 +01:00
Benedek Farkas
ff3986eb1f Reworking ProcessContent to use StringBuilder instead of replaces 2024-11-27 22:07:00 +01:00
Benedek Farkas
54dc8085c6 Code styling 2024-11-27 22:03:58 +01:00
Benedek Farkas
94343e9b66 Updating comments and code styling 2024-11-27 21:08:34 +01:00
Benedek Farkas
deb49cb616 Using regexes instead of HtmlAgilityPack, thanks GHCP 2024-11-27 20:40:51 +01:00
Benedek Farkas
2f8c748afb Code styling 2024-11-14 19:11:50 +01:00
Benedek Farkas
dcde93a3c9 Fixing that Orchard.MediaProcessingHtmlFilter should depend on Orchard.MediaProcessing 2024-11-14 19:11:32 +01:00
Benedek Farkas
61998a357f Adapting MediaProcessingHtmlFilter to IHtmlFilter breaking change 2024-11-14 18:52:55 +01:00
Benedek Farkas
d72a86617e Fixing HtmlAgilityPack and upgrading FW and package version to match Orchard.Specs 2024-11-14 18:52:20 +01:00
Benedek Farkas
cbb9a4ddba Merge remote-tracking branch 'orchard/dev' into features/dev/6986
# Conflicts:
#	src/Orchard.Web/Modules/Orchard.MediaProcessing/Orchard.MediaProcessing.csproj
#	src/Orchard.Web/Modules/Orchard.MediaProcessing/packages.config
2024-11-14 18:44:42 +01:00
aaronamm
32e7d862f9 Add AutorouteSettings.AllowSetAsHomePage to control whether user can set a content to display on a home page (#8424)
* Add AutorouteSettings.AllowSetAsHomePage to control whether user can set a content to display on a home page

* Extending the description of the AllowSetAsHomePage setting

* Updating setting label

---------

Co-authored-by: Benedek Farkas <benedek.farkas@lombiq.com>
2024-11-14 17:36:34 +01:00
Andrea Piovanelli
6e4fefd873 Content Item published before Custom Form submit event triggers (#8795)
* Moved content item publish before triggering form submit events.
Refreshed the content item passed in the context of rules manager and workflow manager.

* Added version to content manager get call after conditionally publishing the content item.
2024-05-17 09:33:45 +02:00
Andrea Piovanelli
fc8b681b4e Ambiguity in LocalizedTaxonomyController action names (#8785)
* Removed ambiguity to LocalizedTaxonomyController actions.

* Properly added ApplyPreRequest to the action.

* Code refactoring

* Code autoformatting

* Sorting using

* Overriding GetTaxonomy action

* Calling base GetTaxonomy action

* Removing ApplyPreRequest function

* Removed ApplyPreRequest function

* Added virtual keyword to GetTaxonomy action in LocalizedTaxonomyController

---------

Co-authored-by: Benedek Farkas <benedek.farkas@lombiq.com>
2024-05-16 17:16:43 +02:00
Benedek Farkas
86bcf413e7 Merge remote-tracking branch 'origin/1.10.x' into dev 2024-05-03 15:33:22 +02:00
Benedek Farkas
0d9fccb624 Fixing WebDownloaderTests 2024-05-03 15:33:07 +02:00
Benedek Farkas
62038ed1bb #6981: Normalized image profile path hash and added profile purging (#8788)
* #6981 Normalized image profile path hash and added profile purging

* Profile purge functions rather belong in IImageProfileService

* Deleting an Image Profile now also removes all its files too

* Comment formatting

* Caching the value of the "Orchard.MediaProcessing.NormalizePath" app setting in ImageProfileManager

* Code styling in ImageProfileManager

* Formatting and code styling ImageProfileManager

---------

Co-authored-by: Arjan Noordende <arjan@zumey.com>
2024-05-03 15:19:20 +02:00
Benedek Farkas
6eab0a1260 Merge branch '1.10.x' into 'dev' through the 'issue/8791' integration branch (the previous commit was a squash merge by mistake) 2024-04-19 16:50:55 +02:00
Benedek Farkas
e60a845359 #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 <github@lombiq.com>

* #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 <jersio@hotmail.com>
Co-authored-by: psp589 <pablosanchez589@gmail.com>

* #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 <MatteoPiovanelli-Laser@users.noreply.github.com>

* Fixing merge

---------

Co-authored-by: Lombiq <github@lombiq.com>
Co-authored-by: Sergio Navarro <jersio@hotmail.com>
Co-authored-by: psp589 <pablosanchez589@gmail.com>
Co-authored-by: Matteo Piovanelli <MatteoPiovanelli-Laser@users.noreply.github.com>
2024-04-19 15:48:17 +02:00
Benedek Farkas
23fb9dff92 Fixing merge 2024-04-19 15:34:59 +02:00
Benedek Farkas
0bce8e634b Merge branch '1.10.x' into issue/8791
# Conflicts:
#	src/Orchard.Web/Modules/Orchard.Azure/Services/FileSystems/AzureFileSystem.cs
#	src/Orchard.Web/Modules/Orchard.Localization/Module.txt
#	src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj
#	src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs
#	src/Orchard.Web/Modules/Orchard.MediaLibrary/Controllers/ClientStorageController.cs
2024-04-19 12:38:53 +02:00
Benedek Farkas
0faf196f74 #6193: IHtmlFilter and TokenFilter improvements and bugfixes (fixing auto-merge) (#8790) 2024-04-19 11:59:21 +02:00
Benedek Farkas
fdbb06ba8d #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 <MatteoPiovanelli-Laser@users.noreply.github.com>
2024-04-19 11:11:47 +02:00
Benedek Farkas
15cad85d1e #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 <jersio@hotmail.com>
Co-authored-by: psp589 <pablosanchez589@gmail.com>
2024-04-18 23:35:48 +02:00
Benedek Farkas
0b86413e60 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 <github@lombiq.com>
2024-04-18 21:37:48 +02:00
Arjan Noordende
02f685a797 #6986 Created feature Media Processing Html Filter 2016-06-22 13:10:26 +01:00
48 changed files with 864 additions and 251 deletions

View File

@@ -1,4 +1,5 @@
using System.Net;
using System;
using System.Net;
using NUnit.Framework;
using Orchard.Warmup.Services;
@@ -20,7 +21,7 @@ namespace Orchard.Tests.Modules.Warmup {
[Test]
public void StatusCodeShouldBe404ForUnexistingResources() {
var download = _webDownloader.Download("http://orchardproject.net/yepyep");
var download = _webDownloader.Download("https://orchardcore.net/" + Guid.NewGuid());
Assert.That(download, Is.Not.Null);
Assert.That(download.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
Assert.That(download.Content, Is.Null);
@@ -28,7 +29,7 @@ namespace Orchard.Tests.Modules.Warmup {
[Test]
public void StatusCodeShouldBe200ForValidRequests() {
var download = _webDownloader.Download("http://orchardproject.net/");
var download = _webDownloader.Download("https://orchardcore.net/");
Assert.That(download, Is.Not.Null);
Assert.That(download.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(download.Content, Is.Not.Empty);

View File

@@ -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 @@ namespace Orchard.Core.Common.Drivers {
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));
}
}
}
}

View File

@@ -18,6 +18,7 @@ namespace Orchard.Autoroute.Settings {
public AutorouteSettings() {
PerItemConfiguration = false;
AllowCustomPattern = true;
AllowSetAsHomePage = true;
UseCulturePattern = false;
AutomaticAdjustmentOnEdit = false;
PatternDefinitions = "[]";
@@ -27,6 +28,7 @@ namespace Orchard.Autoroute.Settings {
public bool PerItemConfiguration { get; set; }
public bool AllowCustomPattern { get; set; }
public bool AllowSetAsHomePage { get; set; }
public bool UseCulturePattern { get; set; }
public bool AutomaticAdjustmentOnEdit { get; set; }
public bool? IsDefault { get; set; }
@@ -100,6 +102,7 @@ namespace Orchard.Autoroute.Settings {
public void Build(ContentTypePartDefinitionBuilder builder) {
builder.WithSetting("AutorouteSettings.PerItemConfiguration", PerItemConfiguration.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("AutorouteSettings.AllowCustomPattern", AllowCustomPattern.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("AutorouteSettings.AllowSetAsHomePage", AllowSetAsHomePage.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("AutorouteSettings.UseCulturePattern", UseCulturePattern.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", AutomaticAdjustmentOnEdit.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("AutorouteSettings.PatternDefinitions", PatternDefinitions);

View File

@@ -15,7 +15,15 @@
<div>
@Html.CheckBoxFor(m => m.AllowCustomPattern)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.AllowCustomPattern)">@T("Allow custom patterns")</label>
<span class="hint">@T("Allow the user to change the pattern on each item")</span>
<span class="hint">@T("Allow the user to change the pattern on each item.")</span>
</div>
</fieldset>
<fieldset>
<div>
@Html.CheckBoxFor(m => m.AllowSetAsHomePage)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.AllowSetAsHomePage)">@T("Allow \"Set as home page\"")</label>
<span class="hint">@T("Allow the user to set a content item of this type as the home page.")</span>
<span class="hint">@T("NOTE: Certain combinations of content type definitions and permission settings can create circumstances for non-Admin users with Dashboard access to remove the home page without the ability to set another one. Use this option with caution.")</span>
</div>
</fieldset>
<fieldset>

View File

@@ -51,7 +51,7 @@
</fieldset>
@if (!Model.IsHomePage) {
if (AuthorizedFor(Permissions.SetHomePage)) {
if (AuthorizedFor(Permissions.SetHomePage) && Model.Settings.AllowSetAsHomePage) {
<fieldset>
<span class="checkbox-and-label">
@Html.CheckBoxFor(m => m.PromoteToHomePage)

View File

@@ -96,6 +96,8 @@ namespace Orchard.Azure.Services.FileSystems {
return newPath;
}
private static string GetFolderName(string path) => path.Substring(path.LastIndexOf('/') + 1);
public string Combine(string path1, string path2) {
if (path1 == null) {
throw new ArgumentNullException("path1");
@@ -148,10 +150,10 @@ namespace Orchard.Azure.Services.FileSystems {
}
return BlobClient.ListBlobs(prefix)
.OfType<CloudBlockBlob>()
.Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry))
.Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot))
.ToArray();
.OfType<CloudBlockBlob>()
.Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry))
.Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot))
.ToArray();
}
public IEnumerable<IStorageFolder> ListFolders(string path) {
@@ -201,6 +203,11 @@ namespace Orchard.Azure.Services.FileSystems {
public void CreateFolder(string path) {
path = ConvertToRelativeUriPath(path);
if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(path))) {
throw new InvalidNameCharacterException("The directory name contains invalid character(s)");
}
Container.EnsureDirectoryDoesNotExist(String.Concat(_root, path));
// Creating a virtually hidden file to make the directory an existing concept
@@ -232,7 +239,11 @@ namespace Orchard.Azure.Services.FileSystems {
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 @@ namespace Orchard.Azure.Services.FileSystems {
path = ConvertToRelativeUriPath(path);
newPath = ConvertToRelativeUriPath(newPath);
if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(newPath))) {
throw new InvalidNameCharacterException("The new file name contains invalid character(s)");
}
Container.EnsureBlobExists(String.Concat(_root, path));
Container.EnsureBlobDoesNotExist(String.Concat(_root, newPath));
@@ -301,6 +316,10 @@ namespace Orchard.Azure.Services.FileSystems {
public IStorageFile CreateFile(string path) {
path = ConvertToRelativeUriPath(path);
if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(path))) {
throw new InvalidNameCharacterException("The file name contains invalid character(s)");
}
if (Container.BlobExists(String.Concat(_root, path))) {
throw new ArgumentException("File " + path + " already exists");
}
@@ -388,10 +407,7 @@ namespace Orchard.Azure.Services.FileSystems {
_rootPath = rootPath;
}
public string GetName() {
var path = GetPath();
return path.Substring(path.LastIndexOf('/') + 1);
}
public string GetName() => GetFolderName(GetPath());
public string GetPath() {
return _blob.Uri.ToString().Substring(_rootPath.Length).Trim('/');
@@ -416,11 +432,12 @@ namespace Orchard.Azure.Services.FileSystems {
long size = 0;
foreach (var blobItem in directoryBlob.ListBlobs()) {
if (blobItem is CloudBlockBlob)
size += ((CloudBlockBlob)blobItem).Properties.Length;
if (blobItem is CloudBlobDirectory)
size += GetDirectorySize((CloudBlobDirectory)blobItem);
if (blobItem is CloudBlockBlob blob) {
size += blob.Properties.Length;
}
else if (blobItem is CloudBlobDirectory directory) {
size += GetDirectorySize(directory);
}
}
return size;

View File

@@ -198,6 +198,13 @@ namespace Orchard.CustomForms.Controllers {
contentItem.As<ICommonPart>().Container = customForm.ContentItem;
// save the submitted form
if (customForm.SaveContentItem) {
conditionallyPublish(contentItem);
// Refresh content item
contentItem = _contentManager.Get(contentItem.Id, VersionOptions.Latest);
}
// triggers any event
_rulesManager.TriggerEvent("CustomForm", "Submitted",
() => new Dictionary<string, object> { { "Content", contentItem } });
@@ -210,11 +217,6 @@ namespace Orchard.CustomForms.Controllers {
returnUrl = _tokenizer.Replace(customForm.RedirectUrl, new Dictionary<string, object> { { "Content", contentItem } });
}
// save the submitted form
if (customForm.SaveContentItem) {
conditionallyPublish(contentItem);
}
// writes a confirmation message
if (customForm.CustomMessage) {
if (!String.IsNullOrWhiteSpace(customForm.Message)) {

View File

@@ -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());
}
}
}

View File

@@ -339,7 +339,6 @@
<Compile Include="Migrations.cs" />
<Compile Include="Handlers\ReadFormValuesHandler.cs" />
<Compile Include="Services\FormElementEventHandlerBase.cs" />
<Compile Include="Helpers\NameValueCollectionExtensions.cs" />
<Compile Include="Models\Submission.cs" />
<Compile Include="Permissions.cs" />
<Compile Include="Services\FormService.cs" />

View File

@@ -467,4 +467,4 @@ namespace Orchard.DynamicForms.Services {
return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType);
}
}
}
}

View File

@@ -4,46 +4,41 @@ using Orchard.ContentManagement.Handlers;
using Orchard.Fields.Fields;
using Orchard.Fields.Settings;
using Orchard.Localization;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Orchard.Fields.Drivers {
public class EnumerationFieldDriver : ContentFieldDriver<EnumerationField> {
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<EnumerationFieldSettings>();
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<EnumerationFieldSettings>();
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) {

View File

@@ -7,28 +7,16 @@ namespace Orchard.Fields.Fields {
private const char Separator = ';';
public string Value {
get { return Storage.Get<string>(); }
set { Storage.Set(value ?? String.Empty); }
get => Storage.Get<string>()?.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) : "";
}
}
}

View File

@@ -1,22 +1,27 @@
@model Orchard.Fields.Fields.EnumerationField
@using Orchard.Fields.Settings;
@{
var settings = Model.PartFieldDefinition.Settings.GetModel<EnumerationFieldSettings>();
string[] options = (!String.IsNullOrWhiteSpace(settings.Options)) ? settings.Options.Split(new string[] { System.Environment.NewLine }, StringSplitOptions.None) : new string[] { T("Select an option").ToString() };
}
<fieldset>
<label for="@Html.FieldIdFor(m => m.Value)" @if (settings.Required) { <text> class="required" </text> }>@Model.DisplayName</label>
<label for="@Html.FieldIdFor(m => m.Value)" @if (settings.Required) { <text> class="required" </text> }>@Model.DisplayName</label>
@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)) {
<label>@Html.RadioButton("Value", "", string.IsNullOrWhiteSpace(Model.Value), settings.Required ? new { required = "required" } : null)<i>@T("unset")</i></label> }
<label>@Html.RadioButton("Value", "", string.IsNullOrWhiteSpace(Model.SelectedValues.FirstOrDefault()), settings.Required ? new { required = "required" } : null)<i>@T("unset")</i></label>
}
else {
<label>@Html.RadioButton("Value", option, (option == Model.Value), settings.Required ? new { required = "required" } : null)@option</label> }
<label>@Html.RadioButton("Value", option, (option == Model.SelectedValues.FirstOrDefault()), settings.Required ? new { required = "required" } : null)@option</label>
}
}
break;

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -89,10 +89,11 @@
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminCultureSelectorController.cs" />
<Compile Include="Extensions\Constants.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\TransliterationAdminController.cs" />
<Compile Include="Controllers\AdminCultureSelectorController.cs" />
<Compile Include="Controllers\UserCultureSelectorController.cs" />
<Compile Include="Extensions\MetaDataExtensions.cs" />
<Compile Include="Handlers\LocalizationCultureNeutralityHandler.cs" />
<Compile Include="Models\TransliterationSpecificationRecord.cs" />
@@ -120,6 +121,7 @@
<Compile Include="Services\LocalizationService.cs" />
<Compile Include="Services\TransliterationService.cs" />
<Compile Include="Events\TransliterationSlugEventHandler.cs" />
<Compile Include="Services\Utils.cs" />
<Compile Include="Settings\LocalizationCultureNeutralityEditorEvents.cs" />
<Compile Include="Settings\LocalizationCultureNeutralitySettings.cs" />
<Compile Include="ViewModels\ContentLocalizationsViewModel.cs" />
@@ -196,8 +198,7 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\UserCultureSelector.cshtml" />
<Content Include="Views\DefinitionTemplates\LocalizationCultureNeutralitySettings.cshtml" />
</ItemGroup>
<PropertyGroup>
@@ -233,4 +234,4 @@
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
</Project>

View File

@@ -1,4 +1,3 @@
using System;
using System.Web;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
@@ -19,7 +18,8 @@ namespace Orchard.Localization.Selectors {
private const string AdminCookieName = "OrchardCurrentCulture-Admin";
private const int DefaultExpireTimeYear = 1;
public CookieCultureSelector(IHttpContextAccessor httpContextAccessor,
public CookieCultureSelector(
IHttpContextAccessor httpContextAccessor,
IClock clock,
ShellSettings shellSettings) {
_httpContextAccessor = httpContextAccessor;
@@ -36,11 +36,10 @@ namespace Orchard.Localization.Selectors {
var cookie = new HttpCookie(cookieName, culture) {
Expires = _clock.UtcNow.AddYears(DefaultExpireTimeYear),
Domain = httpContext.Request.IsLocal ? null : httpContext.Request.Url.Host
};
cookie.Domain = !httpContext.Request.IsLocal ? httpContext.Request.Url.Host : null;
if (!String.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) {
if (!string.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) {
cookie.Path = GetCookiePath(httpContext);
}
@@ -73,4 +72,4 @@ namespace Orchard.Localization.Selectors {
return cookiePath;
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Orchard.Autoroute.Models;
using Orchard.ContentManagement;
using Orchard.Localization.Models;
@@ -10,5 +11,7 @@ namespace Orchard.Localization.Services {
void SetContentCulture(IContent content, string culture);
IEnumerable<LocalizationPart> GetLocalizations(IContent content);
IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions);
bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute);
bool TryGetRouteForUrl(string url, out AutoroutePart route);
}
}

View File

@@ -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;
}
/// <summary>
/// Warning: Returns only the first item of same culture localizations.
/// </summary>
public LocalizationPart GetLocalizedContentItem(IContent content, string culture) =>
GetLocalizedContentItem(content, culture, null);
public LocalizationPart GetLocalizedContentItem(IContent content, string culture) {
return GetLocalizedContentItem(content, culture, null);
}
/// <summary>
/// Warning: Returns only the first item of same culture localizations.
/// </summary>
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<LocalizationPart>();
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<LocalizationPartRecord>(l => l.CultureId == cultureRecord.Id)
.Where<LocalizationPartRecord>(localization => localization.CultureId == cultureRecord.Id)
.Slice(1)
.FirstOrDefault();
}
public string GetContentCulture(IContent content) {
var localized = content.As<LocalizationPart>();
return localized?.Culture == null ? _cultureManager.GetSiteCulture() : localized.Culture.Culture;
}
public string GetContentCulture(IContent content) =>
content.As<LocalizationPart>()?.Culture?.Culture ?? _cultureManager.GetSiteCulture();
public void SetContentCulture(IContent content, string culture) {
var localized = content.As<LocalizationPart>();
@@ -51,30 +60,83 @@ namespace Orchard.Localization.Services {
localized.Culture = _cultureManager.GetCultureByName(culture);
}
public IEnumerable<LocalizationPart> GetLocalizations(IContent content) {
return GetLocalizations(content, null);
}
/// <summary>
/// Warning: May contain more than one localization of the same culture.
/// </summary>
public IEnumerable<LocalizationPart> GetLocalizations(IContent content) => GetLocalizations(content, null);
/// <summary>
/// Warning: May contain more than one localization of the same culture.
/// </summary>
public IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions) {
if (content.ContentItem.Id == 0) return Enumerable.Empty<LocalizationPart>();
var localized = content.As<LocalizationPart>();
return GetLocalizationsQuery(localized, versionOptions)
.Where<LocalizationPartRecord>(l => l.Id != localized.Id) // Exclude the current content.
.Where<LocalizationPartRecord>(localization => localization.Id != localized.Id) // Exclude the current content.
.List();
}
public bool TryGetRouteForUrl(string url, out AutoroutePart route) {
route = _contentManager.Query<AutoroutePart, AutoroutePartRecord>()
.ForVersion(VersionOptions.Published)
.Where(r => r.DisplayAlias == url)
.List()
.FirstOrDefault();
route = route ?? _homeAliasService.GetHomePage(VersionOptions.Latest).As<AutoroutePart>();
return route != null;
}
public bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute) {
if (!routableContent.Parts.Any(p => p.Is<ILocalizableAspect>())) {
localizedRoute = null;
return false;
}
IEnumerable<LocalizationPart> 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<AutoroutePart>();
return localizedRoute != null;
}
/// <summary>
/// Warning: May contain more than one localization of the same culture.
/// </summary>
private IContentQuery<LocalizationPart> 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 = versionOptions == null ?
_contentManager.Query<LocalizationPart>() : _contentManager.Query<LocalizationPart>(versionOptions);
var query = _contentManager.Query<LocalizationPart>(localizationPart.ContentItem.ContentType);
// Warning: May contain more than one localization of the same culture.
return query.Where<LocalizationPartRecord>(l => l.Id == masterId || l.MasterContentItemId == masterId);
if (versionOptions == null) {
query = query.ForVersion(versionOptions);
}
return query
.Where<LocalizationPartRecord>(localization => localization.Id == masterId || localization.MasterContentItemId == masterId);
}
}
}

View File

@@ -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, "");
}
}
}

View File

@@ -0,0 +1,34 @@
@using Orchard.Localization.Services
@{
var currentCulture = WorkContext.CurrentCulture;
var supportedCultures = WorkContext.Resolve<ICultureManager>().ListCultures().ToList();
}
<div id="culture-selection">
<ul>
@foreach (var supportedCulture in supportedCultures)
{
var url = Url.Action(
"ChangeCulture",
"UserCultureSelector",
new
{
area = "Orchard.Localization",
culture = supportedCulture,
returnUrl = Html.ViewContext.HttpContext.Request.RawUrl
});
<li>
@if (supportedCulture.Equals(currentCulture))
{
<a href="@url">@T("{0} (current)", supportedCulture)</a>
}
else
{
<a href="@url">@supportedCulture</a>
}
</li>
}
</ul>
</div>

View File

@@ -111,10 +111,16 @@ namespace Orchard.MediaLibrary.Controllers {
url = mediaPart.FileName,
});
}
catch (Exception ex) {
Logger.Error(ex, "Unexpected exception when uploading a media.");
catch (InvalidNameCharacterException) {
statuses.Add(new {
error = T(ex.Message).Text,
error = T("The file name contains invalid character(s)").Text,
progress = 1.0,
});
}
catch (Exception ex) {
Logger.Error(ex, T("Unexpected exception when uploading a media.").Text);
statuses.Add(new {
error = ex.Message,
progress = 1.0,
});
}
@@ -134,7 +140,7 @@ namespace Orchard.MediaLibrary.Controllers {
return HttpNotFound();
// Check permission
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath))
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath))
&& !_mediaLibraryService.CanManageMediaFolder(replaceMedia.FolderPath)) {
return new HttpUnauthorizedResult();
}
@@ -142,7 +148,7 @@ namespace Orchard.MediaLibrary.Controllers {
var statuses = new List<object>();
var settings = Services.WorkContext.CurrentSite.As<MediaLibrarySettingsPart>();
// Loop through each file in the request
for (int i = 0; i < HttpContext.Request.Files.Count; i++) {
// Pointer to file
@@ -150,7 +156,8 @@ namespace Orchard.MediaLibrary.Controllers {
var filename = Path.GetFileName(file.FileName);
// if the file has been pasted, provide a default name
if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase) && !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) {
if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase)
&& !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) {
filename = "clipboard.png";
}
@@ -180,13 +187,15 @@ namespace Orchard.MediaLibrary.Controllers {
if (mediaItemsUsingTheFile == 1) { // if the file is referenced only by the deleted media content, the file too can be removed.
try {
_mediaLibraryService.DeleteFile(replaceMedia.FolderPath, replaceMedia.FileName);
} catch (ArgumentException) { // File not found by FileSystemStorageProvider is thrown as ArgumentException.
}
catch (ArgumentException) { // File not found by FileSystemStorageProvider is thrown as ArgumentException.
statuses.Add(new {
error = T("Error when deleting file to replace: file {0} does not exist in folder {1}. Media has been updated anyway.", replaceMedia.FileName, replaceMedia.FolderPath).Text,
progress = 1.0
});
}
} else {
}
else {
// it changes the media file name
replaceMedia.FileName = filename;
}
@@ -210,7 +219,7 @@ namespace Orchard.MediaLibrary.Controllers {
});
}
catch (Exception ex) {
Logger.Error(ex, "Unexpected exception when uploading a media.");
Logger.Error(ex, T("Unexpected exception when uploading a media.").Text);
statuses.Add(new {
error = T(ex.Message).Text,

View File

@@ -1,9 +1,9 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.MediaLibrary.Models;
@@ -36,7 +36,7 @@ namespace Orchard.MediaLibrary.Controllers {
public ActionResult Create(string folderPath) {
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) {
Services.Notifier.Error(T("Couldn't create media folder"));
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath });
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath });
}
// If the user is trying to access a folder above his boundaries, redirect him to his home folder
@@ -68,28 +68,32 @@ namespace Orchard.MediaLibrary.Controllers {
return new HttpUnauthorizedResult();
}
var failed = false;
try {
bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$");
if (!valid) {
throw new ArgumentException(T("Folder contains invalid characters").ToString());
}
else {
_mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder created"));
}
_mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder created"));
}
catch (InvalidNameCharacterException) {
Services.Notifier.Error(T("The folder name contains invalid character(s)."));
failed = true;
}
catch (ArgumentException argumentException) {
Services.Notifier.Error(T("Creating Folder failed: {0}", argumentException.Message));
failed = true;
}
if (failed) {
Services.TransactionManager.Cancel();
return View(viewModel);
}
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary" });
}
public ActionResult Edit(string folderPath) {
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) {
Services.Notifier.Error(T("Couldn't edit media folder"));
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath });
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath });
}
if (!_mediaLibraryService.CanManageMediaFolder(folderPath)) {
@@ -125,7 +129,7 @@ namespace Orchard.MediaLibrary.Controllers {
var viewModel = new MediaManagerFolderEditViewModel();
UpdateModel(viewModel);
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath)
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath)
|| _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, viewModel.FolderPath))) {
return new HttpUnauthorizedResult();
}
@@ -136,14 +140,12 @@ namespace Orchard.MediaLibrary.Controllers {
}
try {
bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$");
if (!valid) {
throw new ArgumentException(T("Folder contains invalid characters").ToString());
}
else {
_mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder renamed"));
}
_mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder renamed"));
}
catch (InvalidNameCharacterException) {
Services.Notifier.Error(T("The folder name contains invalid character(s)."));
return View(viewModel);
}
catch (Exception exception) {
Services.Notifier.Error(T("Editing Folder failed: {0}", exception.Message));
@@ -198,7 +200,7 @@ namespace Orchard.MediaLibrary.Controllers {
// don't try to rename the file if there is no associated media file
if (!string.IsNullOrEmpty(media.FileName)) {
// check permission on source folder
if(!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) {
if (!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) {
return new HttpUnauthorizedResult();
}

View File

@@ -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<MediaPart> {
private readonly IAuthenticationService _authenticationService;
private readonly IAuthorizationService _authorizationService;
@@ -58,6 +58,8 @@ namespace Orchard.MediaLibrary.MediaFileName
var priorFileName = model.FileName;
if (updater.TryUpdateModel(model, Prefix, null, null)) {
if (model.FileName != null && !model.FileName.Equals(priorFileName, StringComparison.OrdinalIgnoreCase)) {
var fieldName = "MediaFileNameEditorSettings.FileName";
try {
_mediaLibraryService.RenameFile(part.FolderPath, priorFileName, model.FileName);
part.FileName = model.FileName;
@@ -65,10 +67,13 @@ namespace Orchard.MediaLibrary.MediaFileName
_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 (Exception) {
updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file"));
catch (InvalidNameCharacterException) {
updater.AddModelError(fieldName, T("The file name contains invalid character(s)."));
}
catch (Exception exception) {
updater.AddModelError(fieldName, T("Unable to rename file: {0}", exception.Message));
}
}
}

View File

@@ -6,13 +6,13 @@ using System.Web;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.Core.Common.Models;
using Orchard.Core.Title.Models;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.MediaLibrary.Factories;
using Orchard.MediaLibrary.Models;
using Orchard.Core.Title.Models;
using Orchard.Validation;
using Orchard.MediaLibrary.Providers;
using Orchard.Validation;
namespace Orchard.MediaLibrary.Services {
public class MediaLibraryService : IMediaLibraryService {
@@ -21,7 +21,6 @@ namespace Orchard.MediaLibrary.Services {
private readonly IStorageProvider _storageProvider;
private readonly IEnumerable<IMediaFactorySelector> _mediaFactorySelectors;
private readonly IMediaFolderProvider _mediaFolderProvider;
private static char[] HttpUnallowed = new char[] { '<', '>', '*', '%', '&', ':', '\\', '?', '#' };
public MediaLibraryService(
IOrchardServices orchardServices,
@@ -146,12 +145,6 @@ namespace Orchard.MediaLibrary.Services {
}
public string GetUniqueFilename(string folderPath, string filename) {
// remove any char which is unallowed in an HTTP request
foreach (var unallowedChar in HttpUnallowed) {
filename = filename.Replace(unallowedChar.ToString(), "");
}
// compute a unique filename
var uniqueFilename = filename;
var index = 1;
@@ -178,9 +171,9 @@ namespace Orchard.MediaLibrary.Services {
var mediaFile = BuildMediaFile(relativePath, storageFile);
using (var stream = storageFile.OpenRead()) {
var mediaFactory = GetMediaFactory(stream, mimeType, contentType);
if (mediaFactory == null)
throw new Exception(T("No media factory available to handle this resource.").Text);
var mediaFactory = GetMediaFactory(stream, mimeType, contentType)
?? throw new Exception(T("No media factory available to handle this resource.").Text);
var mediaPart = mediaFactory.CreateMedia(stream, mediaFile.Name, mimeType, contentType);
if (mediaPart != null) {
mediaPart.FolderPath = relativePath;
@@ -257,7 +250,7 @@ namespace Orchard.MediaLibrary.Services {
if (_orchardServices.Authorizer.Authorize(Permissions.ManageMediaContent)) {
return true;
}
if (_orchardServices.WorkContext.CurrentUser==null)
if (_orchardServices.WorkContext.CurrentUser == null)
return _orchardServices.Authorizer.Authorize(permission);
// determines the folder type: public, user own folder (my), folder of another user (private)
var rootedFolderPath = this.GetRootedFolderPath(folderPath) ?? "";
@@ -269,7 +262,7 @@ namespace Orchard.MediaLibrary.Services {
isMyfolder = true;
}
if(isMyfolder) {
if (isMyfolder) {
return _orchardServices.Authorizer.Authorize(Permissions.ManageOwnMedia);
}
else { // other

View File

@@ -0,0 +1,5 @@
namespace Orchard.MediaProcessing {
public static class Features {
public const string OrchardMediaProcessingHtmlFilter = "Orchard.MediaProcessingHtmlFilter";
}
}

View File

@@ -88,7 +88,7 @@ namespace Orchard.MediaProcessing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult();
var viewModel = new AdminIndexViewModel {ImageProfiles = new List<ImageProfileEntry>(), Options = new AdminIndexOptions()};
var viewModel = new AdminIndexViewModel { ImageProfiles = new List<ImageProfileEntry>(), Options = new AdminIndexOptions() };
UpdateModel(viewModel);
var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked);
@@ -133,7 +133,7 @@ namespace Orchard.MediaProcessing.Controllers {
Category = f.Category,
Type = f.Type,
FilterRecordId = filter.Id,
DisplayText = String.IsNullOrWhiteSpace(filter.Description) ? f.Display(new FilterContext {State = FormParametersHelper.ToDynamic(filter.State)}).Text : filter.Description
DisplayText = String.IsNullOrWhiteSpace(filter.Description) ? f.Display(new FilterContext { State = FormParametersHelper.ToDynamic(filter.State) }).Text : filter.Description
});
}
}
@@ -154,7 +154,7 @@ namespace Orchard.MediaProcessing.Controllers {
return HttpNotFound();
}
Services.ContentManager.Remove(profile.ContentItem);
_profileService.DeleteImageProfile(id);
Services.Notifier.Success(T("Image Profile {0} deleted", profile.Name));
return RedirectToAction("Index");
@@ -175,7 +175,7 @@ namespace Orchard.MediaProcessing.Controllers {
throw new ArgumentException("direction");
}
return RedirectToAction("Edit", new {id});
return RedirectToAction("Edit", new { id });
}
public ActionResult Preview(int id) {
@@ -185,6 +185,36 @@ namespace Orchard.MediaProcessing.Controllers {
throw new NotImplementedException();
}
[HttpPost]
public ActionResult Purge(int id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult();
if (_profileService.PurgeImageProfile(id)) {
Services.Notifier.Information(T("The Image Profile has been purged"));
}
else {
Services.Notifier.Warning(T("Unable to purge the Image Profile, it may already have been purged"));
}
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult PurgeObsolete() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles")))
return new HttpUnauthorizedResult();
if (_profileService.PurgeObsoleteImageProfiles()) {
Services.Notifier.Information(T("The obsolete Image Profiles have been purged"));
}
else {
Services.Notifier.Warning(T("Unable to purge the obsolete Image Profiles"));
}
return RedirectToAction("Index");
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;
using Orchard.Forms.Services;
using Orchard.Logging;
using Orchard.MediaProcessing.Models;
using Orchard.MediaProcessing.Services;
using Orchard.Services;
namespace Orchard.MediaProcessing.Filters {
/// <summary>
/// Resizes any images in HTML provided by parts that support IHtmlFilter and sets an alt text if not already supplied.
/// </summary>
[OrchardFeature(Features.OrchardMediaProcessingHtmlFilter)]
public class MediaProcessingHtmlFilter : IHtmlFilter {
private readonly IWorkContextAccessor _wca;
private readonly IImageProfileManager _profileManager;
private MediaHtmlFilterSettingsPart _settingsPart;
private static readonly Regex _imageTagRegex = new Regex(@"<img\b[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly ConcurrentDictionary<string, Regex> _attributeRegexes = new ConcurrentDictionary<string, Regex>();
private static readonly ConcurrentDictionary<string, string> _attributeValues = new ConcurrentDictionary<string, string>();
private static readonly Dictionary<string, string> _validExtensions = new Dictionary<string, string> {
{ ".jpeg", "jpg" }, // For example: .jpeg supports compression (quality), format to 'jpg'.
{ ".jpg", "jpg" },
{ ".png", null }
};
public MediaProcessingHtmlFilter(IWorkContextAccessor wca, IImageProfileManager profileManager) {
_profileManager = profileManager;
_wca = wca;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public MediaHtmlFilterSettingsPart Settings {
get {
if (_settingsPart == null) {
_settingsPart = _wca.GetContext().CurrentSite.As<MediaHtmlFilterSettingsPart>();
}
return _settingsPart;
}
}
public string ProcessContent(string text, HtmlFilterContext context) {
if (string.IsNullOrWhiteSpace(text) || context.Flavor != "html") {
return text;
}
var matches = _imageTagRegex.Matches(text);
if (matches.Count == 0) {
return text;
}
var offset = 0; // This tracks where last image tag ended in the original HTML.
var newText = new StringBuilder();
foreach (Match match in matches) {
newText.Append(text.Substring(offset, match.Index - offset));
offset = match.Index + match.Length;
var imgTag = match.Value;
var processedImgTag = ProcessImageContent(imgTag);
if (Settings.PopulateAlt) {
processedImgTag = ProcessImageAltContent(processedImgTag);
}
newText.Append(processedImgTag);
}
newText.Append(text.Substring(offset));
return newText.ToString();
}
private string ProcessImageContent(string imgTag) {
if (imgTag.Contains("noresize")) {
return imgTag;
}
var src = GetAttributeValue(imgTag, "src");
var ext = string.IsNullOrEmpty(src) ? null : Path.GetExtension(src).ToLowerInvariant();
var width = GetAttributeValueInt(imgTag, "width");
var height = GetAttributeValueInt(imgTag, "height");
if (width > 0 && height > 0
&& !string.IsNullOrEmpty(src)
&& !src.Contains("_Profiles")
&& _validExtensions.ContainsKey(ext)) {
try {
// If the image has a combination of width, height and valid extension, that is not already in
// _Profiles, then process the image.
var newSrc = TryGetImageProfilePath(src, ext, width, height);
imgTag = SetAttributeValue(imgTag, "src", newSrc);
}
catch (Exception ex) {
Logger.Error(ex, "Unable to process Html Dynamic image profile for '{0}'", src);
}
}
return imgTag;
}
private string TryGetImageProfilePath(string src, string ext, int width, int height) {
var filters = new List<FilterRecord> {
// Factor in a minimum width and height with respect to higher pixel density devices.
CreateResizeFilter(width * Settings.DensityThreshold, height * Settings.DensityThreshold)
};
if (_validExtensions[ext] != null && Settings.Quality < 100) {
filters.Add(CreateFormatFilter(Settings.Quality, _validExtensions[ext]));
}
var profileName = string.Format(
"Transform_Resize_w_{0}_h_{1}_m_Stretch_a_MiddleCenter_c_{2}_d_@{3}x",
width,
height,
Settings.Quality,
Settings.DensityThreshold);
return _profileManager.GetImageProfileUrl(src, profileName, null, filters.ToArray());
}
private FilterRecord CreateResizeFilter(int width, int height) {
// Because the images can be resized in the HTML editor, we must assume that the image is of the exact desired
// dimensions and that stretch is an appropriate mode. Note that the default is to never upscale images.
var state = new Dictionary<string, string> {
{ "Width", width.ToString() },
{ "Height", height.ToString() },
{ "Mode", "Stretch" },
{ "Alignment", "MiddleCenter" },
{ "PadColor", "" }
};
return new FilterRecord {
Category = "Transform",
Type = "Resize",
State = FormParametersHelper.ToString(state)
};
}
private FilterRecord CreateFormatFilter(int quality, string format) {
var state = new Dictionary<string, string> {
{ "Quality", quality.ToString() },
{ "Format", format },
};
return new FilterRecord {
Category = "Transform",
Type = "Format",
State = FormParametersHelper.ToString(state)
};
}
private string ProcessImageAltContent(string imgTag) {
var src = GetAttributeValue(imgTag, "src");
var alt = GetAttributeValue(imgTag, "alt");
if (string.IsNullOrEmpty(alt) && !string.IsNullOrEmpty(src)) {
var text = Path.GetFileNameWithoutExtension(src).Replace("-", " ").Replace("_", " ");
imgTag = SetAttributeValue(imgTag, "alt", text);
}
return imgTag;
}
private string GetAttributeValue(string tag, string attributeName) =>
_attributeValues
.GetOrAdd($"{tag}_{attributeName}", _ => {
var match = GetAttributeRegex(attributeName).Match(tag);
return match.Success ? match.Groups[1].Value : null;
});
private int GetAttributeValueInt(string tag, string attributeName) =>
int.TryParse(GetAttributeValue(tag, attributeName), out int result) ? result : 0;
private string SetAttributeValue(string tag, string attributeName, string value) {
var attributeRegex = GetAttributeRegex(attributeName);
var newAttribute = $"{attributeName}=\"{value}\"";
if (attributeRegex.IsMatch(tag)) {
return attributeRegex.Replace(tag, newAttribute);
}
else {
return tag.Insert(tag.Length - 1, $" {newAttribute}");
}
}
private Regex GetAttributeRegex(string attributeName) =>
_attributeRegexes.GetOrAdd(
attributeName,
name => new Regex($@"\b{name}\s*=\s*[""']?([^""'\s>]*)[""']?", RegexOptions.IgnoreCase | RegexOptions.Compiled));
}
}

View File

@@ -0,0 +1,29 @@
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
using Orchard.Localization;
using Orchard.MediaProcessing.Models;
namespace Orchard.MediaProcessing.Handlers {
[OrchardFeature(Features.OrchardMediaProcessingHtmlFilter)]
public class MediaHtmlFilterSettingsPartHandler : ContentHandler {
public MediaHtmlFilterSettingsPartHandler() {
T = NullLocalizer.Instance;
Filters.Add(new ActivatingFilter<MediaHtmlFilterSettingsPart>("Site"));
Filters.Add(new TemplateFilterForPart<MediaHtmlFilterSettingsPart>(
"MediaHtmlFilterSettings",
"Parts.MediaProcessing.MediaHtmlFilterSettings",
"media"));
}
public Localizer T { get; set; }
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site") return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Media")));
}
}
}

View File

@@ -24,4 +24,4 @@ namespace Orchard.MediaProcessing.Models {
get { return Record.FileNames; }
}
}
}
}

View File

@@ -0,0 +1,20 @@
using Orchard.ContentManagement;
namespace Orchard.MediaProcessing.Models {
public class MediaHtmlFilterSettingsPart : ContentPart {
public int DensityThreshold {
get { return this.Retrieve(x => x.DensityThreshold, 2); }
set { this.Store(x => x.DensityThreshold, value); }
}
public int Quality {
get { return this.Retrieve(x => x.Quality, 95); }
set { this.Store(x => x.Quality, value); }
}
public bool PopulateAlt {
get { return this.Retrieve(x => x.PopulateAlt, true); }
set { this.Store(x => x.PopulateAlt, value); }
}
}
}

View File

@@ -6,4 +6,10 @@ Version: 1.10.3
OrchardVersion: 1.10.3
Description: Module for processing Media e.g. image resizing
Category: Media
Dependencies: Orchard.Forms
Dependencies: Orchard.Forms
Features:
Orchard.MediaProcessingHtmlFilter:
Name: Media Processing HTML Filter
Description: Dynamically resizes images to their height and width attributes
Category: Media
Dependencies: Orchard.MediaProcessing

View File

@@ -102,6 +102,10 @@
<Content Include="Web.config" />
<Content Include="Scripts\Web.config" />
<Content Include="Styles\Web.config" />
<Compile Include="Constants.cs" />
<Compile Include="Filters\MediaProcessingHtmlFilter.cs" />
<Compile Include="Handlers\MediaHtmlFilterSettingsPartHandler.cs" />
<Compile Include="Models\MediaHtmlFilterSettingsPart.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
@@ -192,6 +196,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Parts.MediaProcessing.MediaHtmlFilterSettings.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -10,5 +10,12 @@ namespace Orchard.MediaProcessing.Services {
void DeleteImageProfile(int id);
void MoveUp(int filterId);
void MoveDown(int filterId);
bool PurgeImageProfile(int id);
bool PurgeObsoleteImageProfiles();
}
}
public static class ImageProfileServiceExtensions {
public static string GetNameHashCode(this IImageProfileService service, string name) =>
name.GetHashCode().ToString("x").ToLowerInvariant();
}
}

View File

@@ -66,7 +66,7 @@ namespace Orchard.MediaProcessing.Services {
// path is the publicUrl of the media, so it might contain url-encoded chars
// try to load the processed filename from cache
var filePath = _fileNameProvider.GetFileName(profileName, System.Web.HttpUtility.UrlDecode(path));
var filePath = _fileNameProvider.GetFileName(profileName, HttpUtility.UrlDecode(path));
bool process = false;
// Before checking everything else, ensure that the content item that needs to be processed has a ImagePart.
@@ -79,10 +79,10 @@ namespace Orchard.MediaProcessing.Services {
if (checkForProfile) {
//after reboot the app cache is empty so we reload the image in the cache if it exists in the _Profiles folder
if (string.IsNullOrEmpty(filePath)) {
var profileFilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, System.Web.HttpUtility.UrlDecode(path)));
var profileFilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, HttpUtility.UrlDecode(path)));
if (_storageProvider.FileExists(profileFilePath)) {
_fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), profileFilePath);
_fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), profileFilePath);
filePath = profileFilePath;
}
}
@@ -93,28 +93,24 @@ namespace Orchard.MediaProcessing.Services {
process = true;
}
// the processd file doesn't exist anymore, process it
else if (!_storageProvider.FileExists(filePath)) {
Logger.Debug("Processed file no longer exists, processing required, profile {0} for image {1}", profileName, path);
process = true;
}
// if the original file is more recent, process it
else {
DateTime pathLastUpdated;
if (TryGetImageLastUpdated(path, out pathLastUpdated)) {
var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated();
else if (TryGetImageLastUpdated(path, out DateTime pathLastUpdated)) {
var filePathLastUpdated = _storageProvider.GetFile(filePath).GetLastUpdated();
if (pathLastUpdated > filePathLastUpdated) {
Logger.Debug("Original file more recent, processing required, profile {0} for image {1}", profileName, path);
if (pathLastUpdated > filePathLastUpdated) {
Logger.Debug("Original file more recent, processing required, profile {0} for image {1}", profileName, path);
process = true;
}
process = true;
}
}
} else {
}
else {
// Since media with no ImagePart have no profile, filePath is null, so it's set again to its original path on the storage provider.
if (string.IsNullOrWhiteSpace(filePath)) {
filePath = _storageProvider.GetStoragePath(path);
@@ -129,9 +125,11 @@ namespace Orchard.MediaProcessing.Services {
if (customFilters == null || !customFilters.Any(c => c != null)) {
profilePart = _profileService.GetImageProfileByName(profileName);
if (profilePart == null)
return String.Empty;
} else {
if (profilePart == null) {
return string.Empty;
}
}
else {
profilePart = _services.ContentManager.New<ImageProfilePart>("ImageProfile");
profilePart.Name = profileName;
foreach (var customFilter in customFilters) {
@@ -142,13 +140,13 @@ namespace Orchard.MediaProcessing.Services {
// prevent two requests from processing the same file at the same time
// this is only thread safe at the machine level, so there is a try/catch later
// to handle cross machines concurrency
lock (String.Intern(path)) {
lock (string.Intern(path)) {
using (var image = GetImage(path)) {
if (image == null) {
return null;
}
var filterContext = new FilterContext { Media = image, FilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, System.Web.HttpUtility.UrlDecode(path))) };
var filterContext = new FilterContext { Media = image, FilePath = _storageProvider.Combine("_Profiles", FormatProfilePath(profileName, HttpUtility.UrlDecode(path))) };
var tokens = new Dictionary<string, object>();
// if a content item is provided, use it while tokenizing
@@ -166,7 +164,7 @@ namespace Orchard.MediaProcessing.Services {
descriptor.Filter(filterContext);
}
_fileNameProvider.UpdateFileName(profileName, System.Web.HttpUtility.UrlDecode(path), filterContext.FilePath);
_fileNameProvider.UpdateFileName(profileName, HttpUtility.UrlDecode(path), filterContext.FilePath);
if (!filterContext.Saved) {
try {
@@ -187,7 +185,8 @@ namespace Orchard.MediaProcessing.Services {
// the storage provider may have altered the filepath
filterContext.FilePath = newFile.GetPath();
}
} catch (Exception e) {
}
catch (Exception e) {
Logger.Error(e, "A profile could not be processed: " + path);
}
}
@@ -215,14 +214,14 @@ namespace Orchard.MediaProcessing.Services {
try {
var file = _storageProvider.GetFile(storagePath);
return file.OpenRead();
} catch (Exception e) {
}
catch (Exception e) {
Logger.Error(e, "path:" + path + " storagePath:" + storagePath);
}
}
// http://blob.storage-provider.net/my-image.jpg
Uri absoluteUri;
if (Uri.TryCreate(path, UriKind.Absolute, out absoluteUri)) {
if (Uri.TryCreate(path, UriKind.Absolute, out Uri absoluteUri)) {
return new WebClient().OpenRead(absoluteUri);
}
@@ -248,13 +247,12 @@ namespace Orchard.MediaProcessing.Services {
}
private string FormatProfilePath(string profileName, string path) {
var filenameWithExtension = Path.GetFileName(path) ?? "";
var fileLocation = path.Substring(0, path.Length - filenameWithExtension.Length);
return _storageProvider.Combine(
_storageProvider.Combine(profileName.GetHashCode().ToString("x").ToLowerInvariant(), fileLocation.GetHashCode().ToString("x").ToLowerInvariant()),
filenameWithExtension);
_storageProvider.Combine(_profileService.GetNameHashCode(profileName), _profileService.GetNameHashCode(fileLocation)),
filenameWithExtension);
}
}
}

View File

@@ -1,31 +1,34 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Localization;
using Orchard.FileSystems.Media;
using Orchard.Logging;
using Orchard.MediaProcessing.Models;
namespace Orchard.MediaProcessing.Services {
public class ImageProfileService : IImageProfileService {
public class ImageProfileService : Component, IImageProfileService {
private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager;
private readonly IRepository<FilterRecord> _filterRepository;
private readonly ISignals _signals;
private readonly IStorageProvider _storageProvider;
public ImageProfileService(
IContentManager contentManager,
IContentManager contentManager,
ICacheManager cacheManager,
IRepository<FilterRecord> filterRepository,
ISignals signals) {
ISignals signals,
IStorageProvider storageProvider) {
_contentManager = contentManager;
_cacheManager = cacheManager;
_filterRepository = filterRepository;
_signals = signals;
_storageProvider = storageProvider;
}
public Localizer T { get; set; }
public ImageProfilePart GetImageProfile(int id) {
return _contentManager.Get<ImageProfilePart>(id);
}
@@ -70,6 +73,7 @@ namespace Orchard.MediaProcessing.Services {
var profile = _contentManager.Get(id);
if (profile != null) {
DeleteImageProfileFolder(profile.As<ImageProfilePart>().Name);
_contentManager.Remove(profile);
}
}
@@ -115,5 +119,43 @@ namespace Orchard.MediaProcessing.Services {
next.Position = filter.Position;
filter.Position = temp;
}
public bool PurgeImageProfile(int id) {
var profile = GetImageProfile(id);
try {
DeleteImageProfileFolder(profile.Name);
profile.FileNames.Clear();
_signals.Trigger("MediaProcessing_Saved_" + profile.Name);
return true;
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to purge image profile '{0}'", profile.Name);
return false;
}
}
public bool PurgeObsoleteImageProfiles() {
var profiles = GetAllImageProfiles();
try {
if (profiles != null) {
var validPaths = profiles.Select(profile => _storageProvider.Combine("_Profiles", this.GetNameHashCode(profile.Name)));
foreach (var folder in _storageProvider.ListFolders("_Profiles").Select(f => f.GetPath())) {
if (!validPaths.Any(folder.StartsWith)) {
_storageProvider.DeleteFolder(folder);
}
}
}
return true;
}
catch (Exception ex) {
Logger.Warning(ex, "Unable to purge obsolete image profiles");
return false;
}
}
private void DeleteImageProfileFolder(string profileName) {
var folder = _storageProvider.Combine("_Profiles", this.GetNameHashCode(profileName));
_storageProvider.DeleteFolder(folder);
}
}
}
}

View File

@@ -14,7 +14,10 @@
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div class="manage">@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })</div>
<div class="manage">
@Html.ActionLink(T("Purge Obsolete").ToString(), "PurgeObsolete", null, new { itemprop = "UnsafeUrl", @class = "button remove", data_unsafe_url = @T("Are you sure you wish to purge all obsolete profile images and force all dynamic profile images to be regenerated?").ToString() })
@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })
</div>
<fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label>
@@ -56,6 +59,7 @@
<td>
@Html.ActionLink(T("Properties").ToString(), "Edit", new { Area = "Contents", id = entry.ImageProfileId, returnurl = HttpContext.Current.Request.RawUrl }) |
@Html.ActionLink(T("Edit").ToString(), "Edit", new { id = entry.ImageProfileId }) |
@Html.ActionLink(T("Purge").ToString(), "Purge", new { id = entry.ImageProfileId }, new { itemprop = "UnsafeUrl", data_unsafe_url = @T("Are you sure you wish to purge all images for this profile?").ToString() }) |
@Html.ActionLink(T("Delete").ToString(), "Delete", new { id = entry.ImageProfileId }, new { itemprop = "RemoveUrl UnsafeUrl" })
@*@Html.ActionLink(T("Preview").ToString(), "Preview", new { id = entry.ImageProfileId })*@
</td>

View File

@@ -0,0 +1,25 @@
@model Orchard.MediaProcessing.Models.MediaHtmlFilterSettingsPart
<fieldset>
<legend>@T("Media Processing Filter")</legend>
<div>
@Html.LabelFor(m => m.DensityThreshold, T("Density Threshold"))
@Html.DropDownListFor(m => m.DensityThreshold, new List<SelectListItem>(new[] {
new SelectListItem { Value = "1", Text = "@1x" },
new SelectListItem { Value = "2", Text = "@2x" },
new SelectListItem { Value = "3", Text = "@3x" },
new SelectListItem { Value = "4", Text = "@4x" }
}))
<span class="hint">@T("The image will only be reduced if at least this pixel density is maintained.")</span>
</div>
<div>
@Html.LabelFor(m => m.Quality, T("JPEG Quality"))
@Html.TextBoxFor(i => i.Quality, new { @type = "number", @min = "0", @max = "100" }) %
<span class="hint">@T("The quality level to apply on JPEG's, where 100 is full-quality (no compression).")</span>
</div>
<div>
@Html.CheckBoxFor(i => i.PopulateAlt)
@Html.LabelFor(m => m.PopulateAlt, T("Populate Empty Alt Tags").Text, new { @class = "forcheckbox" })
<span class="hint">@T("Populate an Alt tag based on the file name if the Alt tag is empty")</span>
</div>
</fieldset>

View File

@@ -1,5 +1,5 @@
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Routing;
using Orchard.ContentManagement.MetaData;
using Orchard.Environment.Extensions;
using Orchard.Localization.Services;
@@ -25,10 +25,9 @@ namespace Orchard.Taxonomies.Controllers {
}
[OutputCache(NoStore = true, Duration = 0)]
public new ActionResult GetTaxonomy(string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
public override ActionResult GetTaxonomy(string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
AdminFilter.Apply(_requestContext);
return GetTaxonomyInternal(contentTypeName, taxonomyFieldName, contentId, culture, selectedValues);
return base.GetTaxonomy(contentTypeName, taxonomyFieldName, contentId, culture, selectedValues);
}
}
}
}

View File

@@ -35,11 +35,11 @@ namespace Orchard.Taxonomies.Controllers {
}
[OutputCache(NoStore = true, Duration = 0)]
public ActionResult GetTaxonomy(string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
public virtual ActionResult GetTaxonomy(string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
return GetTaxonomyInternal(contentTypeName, taxonomyFieldName, contentId, culture, selectedValues);
}
protected ActionResult GetTaxonomyInternal (string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
protected ActionResult GetTaxonomyInternal(string contentTypeName, string taxonomyFieldName, int contentId, string culture, string selectedValues) {
var viewModel = new TaxonomyFieldViewModel();
bool autocomplete = false;
var contentDefinition = _contentDefinitionManager.GetTypeDefinition(contentTypeName);

View File

@@ -34,7 +34,7 @@ namespace Orchard.Exceptions {
return false;
}
if (sender is IEventBus && exception is OrchardFatalException) {
if (sender is IEventBus && exception is OrchardFatalException) {
return false;
}
@@ -49,7 +49,7 @@ namespace Orchard.Exceptions {
}
private static bool IsFatal(Exception exception) {
return
return
exception is OrchardSecurityException ||
exception is StackOverflowException ||
exception is AccessViolationException ||

View File

@@ -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 {

View File

@@ -4,15 +4,22 @@ using System.IO;
using System.Linq;
using System.Web.Hosting;
using Orchard.Environment.Configuration;
using Orchard.Localization;
using Orchard.Validation;
using Orchard.Exceptions;
using Orchard.Localization;
using Orchard.Utility.Extensions;
using Orchard.Validation;
namespace Orchard.FileSystems.Media {
public class FileSystemStorageProvider : IStorageProvider {
private readonly string _storagePath; // c:\orchard\media\default
private readonly string _virtualPath; // ~/Media/Default/
private readonly string _publicPath; // /Orchard/Media/Default/
public static readonly char[] HttpUnallowedCharacters =
new char[] { '<', '>', '*', '%', '&', ':', '\\', '/', '?', '#', '"', '{', '}', '|', '^', '[', ']', '`' };
public static readonly char[] InvalidFolderNameCharacters =
Path.GetInvalidPathChars().Union(HttpUnallowedCharacters).ToArray();
public static readonly char[] InvalidFileNameCharacters =
Path.GetInvalidFileNameChars().Union(HttpUnallowedCharacters).ToArray();
public FileSystemStorageProvider(ShellSettings settings) {
var mediaPath = HostingEnvironment.IsHosted
@@ -27,7 +34,7 @@ namespace Orchard.FileSystems.Media {
appPath = HostingEnvironment.ApplicationVirtualPath;
}
if (!appPath.EndsWith("/"))
appPath = appPath + '/';
appPath += '/';
if (!appPath.StartsWith("/"))
appPath = '/' + appPath;
@@ -39,21 +46,21 @@ namespace Orchard.FileSystems.Media {
public Localizer T { get; set; }
public int MaxPathLength {
get; set;
// The public setter allows injecting this from Sites.MyTenant.Config or Sites.config, by using
// an AutoFac component:
/*
<component instance-scope="per-lifetime-scope"
type="Orchard.FileSystems.Media.FileSystemStorageProvider, Orchard.Framework"
service="Orchard.FileSystems.Media.IStorageProvider">
/// <summary>
/// The public setter allows injecting this from Sites.MyTenant.Config or Sites.config, by using an AutoFac
/// component. See the example below.
/// </summary>
/*
<component
instance-scope="per-lifetime-scope"
service="Orchard.FileSystems.Media.IStorageProvider"
type="Orchard.FileSystems.Media.FileSystemStorageProvider, Orchard.Framework">
<properties>
<property name="MaxPathLength" value="500" />
</properties>
</component>
*/
}
*/
public int MaxPathLength { get; set; }
/// <summary>
/// Maps a relative path into the storage path.
@@ -215,6 +222,12 @@ namespace Orchard.FileSystems.Media {
/// <param name="path">The relative path to the folder to be created.</param>
/// <exception cref="ArgumentException">If the folder already exists.</exception>
public void CreateFolder(string path) {
// We are dealing with a folder here, but GetFileName returns the last path segment, which in this case is
// the folder name.
if (FolderNameContainsInvalidCharacters(Path.GetFileName(path))) {
throw new InvalidNameCharacterException(T("The directory name contains invalid character(s)").ToString());
}
DirectoryInfo directoryInfo = new DirectoryInfo(MapStorage(path));
if (directoryInfo.Exists) {
throw new ArgumentException(T("Directory {0} already exists", path).ToString());
@@ -248,6 +261,12 @@ namespace Orchard.FileSystems.Media {
throw new ArgumentException(T("Directory {0} does not exist", oldPath).ToString());
}
// We are dealing with a folder here, but GetFileName returns the last path segment, which in this case is
// the folder name.
if (FolderNameContainsInvalidCharacters(Path.GetFileName(newPath))) {
throw new InvalidNameCharacterException(T("The new directory name contains invalid character(s)").ToString());
}
DirectoryInfo targetDirectory = new DirectoryInfo(MapStorage(newPath));
if (targetDirectory.Exists) {
throw new ArgumentException(T("Directory {0} already exists", newPath).ToString());
@@ -313,6 +332,10 @@ namespace Orchard.FileSystems.Media {
throw new ArgumentException(T("File {0} does not exist", oldPath).ToString());
}
if (FileNameContainsInvalidCharacters(Path.GetFileName(newPath))) {
throw new InvalidNameCharacterException(T("The new file name contains invalid character(s)").ToString());
}
FileInfo targetFileInfo = new FileInfo(MapStorage(newPath));
if (targetFileInfo.Exists) {
throw new ArgumentException(T("File {0} already exists", newPath).ToString());
@@ -342,6 +365,10 @@ namespace Orchard.FileSystems.Media {
/// <exception cref="ArgumentException">If the file already exists.</exception>
/// <returns>The created file.</returns>
public IStorageFile CreateFile(string path) {
if (FileNameContainsInvalidCharacters(Path.GetFileName(path))) {
throw new InvalidNameCharacterException(T("The file name contains invalid character(s)").ToString());
}
FileInfo fileInfo = new FileInfo(MapStorage(path));
if (fileInfo.Exists) {
throw new ArgumentException(T("File {0} already exists", fileInfo.Name).ToString());
@@ -427,6 +454,12 @@ namespace Orchard.FileSystems.Media {
return (di.Attributes & FileAttributes.Hidden) != 0;
}
public static bool FolderNameContainsInvalidCharacters(string folderName) =>
folderName.IndexOfAny(InvalidFolderNameCharacters) > -1;
public static bool FileNameContainsInvalidCharacters(string fileName) =>
fileName.IndexOfAny(InvalidFileNameCharacters) > -1;
#endregion
private class FileSystemStorageFile : IStorageFile {

View File

@@ -128,7 +128,7 @@ namespace Orchard.FileSystems.Media {
void SaveStream(string path, Stream inputStream);
/// <summary>
/// Combines to paths.
/// Combines two paths.
/// </summary>
/// <param name="path1">The parent path.</param>
/// <param name="path2">The child path.</param>

View File

@@ -0,0 +1,7 @@
using System;
namespace Orchard.FileSystems.Media {
public class InvalidNameCharacterException : ArgumentException {
public InvalidNameCharacterException(string message) : base(message) { }
}
}

View File

@@ -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 {

View File

@@ -45,7 +45,6 @@
<CodeAnalysisRuleSet>..\OrchardBasicCorrectness.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<UseVSHostingProcess>false</UseVSHostingProcess>
<NoWarn></NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@@ -169,6 +168,7 @@
<Compile Include="Data\Migration\Schema\DropUniqueConstraintCommand.cs" />
<Compile Include="Environment\Extensions\Models\LifecycleStatus.cs" />
<Compile Include="Environment\ShellBuilders\ICompositionStrategy.cs" />
<Compile Include="FileSystems\Media\InvalidNameCharacterException.cs" />
<Compile Include="Mvc\ModelBinders\BooleanBinderProvider.cs" />
<Compile Include="Localization\FormatForScope.cs" />
<Compile Include="Locking\ILockingProvider.cs" />
@@ -718,6 +718,7 @@
<Compile Include="Messaging\Services\IMessageManager.cs" />
<Compile Include="Messaging\Services\IMessagingChannel.cs" />
<Compile Include="IWorkContextAccessor.cs" />
<Compile Include="Utility\Extensions\NameValueCollectionExtensions.cs" />
<Compile Include="Utility\Extensions\VirtualPathProviderExtensions.cs" />
<Compile Include="Utility\NamedReaderWriterLock.cs" />
<Compile Include="Utility\ReflectionHelper.cs" />

View File

@@ -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());
}
}