diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml
index 444b4e69c5..3d26736938 100644
--- a/src/Umbraco.Core/CompatibilitySuppressions.xml
+++ b/src/Umbraco.Core/CompatibilitySuppressions.xml
@@ -85,6 +85,27 @@
lib/net7.0/Umbraco.Core.dll
true
+
+ CP0001
+ T:Umbraco.Cms.Core.Models.ContentEditing.IHaveUploadedFiles
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0001
+ T:Umbraco.Cms.Core.Models.ContentEditing.PostedFiles
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0001
+ T:Umbraco.Cms.Core.Models.Editors.ContentPropertyFile
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
CP0002
M:Umbraco.Cms.Core.Configuration.Grid.GridConfig.#ctor(Umbraco.Cms.Core.Cache.AppCaches,Umbraco.Cms.Core.Manifest.IManifestParser,Umbraco.Cms.Core.Serialization.IJsonSerializer,Umbraco.Cms.Core.Hosting.IHostingEnvironment,Microsoft.Extensions.Logging.ILoggerFactory,Umbraco.Cms.Core.IO.IGridEditorsConfigFileProviderFactory)
@@ -316,6 +337,20 @@
lib/net7.0/Umbraco.Core.dll
true
+
+ CP0002
+ M:Umbraco.Cms.Core.Models.ContentEditing.ContentBaseSave`1.get_UploadedFiles
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0002
+ M:Umbraco.Cms.Core.Models.ContentEditing.ContentItemSave.get_UploadedFiles
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
CP0002
M:Umbraco.Cms.Core.Models.ContentEditing.ContentPropertyDisplay.get_ConfigNullable
@@ -435,6 +470,20 @@
lib/net7.0/Umbraco.Core.dll
true
+
+ CP0002
+ M:Umbraco.Cms.Core.Models.Editors.ContentPropertyData.get_Files
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0002
+ M:Umbraco.Cms.Core.Models.Editors.ContentPropertyData.set_Files(Umbraco.Cms.Core.Models.Editors.ContentPropertyFile[])
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
CP0002
M:Umbraco.Cms.Core.Models.IDataType.get_Configuration
@@ -1506,4 +1555,39 @@
lib/net7.0/Umbraco.Core.dll
true
-
\ No newline at end of file
+
+ CP0008
+ T:Umbraco.Cms.Core.Models.ContentEditing.ContentBaseSave`1
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0008
+ T:Umbraco.Cms.Core.Models.ContentEditing.ContentItemSave
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0008
+ T:Umbraco.Cms.Core.Models.ContentEditing.IContentSave`1
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0008
+ T:Umbraco.Cms.Core.Models.ContentEditing.MediaItemSave
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
+ CP0008
+ T:Umbraco.Cms.Core.Models.ContentEditing.MemberSave
+ lib/net7.0/Umbraco.Core.dll
+ lib/net7.0/Umbraco.Core.dll
+ true
+
+
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index 48c474794e..956eaf003f 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -345,6 +345,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique(provider => new CultureImpactFactory(provider.GetRequiredService>()));
Services.AddUnique();
Services.AddUnique();
+ Services.AddUnique();
}
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs
index 241cde46b4..a9a95da70a 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentBaseSave.cs
@@ -11,8 +11,6 @@ namespace Umbraco.Cms.Core.Models.ContentEditing;
public abstract class ContentBaseSave : ContentItemBasic, IContentSave
where TPersisted : IContentBase
{
- protected ContentBaseSave() => UploadedFiles = new List();
-
#region IContentSave
///
@@ -27,9 +25,6 @@ public abstract class ContentBaseSave : ContentItemBasic base.Properties = value;
}
- [IgnoreDataMember]
- public List UploadedFiles { get; }
-
// These need explicit implementation because we are using internal models
///
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs
index 400436421b..c1df9480cb 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs
@@ -12,7 +12,6 @@ public class ContentItemSave : IContentSave
{
public ContentItemSave()
{
- UploadedFiles = new List();
Variants = new List();
}
@@ -43,9 +42,6 @@ public class ContentItemSave : IContentSave
[Required]
public ContentSaveAction Action { get; set; }
- [IgnoreDataMember]
- public List UploadedFiles { get; }
-
// These need explicit implementation because we are using internal models
///
diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs
index effccf95fa..d0dee2931d 100644
--- a/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/IContentSave.cs
@@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing;
/// logic
///
///
-public interface IContentSave : IHaveUploadedFiles
+public interface IContentSave
where TPersisted : IContentBase
{
///
diff --git a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs
deleted file mode 100644
index 7e467ff124..0000000000
--- a/src/Umbraco.Core/Models/ContentEditing/IHaveUploadedFiles.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Umbraco.Cms.Core.Models.Editors;
-
-namespace Umbraco.Cms.Core.Models.ContentEditing;
-
-public interface IHaveUploadedFiles
-{
- List UploadedFiles { get; }
-}
diff --git a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs b/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs
deleted file mode 100644
index 5d71369141..0000000000
--- a/src/Umbraco.Core/Models/ContentEditing/PostedFiles.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Runtime.Serialization;
-using Umbraco.Cms.Core.Models.Editors;
-
-namespace Umbraco.Cms.Core.Models.ContentEditing;
-
-///
-/// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the
-/// temporary files that were created.
-///
-[DataContract]
-public class PostedFiles : IHaveUploadedFiles, INotificationModel
-{
- public PostedFiles()
- {
- UploadedFiles = new List();
- Notifications = new List();
- }
-
- public List UploadedFiles { get; }
-
- [DataMember(Name = "notifications")]
- public List Notifications { get; private set; }
-}
diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs
index ac19eef0c8..51910815b5 100644
--- a/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs
+++ b/src/Umbraco.Core/Models/Editors/ContentPropertyData.cs
@@ -34,9 +34,4 @@ public class ContentPropertyData
/// Gets or sets the unique identifier of the property type.
///
public Guid PropertyTypeKey { get; set; }
-
- ///
- /// Gets or sets the uploaded files.
- ///
- public ContentPropertyFile[]? Files { get; set; }
}
diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
deleted file mode 100644
index 9bb098697c..0000000000
--- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace Umbraco.Cms.Core.Models.Editors;
-
-///
-/// Represents an uploaded file for a property.
-///
-public class ContentPropertyFile
-{
- ///
- /// Gets or sets the property alias.
- ///
- public string? PropertyAlias { get; set; }
-
- ///
- /// When dealing with content variants, this is the culture for the variant
- ///
- public string? Culture { get; set; }
-
- ///
- /// When dealing with content variants, this is the segment for the variant
- ///
- public string? Segment { get; set; }
-
- ///
- /// An array of metadata that is parsed out from the file info posted to the server which is set on the client.
- ///
- ///
- /// This can be used for property types like Nested Content that need to have special unique identifiers for each file
- /// since there might be multiple files
- /// per property.
- ///
- public string[]? Metadata { get; set; }
-
- ///
- /// Gets or sets the name of the file.
- ///
- public string? FileName { get; set; }
-
- ///
- /// Gets or sets the temporary path where the file has been uploaded.
- ///
- public string TempFilePath { get; set; } = string.Empty;
-}
diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs
index 2ff9232cb5..a218ed3260 100644
--- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs
@@ -377,9 +377,7 @@ public abstract class ContentEditingServiceBase()
+ PropertyTypeKey = propertyType.Key
};
var currentValue = content.GetValue(propertyType.Alias, culture, segment);
diff --git a/src/Umbraco.Core/Services/IMediaImportService.cs b/src/Umbraco.Core/Services/IMediaImportService.cs
new file mode 100644
index 0000000000..0b2c90022e
--- /dev/null
+++ b/src/Umbraco.Core/Services/IMediaImportService.cs
@@ -0,0 +1,8 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services;
+
+public interface IMediaImportService
+{
+ public Task ImportAsync(string fileName, Stream fileStream, Guid? parentId, string? mediaTypeAlias, Guid userKey);
+}
diff --git a/src/Umbraco.Core/Services/ITemporaryMediaService.cs b/src/Umbraco.Core/Services/ITemporaryMediaService.cs
index 9c3c07acaf..2b4f00ab78 100644
--- a/src/Umbraco.Core/Services/ITemporaryMediaService.cs
+++ b/src/Umbraco.Core/Services/ITemporaryMediaService.cs
@@ -2,6 +2,7 @@
namespace Umbraco.Cms.Core.Services;
+[Obsolete($"This service has been superseded by {nameof(IMediaImportService)}. Will be removed in V16.")]
public interface ITemporaryMediaService
{
public IMedia Save(string temporaryLocation, Guid? startNode, string? mediaTypeAlias);
diff --git a/src/Umbraco.Core/Services/MediaImportService.cs b/src/Umbraco.Core/Services/MediaImportService.cs
new file mode 100644
index 0000000000..8c73bbc89f
--- /dev/null
+++ b/src/Umbraco.Core/Services/MediaImportService.cs
@@ -0,0 +1,74 @@
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Strings;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.Services;
+
+internal sealed class MediaImportService : IMediaImportService
+{
+ private readonly IShortStringHelper _shortStringHelper;
+ private readonly MediaFileManager _mediaFileManager;
+ private readonly IMediaService _mediaService;
+ private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
+ private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
+ private readonly IEntityService _entityService;
+ private readonly AppCaches _appCaches;
+ private readonly IUserService _userService;
+
+ public MediaImportService(
+ IShortStringHelper shortStringHelper,
+ MediaFileManager mediaFileManager,
+ IMediaService mediaService,
+ MediaUrlGeneratorCollection mediaUrlGenerators,
+ IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
+ IEntityService entityService,
+ AppCaches appCaches,
+ IUserService userService)
+ {
+ _shortStringHelper = shortStringHelper;
+ _mediaFileManager = mediaFileManager;
+ _mediaService = mediaService;
+ _mediaUrlGenerators = mediaUrlGenerators;
+ _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
+ _entityService = entityService;
+ _appCaches = appCaches;
+ _userService = userService;
+ }
+
+ public async Task ImportAsync(string fileName, Stream fileStream, Guid? parentId, string? mediaTypeAlias, Guid userKey)
+ {
+ if (fileStream.CanRead == false)
+ {
+ throw new InvalidOperationException("Could not read from file stream, please ensure it is open and readable");
+ }
+
+ IUser user = await _userService.GetAsync(userKey)
+ ?? throw new ArgumentException($"Could not find a user with the specified user key ({userKey})", nameof(userKey));
+
+ var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
+ var mediaItemName = safeFileName.ToFriendlyName();
+
+ IMedia mediaFile;
+
+ if (parentId is null)
+ {
+ int[]? userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
+
+ mediaFile = _mediaService.CreateMedia(mediaItemName, userStartNodes != null && userStartNodes.Any() ? userStartNodes[0] : Constants.System.Root, mediaTypeAlias ?? Constants.Conventions.MediaTypes.File, user.Id);
+ }
+ else
+ {
+ mediaFile = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias ?? Constants.Conventions.MediaTypes.File, user.Id);
+ }
+
+ mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
+
+ _mediaService.Save(mediaFile, user.Id);
+
+ return mediaFile;
+ }
+}
diff --git a/src/Umbraco.Core/Services/TemporaryMediaService.cs b/src/Umbraco.Core/Services/TemporaryMediaService.cs
index acd8e08fc9..41f45e0a69 100644
--- a/src/Umbraco.Core/Services/TemporaryMediaService.cs
+++ b/src/Umbraco.Core/Services/TemporaryMediaService.cs
@@ -12,6 +12,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
+[Obsolete($"This service has been superseded by {nameof(IMediaImportService)}. Will be removed in V16.")]
public class TemporaryMediaService : ITemporaryMediaService
{
private readonly IShortStringHelper _shortStringHelper;
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
index 580209c48f..f1b6c482c4 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
@@ -149,11 +149,7 @@ public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
///
/// The corresponding property value editor.
protected override IDataValueEditor CreateValueEditor()
- {
- FileUploadPropertyValueEditor editor = DataValueEditorFactory.Create(Attribute!);
- editor.Validators.Add(new UploadFileTypeValidator(_localizedTextService, _contentSettings));
- return editor;
- }
+ => DataValueEditorFactory.Create(Attribute!);
///
/// Gets a value indicating whether a property is an upload field.
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
index 5a14a1afc1..db2315d5c9 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
@@ -5,9 +5,11 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -18,6 +20,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
internal class FileUploadPropertyValueEditor : DataValueEditor
{
private readonly MediaFileManager _mediaFileManager;
+ private readonly ITemporaryFileService _temporaryFileService;
+ private readonly IScopeProvider _scopeProvider;
private ContentSettings _contentSettings;
public FileUploadPropertyValueEditor(
@@ -27,12 +31,22 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
IShortStringHelper shortStringHelper,
IOptionsMonitor contentSettings,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper)
+ IIOHelper ioHelper,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
+ _temporaryFileService = temporaryFileService;
+ _scopeProvider = scopeProvider;
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
contentSettings.OnChange(x => _contentSettings = x);
+
+ Validators.Add(new TemporaryFileUploadValidator(
+ () => _contentSettings,
+ TryParseTemporaryFileKey,
+ TryGetTemporaryFile,
+ IsAllowedInDataTypeConfiguration));
}
///
@@ -44,108 +58,125 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
///
/// The is used to re-use the folder, if possible.
///
- /// The is value passed in from the editor. We normally don't care what
- /// the editorValue.Value is set to because we are more interested in the files collection associated with it,
- /// however we do care about the value if we are clearing files. By default the editorValue.Value will just
- /// be set to the name of the file - but again, we just ignore this and deal with the file collection in
- /// editorValue.AdditionalData.ContainsKey("files")
- ///
- ///
- /// We only process ONE file. We understand that the current value may contain more than one file,
- /// and that more than one file may be uploaded, so we take care of them all, but we only store ONE file.
- /// Other places (FileUploadPropertyEditor...) do NOT deal with multiple files, and our logic for reusing
- /// folders would NOT work, etc.
+ /// The is value passed in from the editor. If the value is empty, we
+ /// must delete the currently selected file (). If the value is not empty,
+ /// it is assumed to contain a temporary file key, and we will attempt to replace the currently selected
+ /// file with the corresponding temporary file.
///
///
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
- var currentPath = currentValue as string;
- if (!currentPath.IsNullOrWhiteSpace())
+ var currentStringValue = currentValue as string;
+ currentStringValue = currentStringValue.NullOrWhiteSpaceAsNull();
+
+ var editorStringValue = editorValue.Value as string;
+ editorStringValue = editorStringValue.NullOrWhiteSpaceAsNull();
+
+ // no change?
+ if (editorStringValue == currentStringValue)
{
- currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath!);
+ return currentValue;
}
- string? editorFile = null;
- if (editorValue.Value != null)
+ var currentPath = currentStringValue;
+ if (currentPath.IsNullOrWhiteSpace() == false)
{
- editorFile = editorValue.Value as string;
+ currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath);
}
+ // resetting the current value?
+ if (editorStringValue is null && currentPath.IsNullOrWhiteSpace() is false)
+ {
+ // delete the current file and clear the value of this property
+ _mediaFileManager.FileSystem.DeleteFile(currentPath);
+ return null;
+ }
+
+ // uploading a file?
+ if (Guid.TryParse(editorStringValue, out Guid temporaryFileKey) == false)
+ {
+ return editorStringValue;
+ }
+
+ TemporaryFileModel? file = TryGetTemporaryFile(temporaryFileKey);
+ if (file == null)
+ {
+ // at this point the temporary file *should* have been validated by TemporaryFileUploadValidator, so we
+ // should never end up here. In case we do, let's attempt to at least be non-destructive by returning
+ // the current value
+ return currentValue;
+ }
+
+ // schedule temporary file for deletion
+ using IScope scope = _scopeProvider.CreateScope();
+ _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey, _scopeProvider);
+
// ensure we have the required guids
- Guid cuid = editorValue.ContentKey;
- if (cuid == Guid.Empty)
+ Guid contentKey = editorValue.ContentKey;
+ if (contentKey == Guid.Empty)
{
throw new Exception("Invalid content key.");
}
- Guid puid = editorValue.PropertyTypeKey;
- if (puid == Guid.Empty)
+ Guid propertyTypeKey = editorValue.PropertyTypeKey;
+ if (propertyTypeKey == Guid.Empty)
{
throw new Exception("Invalid property type key.");
}
- ContentPropertyFile[]? uploads = editorValue.Files;
- if (uploads == null)
- {
- throw new Exception("Invalid files.");
- }
-
- ContentPropertyFile? file = uploads.Length > 0 ? uploads[0] : null;
-
- // not uploading a file
- if (file == null)
- {
- // if editorFile is empty then either there was nothing to begin with,
- // or it has been cleared and we need to remove the file - else the
- // value is unchanged.
- if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false)
- {
- _mediaFileManager.FileSystem.DeleteFile(currentPath);
- return null; // clear
- }
-
- return currentValue; // unchanged
- }
-
// process the file
- var filepath = editorFile == null ? null : ProcessFile(file, editorValue.DataTypeConfiguration, cuid, puid);
-
- // remove all temp files
- foreach (ContentPropertyFile f in uploads)
- {
- File.Delete(f.TempFilePath);
- }
+ var filepath = ProcessFile(file, editorValue.DataTypeConfiguration, contentKey, propertyTypeKey);
// remove current file if replaced
- if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false)
+ if (currentPath != filepath && currentPath.IsNullOrWhiteSpace() is false)
{
_mediaFileManager.FileSystem.DeleteFile(currentPath);
}
- // update json and return
- if (editorFile == null)
- {
- return null;
- }
+ scope.Complete();
- return filepath == null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath);
+ return filepath == null ? null : _mediaFileManager.FileSystem.GetUrl(filepath);
}
- private string? ProcessFile(ContentPropertyFile file, object? dataTypeConfiguration, Guid cuid, Guid puid)
+ private Guid? TryParseTemporaryFileKey(object? editorValue)
+ => editorValue is string stringValue && Guid.TryParse(stringValue, out Guid temporaryFileKey)
+ ? temporaryFileKey
+ : null;
+
+ private TemporaryFileModel? TryGetTemporaryFile(Guid temporaryFileKey)
+ => _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
+
+ private bool IsAllowedInDataTypeConfiguration(string extension, object? dataTypeConfiguration)
+ {
+ if (dataTypeConfiguration is FileUploadConfiguration fileUploadConfiguration)
+ {
+ // If FileExtensions is empty and no allowed extensions have been specified, we allow everything.
+ // If there are any extensions specified, we need to check that the uploaded extension is one of them.
+ return fileUploadConfiguration.FileExtensions.IsCollectionEmpty() ||
+ fileUploadConfiguration.FileExtensions.Any(x => x.Value?.InvariantEquals(extension) ?? false);
+ }
+
+ return false;
+ }
+
+ private string? ProcessFile(TemporaryFileModel file, object? dataTypeConfiguration, Guid contentKey, Guid propertyTypeKey)
{
// process the file
// no file, invalid file, reject change
- if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) is false ||
- UploadFileTypeValidator.IsAllowedInDataTypeConfiguration(file.FileName, dataTypeConfiguration) is false)
+ // this check is somewhat redundant as the file validity has already been checked by TemporaryFileUploadValidator,
+ // but we'll retain it here as a last measure in case someone accidentally breaks the validator
+ var extension = Path.GetExtension(file.FileName).TrimStart('.');
+ if (_contentSettings.IsFileAllowedForUpload(extension) is false ||
+ IsAllowedInDataTypeConfiguration(extension, dataTypeConfiguration) is false)
{
return null;
}
// get the filepath
// in case we are using the old path scheme, try to re-use numbers (bah...)
- var filepath = _mediaFileManager.GetMediaPath(file.FileName, cuid, puid); // fs-relative path
+ var filepath = _mediaFileManager.GetMediaPath(file.FileName, contentKey, propertyTypeKey); // fs-relative path
- using (FileStream filestream = File.OpenRead(file.TempFilePath))
+ using (Stream filestream = file.OpenReadStream())
{
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
// since we'd need to be able to return values for other properties from these methods
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs
index c668e6c3fb..a163c485dd 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs
@@ -8,12 +8,13 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
-using File = System.IO.File;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -27,6 +28,8 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
private readonly MediaFileManager _mediaFileManager;
private readonly IJsonSerializer _jsonSerializer;
private ContentSettings _contentSettings;
+ private readonly ITemporaryFileService _temporaryFileService;
+ private readonly IScopeProvider _scopeProvider;
public ImageCropperPropertyValueEditor(
DataEditorAttribute attribute,
@@ -37,7 +40,9 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
IOptionsMonitor contentSettings,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
- IDataTypeService dataTypeService)
+ IDataTypeService dataTypeService,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -45,7 +50,11 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
_jsonSerializer = jsonSerializer;
_contentSettings = contentSettings.CurrentValue;
_dataTypeService = dataTypeService;
+ _temporaryFileService = temporaryFileService;
+ _scopeProvider = scopeProvider;
contentSettings.OnChange(x => _contentSettings = x);
+
+ Validators.Add(new TemporaryFileUploadValidator(() => _contentSettings, TryParseTemporaryFileKey, TryGetTemporaryFile));
}
///
@@ -88,8 +97,10 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
///
/// The is used to re-use the folder, if possible.
///
- /// editorValue.Value is used to figure out editorFile and, if it has been cleared, remove the old file - but
- /// it is editorValue.AdditionalData["files"] that is used to determine the actual file that has been uploaded.
+ /// editorValue.Value is used to figure out editorFile and, if it has been cleared, remove the old file.
+ /// If editorValue.Value deserializes as and the
+ /// value is a GUID, it is assumed to contain a temporary file key, and we will attempt to replace the currently
+ /// selected file with the corresponding temporary file.
///
///
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
@@ -115,46 +126,30 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath);
}
- ImageCropperValue? editorImageCropperValue = null;
-
- // FIXME: consider creating an object deserialization method on IJsonSerializer instead of relying on deserializing serialized JSON here (and likely other places as well)
- if (editorValue.Value is JsonObject jsonObject)
- {
- try
- {
- editorImageCropperValue = _jsonSerializer.Deserialize(jsonObject.ToJsonString());
- editorImageCropperValue?.Prune();
- }
- catch (Exception ex)
- {
- // For some reason the value is invalid - log error and continue as if no value was saved
- _logger.LogWarning(ex, "Could not parse editor value to an ImageCropperValue object.");
- }
- }
+ ImageCropperValue? editorImageCropperValue = TryParseImageCropperValue(editorValue.Value);
// ensure we have the required guids
- Guid cuid = editorValue.ContentKey;
- if (cuid == Guid.Empty)
+ Guid contentKey = editorValue.ContentKey;
+ if (contentKey == Guid.Empty)
{
throw new Exception("Invalid content key.");
}
- Guid puid = editorValue.PropertyTypeKey;
- if (puid == Guid.Empty)
+ Guid propertyTypeKey = editorValue.PropertyTypeKey;
+ if (propertyTypeKey == Guid.Empty)
{
throw new Exception("Invalid property type key.");
}
- // editorFile is empty whenever a new file is being uploaded
- // or when the file is cleared (in which case editorJson is null)
- // else editorFile contains the unchanged value
- ContentPropertyFile[]? uploads = editorValue.Files;
- if (uploads == null)
- {
- throw new Exception("Invalid files.");
- }
+ using IScope scope = _scopeProvider.CreateScope();
- ContentPropertyFile? file = uploads.Length > 0 ? uploads[0] : null;
+ TemporaryFileModel? file = null;
+ Guid? temporaryFileKey = TryParseTemporaryFileKey(editorImageCropperValue);
+ if (temporaryFileKey.HasValue)
+ {
+ file = TryGetTemporaryFile(temporaryFileKey.Value);
+ _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey.Value, _scopeProvider);
+ }
if (file == null) // not uploading a file
{
@@ -171,13 +166,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
}
// process the file
- var filepath = editorImageCropperValue == null ? null : ProcessFile(file, cuid, puid);
-
- // remove all temp files
- foreach (ContentPropertyFile f in uploads)
- {
- File.Delete(f.TempFilePath);
- }
+ var filepath = editorImageCropperValue == null ? null : ProcessFile(file, contentKey, propertyTypeKey);
// remove current file if replaced
if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false)
@@ -185,6 +174,8 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
_mediaFileManager.FileSystem.DeleteFile(currentPath);
}
+ scope.Complete();
+
// update json and return
if (editorImageCropperValue == null)
{
@@ -217,20 +208,60 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
return _jsonSerializer.Serialize(new { src = val, crops });
}
- private string? ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid)
+ private ImageCropperValue? TryParseImageCropperValue(object? editorValue)
+ {
+ // FIXME: consider creating an object deserialization method on IJsonSerializer instead of relying on deserializing serialized JSON here (and likely other places as well)
+ if (editorValue is JsonObject jsonObject)
+ {
+ try
+ {
+ ImageCropperValue? imageCropperValue = _jsonSerializer.Deserialize(jsonObject.ToJsonString());
+ imageCropperValue?.Prune();
+ return imageCropperValue;
+ }
+ catch (Exception ex)
+ {
+ // For some reason the value is invalid - log error and continue as if no value was saved
+ _logger.LogWarning(ex, "Could not parse editor value to an ImageCropperValue object.");
+ }
+ }
+
+ return null;
+ }
+
+ private Guid? TryParseTemporaryFileKey(object? editorValue)
+ {
+ ImageCropperValue? imageCropperValue = TryParseImageCropperValue(editorValue);
+ return imageCropperValue != null
+ ? TryParseTemporaryFileKey(imageCropperValue)
+ : null;
+ }
+
+ private Guid? TryParseTemporaryFileKey(ImageCropperValue? editorValue)
+ => Guid.TryParse(editorValue?.Src, out Guid temporaryFileKey)
+ ? temporaryFileKey
+ : null;
+
+ private TemporaryFileModel? TryGetTemporaryFile(Guid temporaryFileKey)
+ => _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
+
+ private string? ProcessFile(TemporaryFileModel file, Guid contentKey, Guid propertyTypeKey)
{
// process the file
// no file, invalid file, reject change
- if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) == false)
+ // this check is somewhat redundant as the file validity has already been checked by TemporaryFileUploadValidator,
+ // but we'll retain it here as a last measure in case someone accidentally breaks the validator
+ var extension = Path.GetExtension(file.FileName).TrimStart('.');
+ if (_contentSettings.IsFileAllowedForUpload(extension) is false)
{
return null;
}
// get the filepath
// in case we are using the old path scheme, try to re-use numbers (bah...)
- var filepath = _mediaFileManager.GetMediaPath(file.FileName, cuid, puid); // fs-relative path
+ var filepath = _mediaFileManager.GetMediaPath(file.FileName, contentKey, propertyTypeKey); // fs-relative path
- using (FileStream filestream = File.OpenRead(file.TempFilePath))
+ using (Stream filestream = file.OpenReadStream())
{
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
// since we'd need to be able to return values for other properties from these methods
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
index b4ab07d8b0..146e4b8949 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
@@ -4,10 +4,13 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
+using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -71,8 +74,11 @@ public class MediaPicker3PropertyEditor : DataEditor
{
private readonly IDataTypeService _dataTypeService;
private readonly IJsonSerializer _jsonSerializer;
- private readonly ITemporaryMediaService _temporaryMediaService;
-
+ private readonly IMediaImportService _mediaImportService;
+ private readonly IMediaService _mediaService;
+ private readonly ITemporaryFileService _temporaryFileService;
+ private readonly IScopeProvider _scopeProvider;
+ private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public MediaPicker3PropertyValueEditor(
ILocalizedTextService localizedTextService,
@@ -81,12 +87,20 @@ public class MediaPicker3PropertyEditor : DataEditor
IIOHelper ioHelper,
DataEditorAttribute attribute,
IDataTypeService dataTypeService,
- ITemporaryMediaService temporaryMediaService)
+ IMediaImportService mediaImportService,
+ IMediaService mediaService,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
_dataTypeService = dataTypeService;
- _temporaryMediaService = temporaryMediaService;
+ _mediaImportService = mediaImportService;
+ _mediaService = mediaService;
+ _temporaryFileService = temporaryFileService;
+ _scopeProvider = scopeProvider;
+ _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
///
@@ -106,6 +120,7 @@ public class MediaPicker3PropertyEditor : DataEditor
var value = property.GetValue(culture, segment);
var dtos = Deserialize(_jsonSerializer, value).ToList();
+ dtos = UpdateMediaTypeAliases(dtos);
IDataType? dataType = _dataTypeService.GetDataType(property.PropertyType.DataTypeId);
if (dataType?.ConfigurationObject != null)
@@ -137,7 +152,8 @@ public class MediaPicker3PropertyEditor : DataEditor
if (editorValue.DataTypeConfiguration is MediaPicker3Configuration configuration)
{
- // FIXME: handle temp files here once we implement file uploads (see old implementation "PersistTempMedia" in the commented out code below)
+ // handle temporary media uploads
+ mediaWithCropsDtos = HandleTemporaryMediaUploads(mediaWithCropsDtos, configuration);
}
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
@@ -188,48 +204,63 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
- // private JArray PersistTempMedia(JArray jArray, MediaPicker3Configuration mediaPicker3Configuration)
- // {
- // var result = new JArray();
- // foreach (JObject? dto in jArray.Values())
- // {
- // if (dto is null)
- // {
- // continue;
- // }
- //
- // if (!dto.TryGetValue("tmpLocation", out JToken? temporaryLocation))
- // {
- // // If it does not have a temporary path, it can be an already saved image or not-yet uploaded temp-image, check for media-key
- // if (dto.TryGetValue("mediaKey", out _))
- // {
- // result.Add(dto);
- // }
- //
- // continue;
- // }
- //
- // var temporaryLocationString = temporaryLocation.Value();
- // if (temporaryLocationString is null)
- // {
- // continue;
- // }
- //
- // GuidUdi? startNodeGuid = mediaPicker3Configuration.StartNodeId as GuidUdi ?? null;
- // JToken? mediaTypeAlias = dto.GetValue("mediaTypeAlias");
- // IMedia mediaFile = _temporaryMediaService.Save(temporaryLocationString, startNodeGuid?.Guid, mediaTypeAlias?.Value());
- // MediaWithCropsDto? mediaDto = _jsonSerializer.Deserialize(dto.ToString());
- // if (mediaDto is null)
- // {
- // continue;
- // }
- //
- // mediaDto.MediaKey = mediaFile.GetUdi().Guid;
- // result.Add(JObject.Parse(_jsonSerializer.Serialize(mediaDto)));
- // }
- //
- // return result;
- // }
+ private List UpdateMediaTypeAliases(List mediaWithCropsDtos)
+ {
+ const string unknownMediaType = "UNKNOWN";
+
+ foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
+ {
+ IMedia? media = _mediaService.GetById(mediaWithCropsDto.MediaKey);
+ mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType;
+ }
+
+ return mediaWithCropsDtos.Where(m => m.MediaTypeAlias != unknownMediaType).ToList();
+ }
+
+ private List HandleTemporaryMediaUploads(List mediaWithCropsDtos, MediaPicker3Configuration configuration)
+ {
+ Guid userKey = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key
+ ?? throw new InvalidOperationException("Could not obtain the current backoffice user");
+
+ var invalidDtos = new List();
+
+ foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
+ {
+ // if the media already exist, don't bother with it
+ if (_mediaService.GetById(mediaWithCropsDto.MediaKey) != null)
+ {
+ continue;
+ }
+
+ // we'll assume that the media key is the key of a temporary file
+ TemporaryFileModel? temporaryFile = _temporaryFileService.GetAsync(mediaWithCropsDto.MediaKey).GetAwaiter().GetResult();
+ if (temporaryFile == null)
+ {
+ // the temporary file is missing, don't process this item any further
+ invalidDtos.Add(mediaWithCropsDto);
+ continue;
+ }
+
+ GuidUdi? startNodeGuid = configuration.StartNodeId as GuidUdi ?? null;
+
+ // make sure we'll clean up the temporary file if the scope completes
+ using IScope scope = _scopeProvider.CreateScope();
+ _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFile.Key, _scopeProvider);
+
+ // create a new media using the temporary file - the media type is passed from the client, in case
+ // there are multiple allowed media types matching the file extension
+ using Stream fileStream = temporaryFile.OpenReadStream();
+ IMedia mediaFile = _mediaImportService
+ .ImportAsync(temporaryFile.FileName, fileStream, startNodeGuid?.Guid, mediaWithCropsDto.MediaTypeAlias, userKey)
+ .GetAwaiter()
+ .GetResult();
+
+ mediaWithCropsDto.MediaKey = mediaFile.Key;
+ scope.Complete();
+ }
+
+ return mediaWithCropsDtos.Except(invalidDtos).ToList();
+ }
///
/// Model/DTO that represents the JSON that the MediaPicker3 stores.
@@ -240,6 +271,8 @@ public class MediaPicker3PropertyEditor : DataEditor
public Guid MediaKey { get; set; }
+ public string MediaTypeAlias { get; set; } = string.Empty;
+
public IEnumerable? Crops { get; set; }
public ImageCropperValue.ImageCropperFocalPoint? FocalPoint { get; set; }
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
index 5044a5b13e..264602897b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
@@ -2,17 +2,22 @@
// See LICENSE for more details.
using HtmlAgilityPack;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -20,17 +25,14 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public sealed class RichTextEditorPastedImages
{
private const string TemporaryImageDataAttribute = "data-tmpimg";
- private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
- private readonly IHostingEnvironment _hostingEnvironment;
- private readonly ILogger _logger;
- private readonly MediaFileManager _mediaFileManager;
- private readonly IMediaService _mediaService;
- private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly IPublishedUrlProvider _publishedUrlProvider;
- private readonly IShortStringHelper _shortStringHelper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
- private readonly string _tempFolderAbsolutePath;
+ private readonly ITemporaryFileService _temporaryFileService;
+ private readonly IScopeProvider _scopeProvider;
+ private readonly IMediaImportService _mediaImportService;
+ private readonly IUserService _userService;
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
ILogger logger,
@@ -41,27 +43,71 @@ public sealed class RichTextEditorPastedImages
MediaUrlGeneratorCollection mediaUrlGenerators,
IShortStringHelper shortStringHelper,
IPublishedUrlProvider publishedUrlProvider)
+ : this(
+ umbracoContextAccessor,
+ logger,
+ hostingEnvironment,
+ mediaService,
+ contentTypeBaseServiceProvider,
+ mediaFileManager,
+ mediaUrlGenerators,
+ shortStringHelper,
+ publishedUrlProvider,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
+ public RichTextEditorPastedImages(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ ILogger logger,
+ IHostingEnvironment hostingEnvironment,
+ IMediaService mediaService,
+ IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
+ MediaFileManager mediaFileManager,
+ MediaUrlGeneratorCollection mediaUrlGenerators,
+ IShortStringHelper shortStringHelper,
+ IPublishedUrlProvider publishedUrlProvider,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider,
+ IMediaImportService mediaImportService)
+ : this(umbracoContextAccessor, publishedUrlProvider, temporaryFileService, scopeProvider, mediaImportService)
+ {
+ }
+
+ public RichTextEditorPastedImages(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ IPublishedUrlProvider publishedUrlProvider,
+ ITemporaryFileService temporaryFileService,
+ IScopeProvider scopeProvider,
+ IMediaImportService mediaImportService)
{
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _hostingEnvironment = hostingEnvironment;
- _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
- _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ??
- throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider));
- _mediaFileManager = mediaFileManager;
- _mediaUrlGenerators = mediaUrlGenerators;
- _shortStringHelper = shortStringHelper;
_publishedUrlProvider = publishedUrlProvider;
+ _temporaryFileService = temporaryFileService;
+ _scopeProvider = scopeProvider;
+ _mediaImportService = mediaImportService;
- _tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads);
+ // this obviously is not correct. however, we only use IUserService in an obsolete method,
+ // so this is better than having even more obsolete constructors for V16
+ _userService = StaticServiceProvider.Instance.GetRequiredService();
+ }
+ [Obsolete($"Please use {nameof(FindAndPersistPastedTempImagesAsync)}. Will be removed in V16.")]
+ public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
+ {
+ IUser user = _userService.GetUserById(userId)
+ ?? throw new ArgumentException($"Could not find a user with the specified user key ({userId})", nameof(userId));
+ return FindAndPersistPastedTempImagesAsync(html, mediaParentFolder, user.Key, imageUrlGenerator).GetAwaiter().GetResult();
}
///
/// Used by the RTE (and grid RTE) for drag/drop/persisting images
///
- public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
+ public async Task FindAndPersistPastedTempImagesAsync(string html, Guid mediaParentFolder, Guid userKey, IImageUrlGenerator imageUrlGenerator)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
@@ -76,66 +122,43 @@ public sealed class RichTextEditorPastedImages
// An array to contain a list of URLs that
// we have already processed to avoid dupes
- var uploadedImages = new Dictionary();
-
+ var uploadedImages = new Dictionary();
foreach (HtmlNode? img in tmpImages)
{
- // The data attribute contains the path to the tmp img to persist as a media item
- var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty);
-
- if (string.IsNullOrEmpty(tmpImgPath))
+ // The data attribute contains the key of the temporary file
+ var tmpImgKey = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty);
+ if (Guid.TryParse(tmpImgKey, out Guid temporaryFileKey) is false)
{
continue;
}
-
- var absoluteTempImagePath = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(tmpImgPath));
-
- if (IsValidPath(absoluteTempImagePath) == false)
+ TemporaryFileModel? temporaryFile = _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
+ if (temporaryFile is null)
{
continue;
}
- var fileName = Path.GetFileName(absoluteTempImagePath);
- var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
-
- var mediaItemName = safeFileName.ToFriendlyName();
- IMedia mediaFile;
GuidUdi udi;
- if (uploadedImages.ContainsKey(tmpImgPath) == false)
+ using (IScope scope = _scopeProvider.CreateScope())
{
- if (mediaParentFolder == Guid.Empty)
+ _temporaryFileService.EnlistDeleteIfScopeCompletes(temporaryFileKey, _scopeProvider);
+
+ if (uploadedImages.ContainsKey(temporaryFileKey) == false)
{
- mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
+ using Stream fileStream = temporaryFile.OpenReadStream();
+ Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? Constants.System.RootKey : mediaParentFolder;
+ IMedia mediaFile = await _mediaImportService.ImportAsync(temporaryFile.FileName, fileStream, parentFolderKey, Constants.Conventions.MediaTypes.Image, userKey);
+ udi = mediaFile.GetUdi();
}
else
{
- mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
+ // Already been uploaded & we have it's UDI
+ udi = uploadedImages[temporaryFileKey];
}
- var fileInfo = new FileInfo(absoluteTempImagePath);
-
- FileStream? fileStream = fileInfo.OpenReadWithRetry();
- if (fileStream == null)
- {
- throw new InvalidOperationException("Could not acquire file stream");
- }
-
- using (fileStream)
- {
- mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
- }
-
- _mediaService.Save(mediaFile, userId);
-
- udi = mediaFile.GetUdi();
- }
- else
- {
- // Already been uploaded & we have it's UDI
- udi = uploadedImages[tmpImgPath];
+ scope.Complete();
}
// Add the UDI to the img element as new data attribute
@@ -172,33 +195,9 @@ public sealed class RichTextEditorPastedImages
img.Attributes.Remove(TemporaryImageDataAttribute);
// Add to the dictionary to avoid dupes
- if (uploadedImages.ContainsKey(tmpImgPath) == false)
- {
- uploadedImages.Add(tmpImgPath, udi);
-
- // Delete folder & image now its saved in media
- // The folder should contain one image - as a unique guid folder created
- // for each image uploaded from TinyMceController
- var folderName = Path.GetDirectoryName(absoluteTempImagePath);
- try
- {
- if (folderName is not null)
- {
- Directory.Delete(folderName, true);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
- }
- }
+ uploadedImages.TryAdd(temporaryFileKey, udi);
}
return htmlDoc.DocumentNode.OuterHtml;
}
-
- private bool IsValidPath(string imagePath)
- {
- return imagePath.StartsWith(_tempFolderAbsolutePath);
- }
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index fee03896f9..207ea92b16 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -281,8 +281,8 @@ public class RichTextPropertyEditor : DataEditor
return null;
}
- var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ??
- Constants.Security.SuperUserId;
+ Guid userKey = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key ??
+ Constants.Security.SuperUserKey;
var config = editorValue.DataTypeConfiguration as RichTextConfiguration;
GuidUdi? mediaParent = config?.MediaParentId;
@@ -293,8 +293,10 @@ public class RichTextPropertyEditor : DataEditor
return null;
}
- var parseAndSavedTempImages =
- _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString()!, mediaParentId, userId, _imageUrlGenerator);
+ var parseAndSavedTempImages = _pastedImages
+ .FindAndPersistPastedTempImagesAsync(editorValue.Value.ToString()!, mediaParentId, userKey, _imageUrlGenerator)
+ .GetAwaiter()
+ .GetResult();
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
var sanitized = _htmlSanitizer.Sanitize(parsed);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs
new file mode 100644
index 0000000000..4aa7d1a211
--- /dev/null
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs
@@ -0,0 +1,67 @@
+using System.ComponentModel.DataAnnotations;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Models.TemporaryFile;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+internal class TemporaryFileUploadValidator : IValueValidator
+{
+ private readonly GetContentSettings _getContentSettings;
+ private readonly ParseTemporaryFileKey _parseTemporaryFileKey;
+ private readonly GetTemporaryFileModel _getTemporaryFileModel;
+ private readonly ValidateFileType? _validateFileType;
+
+ internal delegate ContentSettings GetContentSettings();
+
+ internal delegate Guid? ParseTemporaryFileKey(object? editorValue);
+
+ internal delegate TemporaryFileModel? GetTemporaryFileModel(Guid temporaryFileKey);
+
+ internal delegate bool ValidateFileType(string extension, object? dataTypeConfiguration);
+
+ public TemporaryFileUploadValidator(
+ GetContentSettings getContentSettings,
+ ParseTemporaryFileKey parseTemporaryFileKey,
+ GetTemporaryFileModel getTemporaryFileModel,
+ ValidateFileType? validateFileType = null)
+ {
+ _getContentSettings = getContentSettings;
+ _parseTemporaryFileKey = parseTemporaryFileKey;
+ _getTemporaryFileModel = getTemporaryFileModel;
+ _validateFileType = validateFileType;
+ }
+
+ public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
+ {
+ Guid? temporaryFileKey = _parseTemporaryFileKey(value);
+ if (temporaryFileKey.HasValue == false)
+ {
+ yield break;
+ }
+
+ TemporaryFileModel? temporaryFile = _getTemporaryFileModel(temporaryFileKey.Value);
+ if (temporaryFile == null)
+ {
+ yield return new ValidationResult(
+ $"No temporary file was found for the key: {temporaryFileKey.Value}",
+ new[] { "value" });
+ }
+ else
+ {
+ var extension = Path.GetExtension(temporaryFile.FileName).TrimStart('.');
+ if (extension.IsNullOrWhiteSpace())
+ {
+ yield break;
+ }
+
+ ContentSettings contentSettings = _getContentSettings();
+ if (contentSettings.IsFileAllowedForUpload(extension) || (_validateFileType != null && _validateFileType(extension, dataTypeConfiguration) == false))
+ {
+ yield return new ValidationResult(
+ $"The file type for file name \"{temporaryFile.FileName}\" is not valid for upload",
+ new[] { "value" });
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
deleted file mode 100644
index 847ac9cff5..0000000000
--- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (c) Umbraco.
-// See LICENSE for more details.
-
-using System.ComponentModel.DataAnnotations;
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.Options;
-using Newtonsoft.Json.Linq;
-using Umbraco.Cms.Core.Configuration.Models;
-using Umbraco.Cms.Core.Services;
-using Umbraco.Extensions;
-
-namespace Umbraco.Cms.Core.PropertyEditors;
-
-internal class UploadFileTypeValidator : IValueValidator
-{
- private readonly ILocalizedTextService _localizedTextService;
- private ContentSettings _contentSettings;
-
- public UploadFileTypeValidator(
- ILocalizedTextService localizedTextService,
- IOptionsMonitor contentSettings)
- {
- _localizedTextService = localizedTextService;
- _contentSettings = contentSettings.CurrentValue;
-
- contentSettings.OnChange(x => _contentSettings = x);
- }
-
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration)
- {
- string? selectedFiles = null;
- if (value is JObject jobject && jobject["selectedFiles"] is JToken jToken)
- {
- selectedFiles = jToken.ToString();
- }
- else if (valueType?.InvariantEquals(ValueTypes.String) == true)
- {
- selectedFiles = value as string;
-
- if (string.IsNullOrWhiteSpace(selectedFiles))
- {
- yield break;
- }
- }
-
- var fileNames = selectedFiles?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
-
- if (fileNames == null || !fileNames.Any())
- {
- yield break;
- }
-
- foreach (var filename in fileNames)
- {
- if (IsValidFileExtension(filename, _contentSettings) is false ||
- IsAllowedInDataTypeConfiguration(filename, dataTypeConfiguration) is false)
- {
- // we only store a single value for this editor so the 'member' or 'field'
- // we'll associate this error with will simply be called 'value'
- yield return new ValidationResult(
- _localizedTextService.Localize("errors", "dissallowedMediaType"),
- new[] { "value" });
- }
- }
- }
-
- internal static bool IsValidFileExtension(string? fileName, ContentSettings contentSettings)
- {
- if (TryGetFileExtension(fileName, out var extension) is false)
- {
- return false;
- }
-
- return contentSettings.IsFileAllowedForUpload(extension);
- }
-
- internal static bool IsAllowedInDataTypeConfiguration(string? filename, object? dataTypeConfiguration)
- {
- if (TryGetFileExtension(filename, out var extension) is false)
- {
- return false;
- }
-
- if (dataTypeConfiguration is FileUploadConfiguration fileUploadConfiguration)
- {
- // If FileExtensions is empty and no allowed extensions have been specified, we allow everything.
- // If there are any extensions specified, we need to check that the uploaded extension is one of them.
- return fileUploadConfiguration.FileExtensions.IsCollectionEmpty() ||
- fileUploadConfiguration.FileExtensions.Any(x => x.Value?.InvariantEquals(extension) ?? false);
- }
-
- return false;
- }
-
- internal static bool TryGetFileExtension(string? fileName, [MaybeNullWhen(false)] out string extension)
- {
- extension = null;
- if (fileName is null || fileName.IndexOf('.') <= 0)
- {
- return false;
- }
-
- extension = fileName.GetFileExtension().TrimStart(".");
- return true;
- }
-}
diff --git a/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs
index 502450d714..2ec118dc97 100644
--- a/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs
+++ b/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs
@@ -68,7 +68,7 @@ public class JsonObjectConverter : JsonConverter
///
- [FileUploadCleanupFilter]
[MediaItemSaveValidation]
[OutgoingEditorModelEvent]
public ActionResult? PostSave(
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
{
- //Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
- //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
- //uploaded files to being *only* the actual file name (as it should be).
- if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any())
- {
- foreach (ContentPropertyFile file in contentItem.UploadedFiles)
- {
- file.FileName = Path.GetFileName(file.FileName);
- }
- }
-
//If we've reached here it means:
// * Our model has been bound
// * and validated
@@ -565,265 +552,6 @@ public class MediaController : ContentControllerBase
return _umbracoMapper.Map(f);
}
- ///
- /// Used to submit a media file
- ///
- ///
- ///
- /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data.
- ///
- public async Task PostAddFile([FromForm] string path, [FromForm] string currentFolder,
- [FromForm] string contentTypeAlias, List file)
- {
- await _postAddFileSemaphore.WaitOneAsync();
- var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
- //ensure it exists
- Directory.CreateDirectory(root);
-
- //must have a file
- if (file.Count == 0)
- {
- _postAddFileSemaphore.Release();
- return NotFound();
- }
-
- //get the string json from the request
- ActionResult? parentIdResult = await GetParentIdAsIntAsync(currentFolder, true);
- if (!(parentIdResult?.Result is null))
- {
- _postAddFileSemaphore.Release();
- return parentIdResult.Result;
- }
-
- var parentId = parentIdResult?.Value;
- if (!parentId.HasValue)
- {
- _postAddFileSemaphore.Release();
- return NotFound("The passed id doesn't exist");
- }
-
- var tempFiles = new PostedFiles();
-
- //in case we pass a path with a folder in it, we will create it and upload media to it.
- if (!string.IsNullOrEmpty(path))
- {
- if (!IsFolderCreationAllowedHere(parentId.Value))
- {
- AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "folderUploadNotAllowed"));
- _postAddFileSemaphore.Release();
- return Ok(tempFiles);
- }
-
- var folders = path.Split(Constants.CharArrays.ForwardSlash);
-
- for (var i = 0; i < folders.Length - 1; i++)
- {
- var folderName = folders[i];
- IMedia? folderMediaItem;
-
- //if uploading directly to media root and not a subfolder
- if (parentId == Constants.System.Root)
- {
- //look for matching folder
- folderMediaItem =
- _mediaService.GetRootMedia()?.FirstOrDefault(x =>
- x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
- if (folderMediaItem == null)
- {
- //if null, create a folder
- folderMediaItem =
- _mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
- _mediaService.Save(folderMediaItem);
- }
- }
- else
- {
- //get current parent
- IMedia? mediaRoot = _mediaService.GetById(parentId.Value);
-
- //if the media root is null, something went wrong, we'll abort
- if (mediaRoot == null)
- {
- _postAddFileSemaphore.Release();
- return Problem(
- "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId +
- " returned null");
- }
-
- //look for matching folder
- folderMediaItem = FindInChildren(mediaRoot.Id, folderName, Constants.Conventions.MediaTypes.Folder);
-
- if (folderMediaItem == null)
- {
- //if null, create a folder
- folderMediaItem = _mediaService.CreateMedia(folderName, mediaRoot,
- Constants.Conventions.MediaTypes.Folder);
- _mediaService.Save(folderMediaItem);
- }
- }
-
- //set the media root to the folder id so uploaded files will end there.
- parentId = folderMediaItem.Id;
- }
- }
-
- var mediaTypeAlias = string.Empty;
- var allMediaTypes = _mediaTypeService.GetAll().ToList();
- var allowedContentTypes = new HashSet();
-
- if (parentId != Constants.System.Root)
- {
- IMedia? mediaFolderItem = _mediaService.GetById(parentId.Value);
- IMediaType? mediaFolderType =
- allMediaTypes.FirstOrDefault(x => x.Alias == mediaFolderItem?.ContentType.Alias);
-
- if (mediaFolderType != null)
- {
- IMediaType? mediaTypeItem = null;
-
- if (mediaFolderType.AllowedContentTypes is not null)
- {
- foreach (ContentTypeSort allowedContentType in mediaFolderType.AllowedContentTypes)
- {
- IMediaType? checkMediaTypeItem =
- allMediaTypes.FirstOrDefault(x => x.Id == allowedContentType.Id.Value);
- if (checkMediaTypeItem is not null)
- {
- allowedContentTypes.Add(checkMediaTypeItem);
- }
-
- IPropertyType? fileProperty =
- checkMediaTypeItem?.CompositionPropertyTypes.FirstOrDefault(x =>
- x.Alias == Constants.Conventions.Media.File);
- if (fileProperty != null)
- {
- mediaTypeItem = checkMediaTypeItem;
- }
- }
- }
-
- //Only set the permission-based mediaType if we only allow 1 specific file under this parent.
- if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
- {
- mediaTypeAlias = mediaTypeItem.Alias;
- }
- }
- }
- else
- {
- var typesAllowedAtRoot = allMediaTypes.Where(x => x.AllowedAsRoot).ToList();
- allowedContentTypes.UnionWith(typesAllowedAtRoot);
- }
-
- //get the files
- foreach (IFormFile formFile in file)
- {
- var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
- var safeFileName = fileName.ToSafeFileName(ShortStringHelper);
- var ext = safeFileName[(safeFileName.LastIndexOf('.') + 1)..].ToLowerInvariant();
-
- if (!_contentSettings.IsFileAllowedForUpload(ext))
- {
- tempFiles.Notifications.Add(new BackOfficeNotification(
- _localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
- _localizedTextService.Localize("media", "disallowedFileType"),
- NotificationStyle.Warning));
- continue;
- }
-
- if (string.IsNullOrEmpty(mediaTypeAlias))
- {
- mediaTypeAlias = Constants.Conventions.MediaTypes.File;
-
- if (contentTypeAlias == Constants.Conventions.MediaTypes.AutoSelect)
- {
- // Look up MediaTypes
- foreach (IMediaType mediaTypeItem in allMediaTypes)
- {
- IPropertyType? fileProperty =
- mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x =>
- x.Alias == Constants.Conventions.Media.File);
- if (fileProperty == null)
- {
- continue;
- }
-
- Guid dataTypeKey = fileProperty.DataTypeKey;
- IDataType? dataType = _dataTypeService.GetDataType(dataTypeKey);
-
- if (dataType == null ||
- dataType.ConfigurationObject is not IFileExtensionsConfig fileExtensionsConfig)
- {
- continue;
- }
-
- List? fileExtensions = fileExtensionsConfig.FileExtensions;
- if (fileExtensions == null || fileExtensions.All(x => x.Value != ext))
- {
- continue;
- }
-
- mediaTypeAlias = mediaTypeItem.Alias;
- break;
- }
-
- // If media type is still File then let's check if it's an image.
- if (mediaTypeAlias == Constants.Conventions.MediaTypes.File &&
- _imageUrlGenerator.IsSupportedImageFormat(ext))
- {
- mediaTypeAlias = Constants.Conventions.MediaTypes.Image;
- }
- }
- else
- {
- mediaTypeAlias = contentTypeAlias;
- }
- }
-
- if (allowedContentTypes.Any(x => x.Alias == mediaTypeAlias) == false)
- {
- tempFiles.Notifications.Add(new BackOfficeNotification(
- _localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
- _localizedTextService.Localize("media", "disallowedMediaType", new[] { mediaTypeAlias }),
- NotificationStyle.Warning));
- continue;
- }
-
- var mediaItemName = fileName.ToFriendlyName();
-
- IMedia createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias,
- _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
-
- await using (Stream stream = formFile.OpenReadStream())
- {
- createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
- _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
- }
-
- Attempt saveResult = _mediaService.Save(createdMediaItem,
- _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
- if (saveResult == false)
- {
- AddCancelMessage(tempFiles,
- _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName);
- }
- }
-
- //Different response if this is a 'blueimp' request
- if (HttpContext.Request.Query.Any(x => x.Key == "origin"))
- {
- KeyValuePair origin = HttpContext.Request.Query.First(x => x.Key == "origin");
- if (origin.Value == "blueimp")
- {
- _postAddFileSemaphore.Release();
- return new JsonResult(tempFiles); //Don't output the angular xsrf stuff, blue imp doesn't like that
- }
- }
-
- _postAddFileSemaphore.Release();
- return Ok(tempFiles);
- }
-
private bool IsFolderCreationAllowedHere(int parentId)
{
var allMediaTypes = _mediaTypeService.GetAll().ToList();
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
index d619653740..09d129b2d2 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
@@ -276,7 +276,6 @@ public class MemberController : ContentControllerBase
///
/// The content item to save as a member
/// The resulting member display object
- [FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
[MemberSaveValidation]
public async Task> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
diff --git a/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs
deleted file mode 100644
index 474b1ef581..0000000000
--- a/src/Umbraco.Web.BackOffice/Filters/FileUploadCleanupFilterAttribute.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.Extensions.Logging;
-using Umbraco.Cms.Core.Models.ContentEditing;
-using Umbraco.Cms.Core.Models.Editors;
-using Umbraco.Extensions;
-
-namespace Umbraco.Cms.Web.BackOffice.Filters;
-
-///
-/// Checks if the parameter is IHaveUploadedFiles and then deletes any temporary saved files from file uploads
-/// associated with the request
-///
-public sealed class FileUploadCleanupFilterAttribute : TypeFilterAttribute
-{
- ///
- /// Constructor specifies if the filter should analyze the incoming or outgoing model
- ///
- ///
- public FileUploadCleanupFilterAttribute(bool incomingModel = true) : base(typeof(FileUploadCleanupFilter)) =>
- Arguments = new object[] { incomingModel };
-
- // We need to use IAsyncActionFilter even that we dont have any async because we need access to
- // context.ActionArguments, and this is only available on ActionExecutingContext and not on
- // ActionExecutedContext
-
- private class FileUploadCleanupFilter : IAsyncActionFilter
- {
- private readonly bool _incomingModel;
- private readonly ILogger _logger;
-
- public FileUploadCleanupFilter(ILogger logger, bool incomingModel)
- {
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _incomingModel = incomingModel;
- }
-
- public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
- {
- ActionExecutedContext resultContext = await next(); // We only to do stuff after the action is executed
-
- var tempFolders = new List();
-
- if (_incomingModel)
- {
- if (context.ActionArguments.Any())
- {
- if (context.ActionArguments.First().Value is IHaveUploadedFiles contentItem)
- {
- //cleanup any files associated
- foreach (ContentPropertyFile f in contentItem.UploadedFiles)
- {
- //track all temp folders so we can remove old files afterwards
- var dir = Path.GetDirectoryName(f.TempFilePath);
- if (dir is not null && tempFolders.Contains(dir) == false)
- {
- tempFolders.Add(dir);
- }
-
- try
- {
- File.Delete(f.TempFilePath);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete temp file {FileName}", f.TempFilePath);
- }
- }
- }
- }
- }
- else
- {
- if (resultContext == null)
- {
- _logger.LogWarning("The result context is null.");
- return;
- }
-
- if (resultContext.Result == null)
- {
- _logger.LogWarning("The result context's result is null");
- return;
- }
-
- if (!(resultContext.Result is ObjectResult objectResult))
- {
- _logger.LogWarning("Could not acquire the result context's result as ObjectResult");
- return;
- }
-
- if (objectResult.Value is IHaveUploadedFiles uploadedFiles)
- {
- if (uploadedFiles.UploadedFiles != null)
- {
- //cleanup any files associated
- foreach (ContentPropertyFile f in uploadedFiles.UploadedFiles)
- {
- if (f.TempFilePath.IsNullOrWhiteSpace() == false)
- {
- //track all temp folders so we can remove old files afterwards
- var dir = Path.GetDirectoryName(f.TempFilePath);
- if (dir is not null && tempFolders.Contains(dir) == false)
- {
- tempFolders.Add(dir);
- }
-
- _logger.LogDebug("Removing temp file {FileName}", f.TempFilePath);
-
- try
- {
- File.Delete(f.TempFilePath);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not delete temp file {FileName}", f.TempFilePath);
- }
-
- //clear out the temp path so it's not returned in the response
- f.TempFilePath = string.Empty;
- }
- else
- {
- _logger.LogWarning("The f.TempFilePath is null or whitespace!!??");
- }
- }
- }
- else
- {
- _logger.LogWarning("The uploadedFiles.UploadedFiles is null!!??");
- }
- }
- else
- {
- _logger.LogWarning(
- "The actionExecutedContext.Request.Content.Value is not IHaveUploadedFiles, it is {ObjectType}",
- objectResult.Value?.GetType());
- }
- }
- }
- }
-}
diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs
index 3632b42d8e..2904164b12 100644
--- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs
+++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models.ContentEditing;
-using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
@@ -18,7 +17,7 @@ internal class ContentModelBinderHelper
IJsonSerializer jsonSerializer,
IHostingEnvironment hostingEnvironment,
ModelBindingContext bindingContext)
- where T : class, IHaveUploadedFiles
+ where T : class
{
var modelName = bindingContext.ModelName;
@@ -101,15 +100,6 @@ internal class ContentModelBinderHelper
{
await formFile.CopyToAsync(stream);
}
-
- model.UploadedFiles.Add(new ContentPropertyFile
- {
- TempFilePath = tempFilePath,
- PropertyAlias = propAlias,
- Culture = culture,
- Segment = segment,
- FileName = fileName
- });
}
return model;