diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index 77a207c80da..8811e208b7a 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -40,6 +40,7 @@ //"OrchardCore_Media": { // "SupportedSizes": [ 16, 32, 50, 100, 160, 240, 480, 600, 1024, 2048 ], // "MaxBrowserCacheDays": 30, + // "MaxSecureFilesBrowserCacheDays": 0, // "MaxCacheDays": 365, // "ResizedCacheMaxStale": "01:00:00", // The time before a stale item is removed from the resized media cache, if not provided there is no cleanup. // "RemoteCacheMaxStale": "01:00:00", // The time before a stale item is removed from the remote media cache, if not provided there is no cleanup. diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/app.js b/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/app.js index d6a4768c81c..679dd452919 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/app.js +++ b/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/app.js @@ -27,7 +27,8 @@ function initializeMediaApplication(displayMediaApplication, mediaApplicationUrl name: $('#t-mediaLibrary').text(), path: '', folder: '', - isDirectory: true + isDirectory: true, + canCreateFolder: $('#allowNewRootFolders').val() === 'true' }; mediaApp = new Vue({ diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/folderComponent.js b/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/folderComponent.js index a7a627356d1..bbdb0e24a18 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/folderComponent.js +++ b/src/OrchardCore.Modules/OrchardCore.Media/Assets/js/app/MediaApp/folderComponent.js @@ -10,8 +10,8 @@ Vue.component('folder', {
{{model.name}}
- - + +
@@ -48,6 +48,12 @@ Vue.component('folder', { }, isRoot: function () { return this.model.path === ''; + }, + canCreateFolder: function () { + return this.model.canCreateFolder !== undefined ? this.model.canCreateFolder : true; + }, + canDeleteFolder: function () { + return this.model.canDeleteFolder !== undefined ? this.model.canDeleteFolder : true; } }, mounted: function () { diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminController.cs index 710af8c1616..7286ca87b59 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Controllers/AdminController.cs @@ -36,6 +36,7 @@ public class AdminController : Controller private readonly IChunkFileUploadService _chunkFileUploadService; private readonly IFileVersionProvider _fileVersionProvider; private readonly IServiceProvider _serviceProvider; + private readonly AttachedMediaFieldFileService _attachedMediaFieldFileService; public AdminController( IMediaFileStore mediaFileStore, @@ -48,7 +49,8 @@ public AdminController( IUserAssetFolderNameProvider userAssetFolderNameProvider, IChunkFileUploadService chunkFileUploadService, IFileVersionProvider fileVersionProvider, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + AttachedMediaFieldFileService attachedMediaFieldFileService) { _mediaFileStore = mediaFileStore; _mediaNameNormalizerService = mediaNameNormalizerService; @@ -61,6 +63,7 @@ public AdminController( _chunkFileUploadService = chunkFileUploadService; _fileVersionProvider = fileVersionProvider; _serviceProvider = serviceProvider; + _attachedMediaFieldFileService = attachedMediaFieldFileService; } [Admin("Media", "Media.Index")] @@ -74,7 +77,7 @@ public async Task Index() return View(); } - public async Task>> GetFolders(string path) + public async Task>> GetFolders(string path) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)) { @@ -101,7 +104,21 @@ public async Task>> GetFolders(string var allowed = _mediaFileStore.GetDirectoryContentAsync(path) .WhereAwait(async e => e.IsDirectory && await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)e.Path)); - return Ok(await allowed.ToListAsync()); + return Ok(await allowed.Select(folder => + { + var isSpecial = IsSpecialFolder(folder.Path); + return new MediaFolderViewModel() + { + Name = folder.Name, + Path = folder.Path, + DirectoryPath = folder.DirectoryPath, + IsDirectory = true, + LastModifiedUtc = folder.LastModifiedUtc, + Length = folder.Length, + CanCreateFolder = !isSpecial, + CanDeleteFolder = !isSpecial + }; + }).ToListAsync()); } public async Task>> GetMediaItems(string path, string extensions) @@ -136,7 +153,8 @@ public async Task>> GetMediaItems(string path, public async Task> GetMediaItem(string path) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia) + || (HttpContext.IsSecureMediaEnabled() && !await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)(path ?? string.Empty)))) { return Forbid(); } @@ -160,7 +178,8 @@ public async Task> GetMediaItem(string path) [MediaSizeLimit] public async Task Upload(string path, string extensions) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia)) + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia) + || (HttpContext.IsSecureMediaEnabled() && !await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)(path ?? string.Empty)))) { return Forbid(); } @@ -308,7 +327,8 @@ public async Task DeleteMedia(string path) public async Task MoveMedia(string oldPath, string newPath) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia) - || !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)oldPath)) + || !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)oldPath) + || !await _authorizationService.AuthorizeAsync(User, Permissions.ManageMediaFolder, (object)newPath)) { return Forbid(); } @@ -482,8 +502,11 @@ public object CreateFileResult(IFileStoreEntry mediaFile) }; } - public IActionResult MediaApplication(MediaApplicationViewModel model) + public async Task MediaApplication(MediaApplicationViewModel model) { + // Check if the user has access to new folders. If not, we hide the "create folder" button from the root folder. + model.AllowNewRootFolders = !HttpContext.IsSecureMediaEnabled() || await _authorizationService.AuthorizeAsync(User, SecureMediaPermissions.ViewMedia, (object)"_non-existent-path-87FD1922-8F88-4A33-9766-DA03E6E6F7BA"); + return View(model); } @@ -553,5 +576,8 @@ private async Task PreCacheRemoteMedia(IFileStoreEntry mediaFile, Stream stream localStream?.Dispose(); } } + + private bool IsSpecialFolder(string path) + => string.Equals(path, _mediaOptions.AssetsUsersFolder, StringComparison.OrdinalIgnoreCase) || string.Equals(path, _attachedMediaFieldFileService.MediaFieldsFolder, StringComparison.OrdinalIgnoreCase); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Drivers/MediaFieldDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Media/Drivers/MediaFieldDisplayDriver.cs index 7ededc49906..0bf6e9bf6c1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Drivers/MediaFieldDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Drivers/MediaFieldDisplayDriver.cs @@ -78,7 +78,7 @@ public override IDisplayResult Edit(MediaField field, BuildFieldEditorContext co } model.Paths = JConvert.SerializeObject(itemPaths, JOptions.CamelCase); - model.TempUploadFolder = _attachedMediaFieldFileService.MediaFieldsTempSubFolder; + model.TempUploadFolder = _attachedMediaFieldFileService.GetMediaFieldsTempSubFolder(); model.Field = field; model.Part = context.ContentPart; model.PartFieldDefinition = context.PartFieldDefinition; diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Events/SecureMediaFileStoreEventHandler.cs b/src/OrchardCore.Modules/OrchardCore.Media/Events/SecureMediaFileStoreEventHandler.cs new file mode 100644 index 00000000000..2283de53a95 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/Events/SecureMediaFileStoreEventHandler.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using OrchardCore.Environment.Cache; +using OrchardCore.Media.Core.Events; + +namespace OrchardCore.Media.Events; + +internal sealed class SecureMediaFileStoreEventHandler : MediaEventHandlerBase +{ + private readonly ISignal _signal; + + public SecureMediaFileStoreEventHandler(ISignal signal) + { + _signal = signal; + } + + public override Task MediaCreatedDirectoryAsync(MediaCreatedContext context) + { + if (context.Result) + { + SignalDirectoryChange(); + } + + return Task.CompletedTask; + } + + public override Task MediaDeletedDirectoryAsync(MediaDeletedContext context) + { + if (context.Result) + { + SignalDirectoryChange(); + } + + return Task.CompletedTask; + } + + private void SignalDirectoryChange() => _signal.DeferredSignalToken(nameof(SecureMediaPermissions)); +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Media/Manifest.cs index a38b240905e..408ff3693e0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Manifest.cs @@ -62,3 +62,14 @@ ], Category = "Content Management" )] + +[assembly: Feature( + Id = "OrchardCore.Media.Security", + Name = "Secure Media", + Description = "Adds permissions to restrict access to media folders.", + Dependencies = + [ + "OrchardCore.Media" + ], + Category = "Content Management" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Processing/MediaImageSharpConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media/Processing/MediaImageSharpConfiguration.cs index e9341fcf037..711b0f4d54f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Processing/MediaImageSharpConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Processing/MediaImageSharpConfiguration.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using OrchardCore.Media.Services; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; @@ -83,6 +84,26 @@ public void Configure(ImageSharpMiddlewareOptions options) return Task.CompletedTask; }; + + var onPrepareResponse = options.OnPrepareResponseAsync; + options.OnPrepareResponseAsync = async context => + { + if (onPrepareResponse is not null) + { + await onPrepareResponse(context); + } + + // Override cache control for secure files + if (context.IsSecureMediaRequested()) + { + var mediaOptions = context.RequestServices.GetRequiredService>().Value; + var secureCacheControl = mediaOptions.MaxSecureFilesBrowserCacheDays == 0 + ? "no-store" + : "public, must-revalidate, max-age=" + TimeSpan.FromDays(mediaOptions.MaxSecureFilesBrowserCacheDays).TotalSeconds.ToString(); + + context.Response.Headers.CacheControl = secureCacheControl; + } + }; } private static void ValidateTokenlessCommands(ImageCommandContext context, MediaOptions mediaOptions) diff --git a/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs b/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs new file mode 100644 index 00000000000..6e958017d46 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/SecureMediaPermissions.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OrchardCore.Environment.Cache; +using OrchardCore.Media.Services; +using OrchardCore.Modules; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Media +{ + [Feature("OrchardCore.Media.Security")] + public class SecureMediaPermissions : IPermissionProvider + { + // Note: The ManageMediaFolder permission grants all access, so viewing must be implied by it too. + public static readonly Permission ViewMedia = new("ViewMediaContent", "View media content in all folders", new[] { Permissions.ManageMediaFolder }); + public static readonly Permission ViewRootMedia = new("ViewRootMediaContent", "View media content in the root folder", new[] { ViewMedia }); + public static readonly Permission ViewOthersMedia = new("ViewOthersMediaContent", "View others media content", new[] { Permissions.ManageMediaFolder }); + public static readonly Permission ViewOwnMedia = new("ViewOwnMediaContent", "View own media content", new[] { ViewOthersMedia }); + + private static readonly Permission _viewMediaTemplate = new("ViewMediaContent_{0}", "View media content in folder '{0}'", new[] { ViewMedia }); + + private static Dictionary, Permission> _permissionsByFolder = new(); + private static readonly char[] _trimSecurePathChars = ['/', '\\', ' ']; + private static readonly ReadOnlyDictionary _permissionTemplates = new(new Dictionary() + { + { ViewMedia.Name, _viewMediaTemplate }, + }); + + private readonly MediaOptions _mediaOptions; + private readonly AttachedMediaFieldFileService _attachedMediaFieldFileService; + private readonly ISignal _signal; + private readonly IMediaFileStore _fileStore; + private readonly IMemoryCache _cache; + + public SecureMediaPermissions( + IOptions options, + IMediaFileStore fileStore, + IMemoryCache cache, + AttachedMediaFieldFileService attachedMediaFieldFileService, + ISignal signal) + { + _mediaOptions = options.Value; + _fileStore = fileStore; + _cache = cache; + _attachedMediaFieldFileService = attachedMediaFieldFileService; + _signal = signal; + } + + public async Task> GetPermissionsAsync() + { + return await _cache.GetOrCreateAsync(nameof(SecureMediaPermissions), async (entry) => + { + // Ensure to rebuild at least after some time, to detect directory changes from outside of + // the media module. The signal gets set if a directory is created or deleted in the Media + // Library directly. + entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)) + .AddExpirationToken(_signal.GetToken(nameof(SecureMediaPermissions))); + + return await GetPermissionsInternalAsync(); + }); + } + + public IEnumerable GetDefaultStereotypes() + { + return new[] + { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] + { + ViewMedia, + ViewOthersMedia + } + }, + new PermissionStereotype + { + Name = "Authenticated", + Permissions = new[] + { + ViewOwnMedia + } + }, + new PermissionStereotype + { + Name = "Anonymous", + Permissions = new[] + { + ViewMedia + } + } + }; + } + + /// + /// Returns a dynamic permission for a secure folder, based on a global view media permission template. + /// + internal static Permission ConvertToDynamicPermission(Permission permission) => _permissionTemplates.TryGetValue(permission.Name, out var result) ? result : null; + + internal static Permission CreateDynamicPermission(Permission template, string secureFolder) + { + ArgumentNullException.ThrowIfNull(template); + + secureFolder = secureFolder?.Trim(_trimSecurePathChars); + + var key = new ValueTuple(template.Name, secureFolder); + + if (_permissionsByFolder.TryGetValue(key, out var permission)) + { + return permission; + } + + permission = new Permission( + string.Format(template.Name, secureFolder), + string.Format(template.Description, secureFolder), + (template.ImpliedBy ?? Array.Empty()).Select(t => CreateDynamicPermission(t, secureFolder)) + ); + + var localPermissions = new Dictionary, Permission>(_permissionsByFolder) + { + [key] = permission, + }; + + _permissionsByFolder = localPermissions; + + return permission; + } + + private async Task> GetPermissionsInternalAsync() + { + // The ViewRootMedia permission must be implied by any subfolder permission. + var viewRootImpliedBy = new List(ViewRootMedia.ImpliedBy); + var result = new List() + { + ViewMedia, + new (ViewRootMedia.Name, ViewRootMedia.Description, viewRootImpliedBy), + ViewOthersMedia, + ViewOwnMedia + }; + + await foreach (var entry in _fileStore.GetDirectoryContentAsync()) + { + if (!entry.IsDirectory) + continue; + + if (entry.Name == _mediaOptions.AssetsUsersFolder || + entry.Name == _attachedMediaFieldFileService.MediaFieldsFolder) + continue; + + var folderPath = entry.Path; + + foreach (var template in _permissionTemplates) + { + var dynamicPermission = CreateDynamicPermission(template.Value, folderPath); + result.Add(dynamicPermission); + viewRootImpliedBy.Add(dynamicPermission); + } + } + + return result.AsEnumerable(); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/AttachedMediaFieldFileService.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/AttachedMediaFieldFileService.cs index 7fc4981d69e..43e63d89616 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Services/AttachedMediaFieldFileService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/AttachedMediaFieldFileService.cs @@ -19,15 +19,18 @@ public class AttachedMediaFieldFileService { private readonly IMediaFileStore _fileStore; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserAssetFolderNameProvider _userAssetFolderNameProvider; private readonly ILogger _logger; public AttachedMediaFieldFileService( IMediaFileStore fileStore, IHttpContextAccessor httpContextAccessor, + IUserAssetFolderNameProvider userAssetFolderNameProvider, ILogger logger) { _fileStore = fileStore; _httpContextAccessor = httpContextAccessor; + _userAssetFolderNameProvider = userAssetFolderNameProvider; _logger = logger; MediaFieldsFolder = "mediafields"; @@ -67,6 +70,13 @@ public async Task HandleFilesOnFieldUpdateAsync(List ite await MoveNewFilesToContentItemDirAndUpdatePathsAsync(items, contentItem); } + /// + /// Gets the per-user temporary upload directory. + /// + /// + public string GetMediaFieldsTempSubFolder() + => _fileStore.Combine(MediaFieldsTempSubFolder, _userAssetFolderNameProvider.GetUserAssetFolderName(_httpContextAccessor.HttpContext.User)); + private async Task EnsureGlobalDirectoriesAsync() { await _fileStore.TryCreateDirectoryAsync(MediaFieldsFolder); diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/ManageMediaFolderAuthorizationHandler.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/ManageMediaFolderAuthorizationHandler.cs index 13272151dda..55c936bc49c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Services/ManageMediaFolderAuthorizationHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/ManageMediaFolderAuthorizationHandler.cs @@ -91,7 +91,12 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext if (await authorizationService.AuthorizeAsync(context.User, permission)) { - context.Succeed(requirement); + // Check if viewing is allowed for this folder, if secure media is also enabled. + if (!_serviceProvider.IsSecureMediaEnabled() || + await authorizationService.AuthorizeAsync(context.User, SecureMediaPermissions.ViewMedia, (object)path)) + { + context.Succeed(requirement); + } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/MediaOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/MediaOptionsConfiguration.cs index c0fceea468d..498f275e942 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Services/MediaOptionsConfiguration.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/MediaOptionsConfiguration.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; using OrchardCore.Environment.Shell.Configuration; namespace OrchardCore.Media.Services @@ -54,6 +53,7 @@ public class MediaOptionsConfiguration : IConfigureOptions ]; private const int DefaultMaxBrowserCacheDays = 30; + private const int DefaultSecureFilesMaxBrowserCacheDays = 0; private const int DefaultMaxCacheDays = 365; private const int DefaultMaxFileSize = 30_000_000; @@ -92,6 +92,7 @@ public void Configure(MediaOptions options) StringComparer.OrdinalIgnoreCase); options.MaxBrowserCacheDays = section.GetValue("MaxBrowserCacheDays", DefaultMaxBrowserCacheDays); + options.MaxSecureFilesBrowserCacheDays = section.GetValue("MaxSecureFilesBrowserCacheDays", DefaultSecureFilesMaxBrowserCacheDays); options.MaxCacheDays = section.GetValue("MaxCacheDays", DefaultMaxCacheDays); options.ResizedCacheMaxStale = section.GetValue(nameof(options.ResizedCacheMaxStale)); options.RemoteCacheMaxStale = section.GetValue(nameof(options.RemoteCacheMaxStale)); @@ -108,6 +109,10 @@ public void Configure(MediaOptions options) // Use the same cache control header as ImageSharp does for resized images. var cacheControl = "public, must-revalidate, max-age=" + TimeSpan.FromDays(options.MaxBrowserCacheDays).TotalSeconds.ToString(); + // Secure files are not cached at all. + var secureCacheControl = options.MaxSecureFilesBrowserCacheDays == 0 + ? "no-store" + : "public, must-revalidate, max-age=" + TimeSpan.FromDays(options.MaxSecureFilesBrowserCacheDays).TotalSeconds.ToString(); options.StaticFileOptions = new StaticFileOptions { @@ -115,8 +120,8 @@ public void Configure(MediaOptions options) ServeUnknownFileTypes = true, OnPrepareResponse = ctx => { - ctx.Context.Response.Headers[HeaderNames.CacheControl] = cacheControl; - ctx.Context.Response.Headers[HeaderNames.ContentSecurityPolicy] = contentSecurityPolicy; + ctx.Context.Response.Headers.CacheControl = ctx.Context.IsSecureMediaRequested() ? secureCacheControl : cacheControl; + ctx.Context.Response.Headers.ContentSecurityPolicy = contentSecurityPolicy; } }; } diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaExtensions.cs new file mode 100644 index 00000000000..89136080bd6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaExtensions.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Media.Services +{ + internal static class SecureMediaExtensions + { + private const string IsSecureMediaKey = "IsSecureMedia"; + + public static bool IsSecureMediaEnabled(this IServiceProvider serviceProvider) + => serviceProvider.GetService(typeof(SecureMediaMarker)) is not null; + + public static bool IsSecureMediaEnabled(this HttpContext httpContext) + => httpContext.RequestServices.IsSecureMediaEnabled(); + + public static bool IsSecureMediaRequested(this HttpContext httpContext) + => httpContext.Items.ContainsKey(IsSecureMediaKey); + + public static void MarkAsSecureMediaRequested(this HttpContext httpContext) + => httpContext.Items[IsSecureMediaKey] = bool.TrueString; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMarker.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMarker.cs new file mode 100644 index 00000000000..4e552bcfc01 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMarker.cs @@ -0,0 +1,4 @@ +namespace OrchardCore.Media.Services +{ + internal sealed class SecureMediaMarker { } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMiddleware.cs new file mode 100644 index 00000000000..8ceb738c472 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/SecureMediaMiddleware.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using OrchardCore.Routing; + +namespace OrchardCore.Media.Services +{ + public class SecureMediaMiddleware + { + private readonly RequestDelegate _next; + private readonly PathString _assetsRequestPath; + + public SecureMediaMiddleware( + RequestDelegate next, + IOptions mediaOptions) + { + _next = next; + _assetsRequestPath = mediaOptions.Value.AssetsRequestPath; + } + + public async Task Invoke(HttpContext context, IAuthorizationService authorizationService, IAuthenticationService authenticationService) + { + var validateAssetsRequestPath = context.Request.Path.StartsWithNormalizedSegments(_assetsRequestPath, StringComparison.OrdinalIgnoreCase, out var subPath); + if (!validateAssetsRequestPath) + { + await _next(context); + + return; + } + + if (!(context.User.Identity?.IsAuthenticated ?? false)) + { + // Allow bearer (API) authentication too. + var authenticateResult = await authenticationService.AuthenticateAsync(context, "Api"); + + if (authenticateResult.Succeeded) + { + context.User = authenticateResult.Principal; + } + } + + if (await authorizationService.AuthorizeAsync(context.User, SecureMediaPermissions.ViewMedia, (object)subPath.ToString())) + { + await _next(context); + } + else + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFolderAuthorizationHandler.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFolderAuthorizationHandler.cs new file mode 100644 index 00000000000..f5dfe038346 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/ViewMediaFolderAuthorizationHandler.cs @@ -0,0 +1,231 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.ContentManagement; +using OrchardCore.FileStorage; +using OrchardCore.Security; +using OrchardCore.Security.Permissions; + +namespace OrchardCore.Media.Services +{ + /// + /// Checks if the user has related permission to view media in the path resource which is passed from AuthorizationHandler. + /// + public class ViewMediaFolderAuthorizationHandler : AuthorizationHandler + { + private const char PathSeparator = '/'; + + private static readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); + + private readonly IServiceProvider _serviceProvider; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IContentManager _contentManager; + private readonly IMediaFileStore _fileStore; + private readonly IUserAssetFolderNameProvider _userAssetFolderNameProvider; + private readonly MediaOptions _mediaOptions; + private readonly string _mediaFieldsFolder; + private readonly string _usersFolder; + + public ViewMediaFolderAuthorizationHandler( + IServiceProvider serviceProvider, + IHttpContextAccessor httpContextAccessor, + AttachedMediaFieldFileService attachedMediaFieldFileService, + IMediaFileStore fileStore, + IOptions options, + IUserAssetFolderNameProvider userAssetFolderNameProvider, + IContentManager contentManager) + { + _serviceProvider = serviceProvider; + _httpContextAccessor = httpContextAccessor; + _fileStore = fileStore; + _userAssetFolderNameProvider = userAssetFolderNameProvider; + _contentManager = contentManager; + _mediaOptions = options.Value; + _mediaFieldsFolder = EnsureTrailingSlash(attachedMediaFieldFileService.MediaFieldsFolder); + _usersFolder = EnsureTrailingSlash(_mediaOptions.AssetsUsersFolder); + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + { + if (context.HasSucceeded) + { + // This handler is not revoking any pre-existing grants. + return; + } + + if (requirement.Permission.Name != SecureMediaPermissions.ViewMedia.Name) + { + return; + } + + if (context.Resource is not string path) + { + return; + } + + path = Uri.UnescapeDataString(path); + + path = _fileStore.NormalizePath(path); + + // Permissions are only set for the root and the first folder tier. Only for users and + // media fields we will check sub folders too. + var i = path.IndexOf(PathSeparator); + var folderPath = i >= 0 ? path[..i] : path; + var directory = await _fileStore.GetDirectoryInfoAsync(folderPath); + if (directory is null && path.IndexOf(PathSeparator, folderPath.Length) < 0) + { + // This could be a new directory, or a new or existing file in the root folder. As we cannot directly determine + // whether a file is uploaded or a new directory is created, we will check against the list of allowed extensions. + // If none is matched, we assume a new directory is created, otherwise we will check the root access only. + // Note: The file path is currently not authorized during upload, only the folder is checked. Therefore checking + // the file extensions is not actually required, but let's leave this in case we add an authorization call later. + if (await _fileStore.GetFileInfoAsync(folderPath) is not null || + _mediaOptions.AllowedFileExtensions.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) + { + path = string.Empty; + } + } + + if (IsAuthorizedFolder("/", path)) + { + await AuthorizeAsync(context, requirement, SecureMediaPermissions.ViewRootMedia); + + return; + } + + if (IsAuthorizedFolder(_mediaFieldsFolder, path) || IsDescendantOfAuthorizedFolder(_mediaFieldsFolder, path)) + { + await AuthorizeAttachedMediaFieldsFolderAsync(context, requirement, path); + + return; + } + + if (IsAuthorizedFolder(_usersFolder, path) || IsDescendantOfAuthorizedFolder(_usersFolder, path)) + { + await AuthorizeUsersFolderAsync(context, requirement, path); + + return; + } + + // Create a dynamic permission for the folder path. This allows to give access to a specific folders only. + var template = SecureMediaPermissions.ConvertToDynamicPermission(SecureMediaPermissions.ViewMedia); + if (template != null) + { + var permission = SecureMediaPermissions.CreateDynamicPermission(template, folderPath); + await AuthorizeAsync(context, requirement, permission); + } + else + { + // Not a secure file + context.Succeed(requirement); + } + } + + private async Task AuthorizeAttachedMediaFieldsFolderAsync(AuthorizationHandlerContext context, PermissionRequirement requirement, string path) + { + var attachedMediaPathParts = path + .Substring(_mediaFieldsFolder.Length - 1) + .Split(PathSeparator, 3, StringSplitOptions.RemoveEmptyEntries); + + // Don't allow 'mediafields' directly. + if (attachedMediaPathParts.Length == 0) + return; + + if (string.Equals(attachedMediaPathParts[0], "temp", StringComparison.OrdinalIgnoreCase)) + { + // Authorize per-user temporary files + var userId = attachedMediaPathParts.Length > 1 ? attachedMediaPathParts[1] : null; + var userAssetsFolderName = EnsureTrailingSlash(_userAssetFolderNameProvider.GetUserAssetFolderName(context.User)); + + if (IsAuthorizedFolder(userAssetsFolderName, userId)) + { + await AuthorizeAsync(context, requirement, SecureMediaPermissions.ViewOwnMedia); + } + else + { + await AuthorizeAsync(context, requirement, SecureMediaPermissions.ViewOthersMedia); + } + } + else + { + // Authorize by using the content item permission. The user must have access to the content item to allow its media + // as well. + var contentItemId = attachedMediaPathParts.Length > 1 ? attachedMediaPathParts[1] : null; + var contentItem = !string.IsNullOrEmpty(contentItemId) ? await _contentManager.GetAsync(contentItemId) : null; + + // Disallow if content item is not found or allowed + if (contentItem is not null) + { + await AuthorizeAsync(context, requirement, Contents.CommonPermissions.ViewContent, contentItem); + } + } + } + + private async Task AuthorizeUsersFolderAsync(AuthorizationHandlerContext context, PermissionRequirement requirement, string path) + { + // We need to allow the _Users folder for own media access too. If someone uploads into this folder, we are screwed. + Permission permission; + if (path.IndexOf(PathSeparator) < 0) + { + permission = SecureMediaPermissions.ViewOwnMedia; + } + else + { + permission = SecureMediaPermissions.ViewOthersMedia; + + var userFolderName = _userAssetFolderNameProvider.GetUserAssetFolderName(context.User); + if (!string.IsNullOrEmpty(userFolderName)) + { + var userOwnFolder = EnsureTrailingSlash(_fileStore.Combine(_usersFolder, userFolderName)); + + if (IsAuthorizedFolder(userOwnFolder, path) || IsDescendantOfAuthorizedFolder(userOwnFolder, path)) + { + permission = SecureMediaPermissions.ViewOwnMedia; + } + } + } + + await AuthorizeAsync(context, requirement, permission); + } + + private async Task AuthorizeAsync(AuthorizationHandlerContext context, PermissionRequirement requirement, Permission permission, object resource = null) + { + var authorizationService = _serviceProvider.GetService(); + if (await authorizationService.AuthorizeAsync(context.User, permission, resource)) + { + // If anonymous access is also possible, we want to use default browser caching policies. + // Otherwise we set a marker which causes a different caching policy being used. + if ((context.User.Identity?.IsAuthenticated ?? false) && !await authorizationService.AuthorizeAsync(_anonymous, permission, resource)) + { + _httpContextAccessor.HttpContext.MarkAsSecureMediaRequested(); + } + + context.Succeed(requirement); + } + else + { + // Note: We don't want other authorization handlers to succeed the requirement. This would allow access to the + // users and attached media field folders, e.g. if the anonymous role has the "ViewMedia" permission set. + context.Fail(new AuthorizationFailureReason(this, "View media permission not granted")); + } + } + + private static bool IsAuthorizedFolder(string authorizedFolder, string childPath) + { + // Ensure end trailing slash. childPath is already normalized. + childPath += PathSeparator; + + return childPath.Equals(authorizedFolder, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsDescendantOfAuthorizedFolder(string authorizedFolder, string childPath) + => childPath.StartsWith(authorizedFolder, StringComparison.OrdinalIgnoreCase); + + private string EnsureTrailingSlash(string path) => _fileStore.NormalizePath(path) + PathSeparator; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs index 01e4fd1da3a..4c04988f3b5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs @@ -201,6 +201,14 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro var mediaOptions = serviceProvider.GetRequiredService>().Value; var mediaFileStoreCache = serviceProvider.GetService(); + // Move middleware into SecureMediaStartup if it is possible to insert it between the users and media + // module. See issue https://github.com/OrchardCMS/OrchardCore/issues/15716. + // Secure media file middleware, but only if the feature is enabled. + if (serviceProvider.IsSecureMediaEnabled()) + { + app.UseMiddleware(); + } + // FileStore middleware before ImageSharp, but only if a remote storage module has registered a cache provider. if (mediaFileStoreCache != null) { @@ -313,4 +321,18 @@ public override void ConfigureServices(IServiceCollection services) }); } } + + [Feature("OrchardCore.Media.Security")] + public class SecureMediaStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + // Marker service to easily detect if the feature has been enabled. + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + } + } } diff --git a/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaApplicationViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaApplicationViewModel.cs index 18644755a05..bbc3015e620 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaApplicationViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaApplicationViewModel.cs @@ -3,4 +3,6 @@ namespace OrchardCore.Media.ViewModels; public class MediaApplicationViewModel { public string Extensions { get; set; } + + public bool AllowNewRootFolders { get; set; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaFolderViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaFolderViewModel.cs new file mode 100644 index 00000000000..29f749250d9 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Media/ViewModels/MediaFolderViewModel.cs @@ -0,0 +1,22 @@ +using System; + +namespace OrchardCore.Media.ViewModels; + +public class MediaFolderViewModel +{ + public string Path { get; set; } + + public string Name { get; set; } + + public string DirectoryPath { get; set; } + + public long Length { get; set; } + + public DateTime LastModifiedUtc { get; set; } + + public bool IsDirectory { get; set; } + + public bool CanCreateFolder { get; set; } + + public bool CanDeleteFolder { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/MediaApplication.cshtml b/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/MediaApplication.cshtml index 88d36d84b66..2437cb140da 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/MediaApplication.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/MediaApplication.cshtml @@ -127,6 +127,9 @@ +@* Settings *@ + + @* Chunked file upload settings *@ diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/Options.cshtml b/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/Options.cshtml index d7af4513ccd..77a9a3e158c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/Options.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Media/Views/Admin/Options.cshtml @@ -34,6 +34,14 @@ +
+ +
+ + @T["The default number of days for the media cache control header for secure files."] +
+
+
@@ -92,5 +100,5 @@
@T["The folder under AssetsPath used to store users own media assets."] -
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.js b/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.js index bddbc52d838..e18e4388362 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.js +++ b/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.js @@ -30,7 +30,8 @@ function initializeMediaApplication(displayMediaApplication, mediaApplicationUrl name: $('#t-mediaLibrary').text(), path: '', folder: '', - isDirectory: true + isDirectory: true, + canCreateFolder: $('#allowNewRootFolders').val() === 'true' }; mediaApp = new Vue({ el: '#mediaApp', @@ -545,7 +546,7 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _ty function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // component Vue.component('folder', { - template: "\n
  • \n \n
      \n \n \n
    \n
  • \n "), + template: "\n
  • \n \n
      \n \n \n
    \n
  • \n "), props: { model: Object, selectedInMediaApp: Object, @@ -570,6 +571,12 @@ Vue.component('folder', { }, isRoot: function isRoot() { return this.model.path === ''; + }, + canCreateFolder: function canCreateFolder() { + return this.model.canCreateFolder !== undefined ? this.model.canCreateFolder : true; + }, + canDeleteFolder: function canDeleteFolder() { + return this.model.canDeleteFolder !== undefined ? this.model.canDeleteFolder : true; } }, mounted: function mounted() { diff --git a/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.min.js b/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.min.js index df24f4fcf2a..370f38585d3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.min.js +++ b/src/OrchardCore.Modules/OrchardCore.Media/wwwroot/Scripts/media.min.js @@ -1 +1 @@ -function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,n)}return i}function _objectSpread(e){for(var t=1;t-1}));switch(e.sortBy){case"size":t.sort((function(t,i){return e.sortAsc?t.size-i.size:i.size-t.size}));break;case"mime":t.sort((function(t,i){return e.sortAsc?t.mime.toLowerCase().localeCompare(i.mime.toLowerCase()):i.mime.toLowerCase().localeCompare(t.mime.toLowerCase())}));break;case"lastModify":t.sort((function(t,i){return e.sortAsc?t.lastModify-i.lastModify:i.lastModify-t.lastModify}));break;default:t.sort((function(t,i){return e.sortAsc?t.name.toLowerCase().localeCompare(i.name.toLowerCase()):i.name.toLowerCase().localeCompare(t.name.toLowerCase())}))}return t},hiddenCount:function(){return this.mediaItems.length-this.filteredMediaItems.length},thumbSize:function(){return this.smallThumbs?100:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs,selectedFolder:this.selectedFolder,gridView:this.gridView}},set:function(e){e&&(this.smallThumbs=e.smallThumbs,this.selectedFolder=e.selectedFolder,this.gridView=e.gridView)}}},watch:{currentPrefs:function(e){localStorage.setItem("mediaApplicationPrefs",JSON.stringify(e))},selectedFolder:function(e){this.mediaFilter="",this.selectedFolder=e,this.loadFolder(e)}},mounted:function(){this.$refs.rootFolder.toggle()},methods:{uploadUrl:function(){if(!this.selectedFolder)return null;var e=$("#uploadFiles").val();return e+(-1==e.indexOf("?")?"?":"&")+"path="+encodeURIComponent(this.selectedFolder.path)},selectRoot:function(){this.selectedFolder=this.root},loadFolder:function(e){this.errors=[],this.selectedMedias=[];var t=this,i=$("#getMediaItemsUrl").val();console.log(e.path),$.ajax({url:i+(-1==i.indexOf("?")?"?":"&")+"path="+encodeURIComponent(e.path),method:"GET",success:function(e){e.forEach((function(e){e.open=!1})),t.mediaItems=e,t.selectedMedias=[],t.sortBy="",t.sortAsc=!0},error:function(i){console.log("error loading folder:"+e.path),t.selectRoot()}})},selectAll:function(){this.selectedMedias=[];for(var e=0;e-1&&(t.mediaItems.splice(n,1),bus.$emit("mediaDeleted",t.selectedMedias[i]))}t.selectedMedias=[]},error:function(e){console.error(e.responseText)}})}}}))},deleteMediaItem:function(e){var t=this;e&&confirmDialog(_objectSpread(_objectSpread({},$("#deleteMedia").data()),{},{callback:function(i){i&&$.ajax({url:$("#deleteMediaUrl").val()+"?path="+encodeURIComponent(e.mediaPath),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(i){var n=t.mediaItems&&t.mediaItems.indexOf(e);n>-1&&(t.mediaItems.splice(n,1),bus.$emit("mediaDeleted",e))},error:function(e){console.error(e.responseText)}})}}))},handleDragStart:function(e,t){var i=[];this.selectedMedias.forEach((function(e){i.push(e.name)})),0==this.isMediaSelected(e)&&(i.push(e.name),this.selectedMedias.push(e)),t.dataTransfer.setData("mediaNames",JSON.stringify(i)),t.dataTransfer.setData("sourceFolder",this.selectedFolder.path),t.dataTransfer.setDragImage(this.dragDropThumbnail,10,10),t.dataTransfer.effectAllowed="move"},handleScrollWhileDrag:function(e){e.clientY<150&&window.scrollBy(0,-10),e.clientY>window.innerHeight-100&&window.scrollBy(0,10)},changeSort:function(e){this.sortBy==e?this.sortAsc=!this.sortAsc:(this.sortAsc=!0,this.sortBy=e)}}}),$("#create-folder-name").keypress((function(e){if(13==e.which)return $("#modalFooterOk").click(),!1})),$("#modalFooterOk").on("click",(function(e){var t=$("#create-folder-name").val();""!==t&&$.ajax({url:$("#createFolderUrl").val()+"?path="+encodeURIComponent(mediaApp.selectedFolder.path)+"&name="+encodeURIComponent(t),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(e){bus.$emit("addFolder",mediaApp.selectedFolder,e),bootstrap.Modal.getOrCreateInstance($("#createFolderModal")).hide()},error:function(e){$("#createFolderModal-errors").empty();var t=JSON.parse(e.responseText).value;$('').text(t).appendTo($("#createFolderModal-errors"))}})})),$("#renameMediaModalFooterOk").on("click",(function(e){var t=$("#new-item-name").val(),i=$("#old-item-name").val();if(""!==t){var n=mediaApp.selectedFolder.path+"/";"/"===n&&(n="");var a=n+t,o=n+i;if(a.toLowerCase()!==o.toLowerCase())$.ajax({url:$("#renameMediaUrl").val()+"?oldPath="+encodeURIComponent(o)+"&newPath="+encodeURIComponent(a),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(e){bootstrap.Modal.getOrCreateInstance($("#renameMediaModal")).hide(),bus.$emit("mediaRenamed",t,a,o,e.newUrl)},error:function(e){$("#renameMediaModal-errors").empty();var t=JSON.parse(e.responseText).value;$('').text(t).appendTo($("#renameMediaModal-errors"))}});else bootstrap.Modal.getOrCreateInstance($("#renameMediaModal")).hide()}})),e&&(document.getElementById("mediaApp").style.display=""),$(document).trigger("mediaApp:ready")},error:function(e){console.error(e.responseText)}}))}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,n)}return i}function _objectSpread(e){for(var t=1;t\n \n
      \n \n \n
    \n \n '),props:{model:Object,selectedInMediaApp:Object,level:Number},data:function(){return{open:!1,children:null,parent:null,isHovered:!1,padding:0}},computed:{empty:function(){return!this.children||0==this.children.length},isSelected:function(){return this.selectedInMediaApp.name==this.model.name&&this.selectedInMediaApp.path==this.model.path},isRoot:function(){return""===this.model.path}},mounted:function(){0==this.isRoot&&this.isAncestorOfSelectedFolder()&&this.toggle(),this.padding=this.level<3?16:16+8*this.level},created:function(){var e=this;bus.$on("deleteFolder",(function(t){if(e.children){var i=e.children&&e.children.indexOf(t);i>-1&&(e.children.splice(i,1),bus.$emit("folderDeleted"))}})),bus.$on("addFolder",(function(t,i){e.model==t&&(null!==e.children&&e.children.push(i),i.parent=e.model,bus.$emit("folderAdded",i))}))},methods:{isAncestorOfSelectedFolder:function(){for(parentFolder=mediaApp.selectedFolder;parentFolder;){if(parentFolder.path==this.model.path)return!0;parentFolder=parentFolder.parent}return!1},toggle:function(){this.open=!this.open,this.open&&!this.children&&this.loadChildren()},select:function(){bus.$emit("folderSelected",this.model),this.loadChildren()},createFolder:function(){bus.$emit("createFolderRequested")},deleteFolder:function(){bus.$emit("deleteFolderRequested")},loadChildren:function(){var e=this;0==this.open&&(this.open=!0),$.ajax({url:$("#getFoldersUrl").val()+"?path="+encodeURIComponent(e.model.path),method:"GET",success:function(t){e.children=t,e.children.forEach((function(t){t.parent=e.model}))},error:function(e){emtpy=!1,console.error(e.responseText)}})},handleDragOver:function(e){this.isHovered=!0},handleDragLeave:function(e){this.isHovered=!1},moveMediaToFolder:function(e,t){this.isHovered=!1;var i=JSON.parse(t.dataTransfer.getData("mediaNames"));if(!(i.length<1)){var n=t.dataTransfer.getData("sourceFolder"),a=e.path;""===n&&(n="root"),""===a&&(a="root"),n!==a?confirmDialog(_objectSpread(_objectSpread({},$("#moveMedia").data()),{},{callback:function(e){e&&$.ajax({url:$("#moveMediaListUrl").val(),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val(),mediaNames:i,sourceFolder:n,targetFolder:a},success:function(){bus.$emit("mediaListMoved")},error:function(e){console.error(e.responseText),bus.$emit("mediaListMoved",e.responseText)}})}})):alert($("#sameFolderMessage").val())}}}});var faIcons={image:"fa-regular fa-image",pdf:"fa-regular fa-file-pdf",word:"fa-regular fa-file-word",powerpoint:"fa-regular fa-file-powerpoint",excel:"fa-regular fa-file-excel",csv:"fa-regular fa-file",audio:"fa-regular fa-file-audio",video:"fa-regular fa-file-video",archive:"fa-regular fa-file-zipper",code:"fa-regular fa-file-code",text:"fa-regular fa-file-lines",file:"fa-regular fa-file"},faThumbnails={gif:faIcons.image,jpeg:faIcons.image,jpg:faIcons.image,png:faIcons.image,pdf:faIcons.pdf,doc:faIcons.word,docx:faIcons.word,ppt:faIcons.powerpoint,pptx:faIcons.powerpoint,xls:faIcons.excel,xlsx:faIcons.excel,csv:faIcons.csv,aac:faIcons.audio,mp3:faIcons.audio,ogg:faIcons.audio,avi:faIcons.video,flv:faIcons.video,mkv:faIcons.video,mp4:faIcons.video,webm:faIcons.video,gz:faIcons.archive,zip:faIcons.archive,css:faIcons.code,html:faIcons.code,js:faIcons.code,txt:faIcons.text};function getClassNameForExtension(e){return faThumbnails[e.toLowerCase()]||faIcons.file}function getExtensionForFilename(e){return e.slice(2+(e.lastIndexOf(".")-1>>>0))}function getClassNameForFilename(e){return getClassNameForExtension(getExtensionForFilename(e))}function initializeAttachedMediaField(e,t,i,n,a,o,r,s,l){var d,c=$(document.getElementById($(e).data("for"))).data("init"),u=$(e),m=u.attr("id");mediaFieldApps.push(d=new Vue({el:u.get(0),data:{mediaItems:[],selectedMedia:null,smallThumbs:!1,idPrefix:m,initialized:!1,allowMediaText:o,backupMediaText:"",allowAnchors:r,backupAnchor:null,mediaTextmodal:null,anchoringModal:null},created:function(){this.currentPrefs=JSON.parse(localStorage.getItem("mediaFieldPrefs"))},computed:{paths:{get:function(){var e=[];return this.initialized?(this.mediaItems.forEach((function(t){"not-found"!==t.mediaPath&&e.push({path:t.mediaPath,isRemoved:t.isRemoved,isNew:t.isNew,mediaText:t.mediaText,anchor:t.anchor,attachedFileName:t.attachedFileName})})),JSON.stringify(e)):JSON.stringify(c)},set:function(e){var t=this,i=e||[],a=$.Deferred(),o=[],r=0;i.forEach((function(e,i){o.push({name:" "+e.path,mime:"",mediaPath:"",anchor:e.anchor,attachedFileName:e.attachedFileName}),promise=$.when(a).done((function(){$.ajax({url:n+"?path="+encodeURIComponent(e.path),method:"GET",success:function(n){n.vuekey=n.name+i.toString(),n.mediaText=e.mediaText,n.anchor=e.anchor,n.attachedFileName=e.attachedFileName,o.splice(i,1,n),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)},error:function(n){console.log(JSON.stringify(n)),o.splice(i,1,{name:e.path,mime:"",mediaPath:"not-found",mediaText:"",anchor:{x:.5,y:.5},attachedFileName:e.attachedFileName}),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)}})}))})),a.resolve()}},fileSize:function(){return Math.round(this.selectedMedia.size/1024)},canAddMedia:function(){for(var e=[],t=0;t0&&a},thumbSize:function(){return this.smallThumbs?120:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs}},set:function(e){e&&(this.smallThumbs=e.smallThumbs)}}},mounted:function(){var e=this;e.paths=c,e.$on("selectAndDeleteMediaRequested",(function(t){e.selectAndDeleteMedia(t)})),e.$on("selectMediaRequested",(function(t){e.selectMedia(t)}));var n="#"+t,o=u.attr("id"),r=randomUUID();$(n).fileupload({limitConcurrentUploads:20,dropZone:$("#"+o),dataType:"json",url:i,maxChunkSize:l,add:function(t,i){var n,a=i.files.length;for(n=0;n0)for(var o=0;o1&&!1===a?(alert($("#onlyOneItemMessage").val()),d.mediaItems.push(i[0]),d.initialized=!0):(d.mediaItems=d.mediaItems.concat(i),d.initialized=!0)):alert(n)},error:function(e,t,i){console.log("Error on upload."),console.log(e),console.log(t),console.log(i)}}).on("fileuploadchunkbeforesend",(function(e,t){var i=t.files[0];t.blob=new File([t.blob],i.name,{type:i.type,lastModified:i.lastModified})}))},methods:{selectMedia:function(e){this.selectedMedia=e},getUniqueId:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}))},removeSelected:function(e){if(this.selectedMedia){var t=this.mediaItems&&this.mediaItems.indexOf(this.selectedMedia);t>-1&&(this.mediaItems[t].isRemoved=!0,this.mediaItems.splice(t,1))}else 1===this.mediaItems.length&&(this.mediaItems[t].isRemoved=!0,this.mediaItems.splice(0,1));this.selectedMedia=null},showMediaTextModal:function(e){this.mediaTextModal=new bootstrap.Modal(this.$refs.mediaTextModal),this.mediaTextModal.show(),this.backupMediaText=this.selectedMedia.mediaText},cancelMediaTextModal:function(e){this.mediaTextModal.hide(),this.selectedMedia.mediaText=this.backupMediaText},showAnchorModal:function(e){this.anchoringModal=new bootstrap.Modal(this.$refs.anchoringModal),this.anchoringModal.show(),this.selectedMedia.anchor={x:this.selectedMedia.anchor.x,y:this.selectedMedia.anchor.y},this.backupAnchor=this.selectedMedia.anchor},cancelAnchoringModal:function(e){this.anchoringModal.hide(),this.selectedMedia.anchor=this.backupAnchor},resetAnchor:function(e){this.selectedMedia.anchor={x:.5,y:.5}},onAnchorDrop:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},anchorLeft:function(){if(this.$refs.anchorImage&&this.$refs.modalBody&&this.selectedMedia){var e=(this.$refs.modalBody.clientWidth-this.$refs.anchorImage.clientWidth)/2,t=this.selectedMedia.anchor.x*this.$refs.anchorImage.clientWidth+e;return t<17?t=17:t-=8,t+"px"}return"0"},anchorTop:function(){if(this.$refs.anchorImage&&this.selectedMedia){var e=this.selectedMedia.anchor.y*this.$refs.anchorImage.clientHeight;return e<15?e=15:e+=5,e+"px"}return"0"},setAnchor:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},addMediaFiles:function(e){e.length>1&&!1===a?(alert($("#onlyOneItemMessage").val()),d.mediaItems.push(e[0]),d.initialized=!0):(d.mediaItems=d.mediaItems.concat(e),d.initialized=!0)},selectAndDeleteMedia:function(e){var t=this;t.selectedMedia=e,setTimeout((function(){t.removeSelected()}),100)}},watch:{mediaItems:{deep:!0,handler:function(){setTimeout((function(){$(document).trigger("contentpreview:render")}),100)}},currentPrefs:function(e){localStorage.setItem("mediaFieldPrefs",JSON.stringify(e))}}}))}function initializeMediaField(e,t,i,n,a,o){if(null!==e){var r,s=$(document.getElementById($(e).data("for"))).data("init"),l=$(e),d=l.attr("id");t.addEventListener("hidden.bs.modal",(function(e){$("#mediaApp").appendTo("body"),$("#mediaApp").hide()})),mediaFieldApps.push(r=new Vue({el:l.get(0),data:{mediaItems:[],selectedMedia:null,smallThumbs:!1,idPrefix:d,initialized:!1,allowMediaText:a,backupMediaText:"",allowAnchors:o,backupAnchor:null,mediaTextModal:null,anchoringModal:null},created:function(){this.currentPrefs=JSON.parse(localStorage.getItem("mediaFieldPrefs"))},computed:{paths:{get:function(){var e=[];return this.initialized?(this.mediaItems.forEach((function(t){"not-found"!==t.mediaPath&&e.push({path:t.mediaPath,mediaText:t.mediaText,anchor:t.anchor})})),JSON.stringify(e)):JSON.stringify(s)},set:function(e){var t=this,n=e||[],a=$.Deferred(),o=[],r=0;n.forEach((function(e,n){o.push({name:" "+e.path,mime:"",mediaPath:""}),promise=$.when(a).done((function(){$.ajax({url:i+"?path="+encodeURIComponent(e.path),method:"GET",success:function(i){i.vuekey=i.name+n.toString(),i.mediaText=e.mediaText,i.anchor=e.anchor,o.splice(n,1,i),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)},error:function(i){console.log(i),o.splice(n,1,{name:e.path,mime:"",mediaPath:"not-found",mediaText:"",anchor:{x:0,y:0}}),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)}})}))})),a.resolve()}},fileSize:function(){return Math.round(this.selectedMedia.size/1024)},canAddMedia:function(){return 0===this.mediaItems.length||this.mediaItems.length>0&&n},thumbSize:function(){return this.smallThumbs?120:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs}},set:function(e){e&&(this.smallThumbs=e.smallThumbs)}}},mounted:function(){var e=this;e.paths=s,e.$on("selectAndDeleteMediaRequested",(function(t){e.selectAndDeleteMedia(t)})),e.$on("selectMediaRequested",(function(t){e.selectMedia(t)})),e.$on("filesUploaded",(function(t){e.addMediaFiles(t)}))},methods:{selectMedia:function(e){this.selectedMedia=e},showModal:function(e){var i=this;if(i.canAddMedia){$("#mediaApp").appendTo($(t).find(".modal-body")),$("#mediaApp").show();var n=new bootstrap.Modal(t);n.show(),$(t).find(".mediaFieldSelectButton").off("click").on("click",(function(e){return i.addMediaFiles(mediaApp.selectedMedias),mediaApp.selectedMedias=[],n.hide(),!0}))}},showMediaTextModal:function(e){this.mediaTextModal=new bootstrap.Modal(this.$refs.mediaTextModal),this.mediaTextModal.show(),this.backupMediaText=this.selectedMedia.mediaText},cancelMediaTextModal:function(e){this.mediaTextModal.hide(),this.selectedMedia.mediaText=this.backupMediaText},showAnchorModal:function(e){this.anchoringModal=new bootstrap.Modal(this.$refs.anchoringModal),this.anchoringModal.show(),this.selectedMedia.anchor={x:this.selectedMedia.anchor.x,y:this.selectedMedia.anchor.y},this.backupAnchor=this.selectedMedia.anchor},cancelAnchoringModal:function(e){this.anchoringModal.hide(),this.selectedMedia.anchor=this.backupAnchor},resetAnchor:function(e){this.selectedMedia.anchor={x:.5,y:.5}},onAnchorDrop:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},anchorLeft:function(){if(this.$refs.anchorImage&&this.$refs.modalBody&&this.selectedMedia){var e=(this.$refs.modalBody.clientWidth-this.$refs.anchorImage.clientWidth)/2,t=this.selectedMedia.anchor.x*this.$refs.anchorImage.clientWidth+e,i=Math.round(this.$refs.modalBody.querySelector(".icon-media-anchor").clientWidth);return Number.isInteger(i)&&(t-=i/2),t+"px"}return"0"},anchorTop:function(){return this.$refs.anchorImage&&this.selectedMedia?this.selectedMedia.anchor.y*this.$refs.anchorImage.clientHeight+"px":"0"},setAnchor:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},addMediaFiles:function(e){e.length>1&&!1===n?(alert($("#onlyOneItemMessage").val()),r.mediaItems.push(e[0]),r.initialized=!0):(r.mediaItems=r.mediaItems.concat(e),r.initialized=!0)},removeSelected:function(e){if(this.selectedMedia){var t=this.mediaItems&&this.mediaItems.indexOf(this.selectedMedia);t>-1&&this.mediaItems.splice(t,1)}else 1===this.mediaItems.length&&this.mediaItems.splice(0,1);this.selectedMedia=null},selectAndDeleteMedia:function(e){var t=this;t.selectedMedia=e,setTimeout((function(){t.removeSelected()}),100)}},watch:{mediaItems:{deep:!0,handler:function(){setTimeout((function(){$(document).trigger("contentpreview:render")}),100)}},currentPrefs:function(e){localStorage.setItem("mediaFieldPrefs",JSON.stringify(e))}}}))}}Vue.component("media-items-grid",{template:'\n
      \n
    1. \n
      \n \n \n
      \n
      \n \n \n \n {{ media.name }}\n
      \n
    2. \n
    \n ',data:function(){return{T:{}}},props:{filteredMediaItems:Array,selectedMedias:Array,thumbSize:Number},created:function(){this.T.editButton=$("#t-edit-button").val(),this.T.deleteButton=$("#t-delete-button").val()},methods:{isMediaSelected:function(e){return this.selectedMedias.some((function(t,i,n){return t.url.toLowerCase()===e.url.toLowerCase()}))},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},toggleSelectionOfMedia:function(e){bus.$emit("mediaToggleRequested",e)},renameMedia:function(e){bus.$emit("renameMediaRequested",e)},deleteMedia:function(e){bus.$emit("deleteMediaRequested",e)},dragStart:function(e,t){bus.$emit("mediaDragStartRequested",e,t)},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}}),Vue.component("media-items-table",{template:'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{ T.imageHeader }}\n {{ T.nameHeader }}\n \n \n {{ T.lastModifyHeader }} \n \n \n \n {{ T.sizeHeader }}\n \n \n \n \n {{ T.typeHeader }}\n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    {{ printDateTime(media.lastModify) }}
    \n
    \n
    {{ isNaN(media.size)? 0 : Math.round(media.size / 1024) }} KB
    \n
    \n
    {{ media.mime }}
    \n
    \n ',data:function(){return{T:{}}},props:{sortBy:String,sortAsc:Boolean,filteredMediaItems:Array,selectedMedias:Array,thumbSize:Number},created:function(){var e=this;e.T.imageHeader=$("#t-image-header").val(),e.T.nameHeader=$("#t-name-header").val(),e.T.lastModifyHeader=$("#t-lastModify-header").val(),e.T.sizeHeader=$("#t-size-header").val(),e.T.typeHeader=$("#t-type-header").val(),e.T.editButton=$("#t-edit-button").val(),e.T.deleteButton=$("#t-delete-button").val(),e.T.viewButton=$("#t-view-button").val()},methods:{isMediaSelected:function(e){return this.selectedMedias.some((function(t,i,n){return t.url.toLowerCase()===e.url.toLowerCase()}))},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},changeSort:function(e){bus.$emit("sortChangeRequested",e)},toggleSelectionOfMedia:function(e){bus.$emit("mediaToggleRequested",e)},renameMedia:function(e){bus.$emit("renameMediaRequested",e)},deleteMedia:function(e){bus.$emit("deleteMediaRequested",e)},dragStart:function(e,t){bus.$emit("mediaDragStartRequested",e,t)},printDateTime:function(e){return new Date(e).toLocaleString()},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}}),Vue.component("pager",{template:'\n
    \n \n \n
    \n ',props:{sourceItems:Array},data:function(){return{pageSize:10,pageSizeOptions:[10,30,50,100],current:0,T:{}}},created:function(){var e=this;e.T.pagerFirstButton=$("#t-pager-first-button").val(),e.T.pagerPreviousButton=$("#t-pager-previous-button").val(),e.T.pagerNextButton=$("#t-pager-next-button").val(),e.T.pagerLastButton=$("#t-pager-last-button").val(),e.T.pagerPageSizeLabel=$("#t-pager-page-size-label").val(),e.T.pagerPageLabel=$("#t-pager-page-label").val(),e.T.pagerTotalLabel=$("#t-pager-total-label").val()},methods:{next:function(){this.current=this.current+1},previous:function(){this.current=this.current-1},goFirst:function(){this.current=0},goLast:function(){this.current=this.totalPages-1},goTo:function(e){this.current=e}},computed:{total:function(){return this.sourceItems?this.sourceItems.length:0},totalPages:function(){var e=Math.ceil(this.total/this.pageSize);return e>0?e:1},isLastPage:function(){return this.current+1>=this.totalPages},isFirstPage:function(){return 0===this.current},canDoNext:function(){return!this.isLastPage},canDoPrev:function(){return!this.isFirstPage},canDoFirst:function(){return!this.isFirstPage},canDoLast:function(){return!this.isLastPage},itemsInCurrentPage:function(){var e=this.pageSize*this.current,t=e+this.pageSize,i=this.sourceItems.slice(e,t);return bus.$emit("pagerEvent",i),i},pageLinks:function(){var e=[];e.push(this.current+1);var t=this.current>0?this.current:-1;e.unshift(t);var i=this.current>1?this.current-1:-1;e.unshift(i);var n=this.totalPages-this.current>1?this.current+2:-1;e.push(n);var a=this.totalPages-this.current>2?this.current+3:-1;return e.push(a),e}},watch:{sourceItems:function(){this.current=0},pageSize:function(){this.current=0}}}),Vue.component("sortIndicator",{template:'\n
    \n \n \n
    \n ',props:{colname:String,selectedcolname:String,asc:Boolean},computed:{isActive:function(){return this.colname.toLowerCase()==this.selectedcolname.toLowerCase()}}}),Vue.component("mediaFieldThumbsContainer",{template:'
    {{T.noImages}}
  • {{ media.isNew ? media.name.substr(36) : media.name }}
    {{ T.mediaNotFound }} {{ T.discardWarning }}
    {{ media.name }}
  • ',data:function(){return{T:{}}},props:{mediaItems:Array,selectedMedia:Object,thumbSize:Number,idPrefix:String},created:function(){var e=this;e.T.mediaNotFound=$("#t-media-not-found").val(),e.T.discardWarning=$("#t-discard-warning").val(),e.T.noImages=$("#t-no-images").val()},methods:{selectAndDeleteMedia:function(e){this.$parent.$emit("selectAndDeleteMediaRequested",e)},selectMedia:function(e){this.$parent.$emit("selectMediaRequested",e)},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}});var mediaFieldApps=[];function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function randomUUID(){return"object"===("undefined"==typeof crypto?"undefined":_typeof(crypto))&&"function"==typeof crypto.randomUUID?crypto.randomUUID():([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(function(e){return(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)}))}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}Vue.component("upload",{template:'

    {{ model.name }}

    Error: {{ model.errorMessage }}
    ',props:{model:Object,uploadInputId:String},mounted:function(){var e,t=this,i=document.getElementById(null!==(e=t.uploadInputId)&&void 0!==e?e:"fileupload");$(i).bind("fileuploadprogress",(function(e,i){i.files[0].name===t.model.name&&(t.model.percentage=parseInt(i.loaded/i.total*100,10))})),$(i).bind("fileuploaddone",(function(e,i){i.files[0].name===t.model.name&&(i.result.files[0].error?t.handleFailure(i.files[0].name,i.result.files[0].error):bus.$emit("removalRequest",t.model))})),$(i).bind("fileuploadfail",(function(e,i){i.files[0].name===t.model.name&&t.handleFailure(i.files[0].name,$("#t-error").val())}))},methods:{handleFailure:function(e,t){e===this.model.name&&(this.model.errorMessage=t,bus.$emit("ErrorOnUpload",this.model))},dismissWarning:function(){bus.$emit("removalRequest",this.model)}}}),Vue.component("uploadList",{template:'
    {{ T.uploads }} (Pending: {{ pendingCount }}) ( {{ T.errors }}: {{ errorCount }} / {{ T.clearErrors }} )
    ',data:function(){return{files:[],T:{},expanded:!1,pendingCount:0,errorCount:0}},props:{uploadInputId:String},created:function(){var e=this;e.T.uploads=$("#t-uploads").val(),e.T.errors=$("#t-errors").val(),e.T.clearErrors=$("#t-clear-errors").val()},computed:{fileCount:function(){return this.files.length}},mounted:function(){var e,t=this,i=document.getElementById(null!==(e=t.uploadInputId)&&void 0!==e?e:"fileupload");$(i).bind("fileuploadadd",(function(e,i){i.files&&i.files.forEach((function(e){t.files.some((function(t){return t.name==e.name}))?console.error("A file with the same name is already on the queue:"+e.name):t.files.push({name:e.name,percentage:0,errorMessage:""})}))})),bus.$on("removalRequest",(function(e){t.files.forEach((function(t,i,n){t.name==e.name&&n.splice(i,1)}))})),bus.$on("ErrorOnUpload",(function(e){t.updateCount()}))},methods:{updateCount:function(){this.errorCount=this.files.filter((function(e){return""!=e.errorMessage})).length,this.pendingCount=this.files.length-this.errorCount,this.files.length<1&&(this.expanded=!1)},clearErrors:function(){this.files=this.files.filter((function(e){return""==e.errorMessage}))}},watch:{files:function(){this.updateCount()}}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery","jquery-ui/ui/widget"],e):"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?e(require("jquery"),require("./vendor/jquery.ui.widget")):e(window.jQuery)}((function(e){"use strict";function t(t){var i="dragover"===t;return function(n){n.dataTransfer=n.originalEvent&&n.originalEvent.dataTransfer;var a=n.dataTransfer;a&&-1!==e.inArray("Files",a.types)&&!1!==this._trigger(t,e.Event(t,{delegatedEvent:n}))&&(n.preventDefault(),i&&(a.dropEffect="copy"))}}var i;e.support.fileInput=!(new RegExp("(Android (1\\.[0156]|2\\.[01]))|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)|(w(eb)?OSBrowser)|(webOS)|(Kindle/(1\\.0|2\\.[05]|3\\.0))").test(window.navigator.userAgent)||e('').prop("disabled")),e.support.xhrFileUpload=!(!window.ProgressEvent||!window.FileReader),e.support.xhrFormDataFileUpload=!!window.FormData,e.support.blobSlice=window.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice),e.widget("blueimp.fileupload",{options:{dropZone:e(document),pasteZone:void 0,fileInput:void 0,replaceFileInput:!0,paramName:void 0,singleFileUploads:!0,limitMultiFileUploads:void 0,limitMultiFileUploadSize:void 0,limitMultiFileUploadSizeOverhead:512,sequentialUploads:!1,limitConcurrentUploads:void 0,forceIframeTransport:!1,redirect:void 0,redirectParamName:void 0,postMessage:void 0,multipart:!0,maxChunkSize:void 0,uploadedBytes:void 0,recalculateProgress:!0,progressInterval:100,bitrateInterval:500,autoUpload:!0,uniqueFilenames:void 0,messages:{uploadedBytes:"Uploaded bytes exceed file size"},i18n:function(t,i){return t=this.messages[t]||t.toString(),i&&e.each(i,(function(e,i){t=t.replace("{"+e+"}",i)})),t},formData:function(e){return e.serializeArray()},add:function(t,i){if(t.isDefaultPrevented())return!1;(i.autoUpload||!1!==i.autoUpload&&e(this).fileupload("option","autoUpload"))&&i.process().done((function(){i.submit()}))},processData:!1,contentType:!1,cache:!1,timeout:0},_promisePipe:(i=e.fn.jquery.split("."),Number(i[0])>1||Number(i[1])>7?"then":"pipe"),_specialOptions:["fileInput","dropZone","pasteZone","multipart","forceIframeTransport"],_blobSlice:e.support.blobSlice&&function(){return(this.slice||this.webkitSlice||this.mozSlice).apply(this,arguments)},_BitrateTimer:function(){this.timestamp=Date.now?Date.now():(new Date).getTime(),this.loaded=0,this.bitrate=0,this.getBitrate=function(e,t,i){var n=e-this.timestamp;return(!this.bitrate||!i||n>i)&&(this.bitrate=(t-this.loaded)*(1e3/n)*8,this.loaded=t,this.timestamp=e),this.bitrate}},_isXHRUpload:function(t){return!t.forceIframeTransport&&(!t.multipart&&e.support.xhrFileUpload||e.support.xhrFormDataFileUpload)},_getFormData:function(t){var i;return"function"===e.type(t.formData)?t.formData(t.form):e.isArray(t.formData)?t.formData:"object"===e.type(t.formData)?(i=[],e.each(t.formData,(function(e,t){i.push({name:e,value:t})})),i):[]},_getTotal:function(t){var i=0;return e.each(t,(function(e,t){i+=t.size||1})),i},_initProgressObject:function(t){var i={loaded:0,total:0,bitrate:0};t._progress?e.extend(t._progress,i):t._progress=i},_initResponseObject:function(e){var t;if(e._response)for(t in e._response)Object.prototype.hasOwnProperty.call(e._response,t)&&delete e._response[t];else e._response={}},_onProgress:function(t,i){if(t.lengthComputable){var n,a=Date.now?Date.now():(new Date).getTime();if(i._time&&i.progressInterval&&a-i._time").prop("href",t.url).prop("host");t.dataType="iframe "+(t.dataType||""),t.formData=this._getFormData(t),t.redirect&&i&&i!==location.host&&t.formData.push({name:t.redirectParamName||"redirect",value:t.redirect})},_initDataSettings:function(e){this._isXHRUpload(e)?(this._chunkedUpload(e,!0)||(e.data||this._initXHRData(e),this._initProgressListener(e)),e.postMessage&&(e.dataType="postmessage "+(e.dataType||""))):this._initIframeSettings(e)},_getParamName:function(t){var i=e(t.fileInput),n=t.paramName;return n?e.isArray(n)||(n=[n]):(n=[],i.each((function(){for(var t=e(this),i=t.prop("name")||"files[]",a=(t.prop("files")||[1]).length;a;)n.push(i),a-=1})),n.length||(n=[i.prop("name")||"files[]"])),n},_initFormSettings:function(t){t.form&&t.form.length||(t.form=e(t.fileInput.prop("form")),t.form.length||(t.form=e(this.options.fileInput.prop("form")))),t.paramName=this._getParamName(t),t.url||(t.url=t.form.prop("action")||location.href),t.type=(t.type||"string"===e.type(t.form.prop("method"))&&t.form.prop("method")||"").toUpperCase(),"POST"!==t.type&&"PUT"!==t.type&&"PATCH"!==t.type&&(t.type="POST"),t.formAcceptCharset||(t.formAcceptCharset=t.form.attr("accept-charset"))},_getAJAXSettings:function(t){var i=e.extend({},this.options,t);return this._initFormSettings(i),this._initDataSettings(i),i},_getDeferredState:function(e){return e.state?e.state():e.isResolved()?"resolved":e.isRejected()?"rejected":"pending"},_enhancePromise:function(e){return e.success=e.done,e.error=e.fail,e.complete=e.always,e},_getXHRPromise:function(t,i,n){var a=e.Deferred(),o=a.promise();return i=i||this.options.context||o,!0===t?a.resolveWith(i,n):!1===t&&a.rejectWith(i,n),o.abort=a.promise,this._enhancePromise(o)},_addConvenienceMethods:function(t,i){var n=this,a=function(t){return e.Deferred().resolveWith(n,t).promise()};i.process=function(t,o){return(t||o)&&(i._processQueue=this._processQueue=(this._processQueue||a([this]))[n._promisePipe]((function(){return i.errorThrown?e.Deferred().rejectWith(n,[i]).promise():a(arguments)}))[n._promisePipe](t,o)),this._processQueue||a([this])},i.submit=function(){return"pending"!==this.state()&&(i.jqXHR=this.jqXHR=!1!==n._trigger("submit",e.Event("submit",{delegatedEvent:t}),this)&&n._onSend(t,this)),this.jqXHR||n._getXHRPromise()},i.abort=function(){return this.jqXHR?this.jqXHR.abort():(this.errorThrown="abort",n._trigger("fail",null,this),n._getXHRPromise(!1))},i.state=function(){return this.jqXHR?n._getDeferredState(this.jqXHR):this._processQueue?n._getDeferredState(this._processQueue):void 0},i.processing=function(){return!this.jqXHR&&this._processQueue&&"pending"===n._getDeferredState(this._processQueue)},i.progress=function(){return this._progress},i.response=function(){return this._response}},_getUploadedBytes:function(e){var t=e.getResponseHeader("Range"),i=t&&t.split("-"),n=i&&i.length>1&&parseInt(i[1],10);return n&&n+1},_chunkedUpload:function(t,i){t.uploadedBytes=t.uploadedBytes||0;var n,a,o=this,r=t.files[0],s=r.size,l=t.uploadedBytes,d=t.maxChunkSize||s,c=this._blobSlice,u=e.Deferred(),m=u.promise();return!(!(this._isXHRUpload(t)&&c&&(l||("function"===e.type(d)?d(t):d)=s?(r.error=t.i18n("uploadedBytes"),this._getXHRPromise(!1,t.context,[null,"error",r.error])):(a=function(){var i=e.extend({},t),m=i._progress.loaded;i.blob=c.call(r,l,l+("function"===e.type(d)?d(i):d),r.type),i.chunkSize=i.blob.size,i.contentRange="bytes "+l+"-"+(l+i.chunkSize-1)+"/"+s,o._trigger("chunkbeforesend",null,i),o._initXHRData(i),o._initProgressListener(i),n=(!1!==o._trigger("chunksend",null,i)&&e.ajax(i)||o._getXHRPromise(!1,i.context)).done((function(n,r,d){l=o._getUploadedBytes(d)||l+i.chunkSize,m+i.chunkSize-i._progress.loaded&&o._onProgress(e.Event("progress",{lengthComputable:!0,loaded:l-i.uploadedBytes,total:l-i.uploadedBytes}),i),t.uploadedBytes=i.uploadedBytes=l,i.result=n,i.textStatus=r,i.jqXHR=d,o._trigger("chunkdone",null,i),o._trigger("chunkalways",null,i),ls._sending)for(var n=s._slots.shift();n;){if("pending"===s._getDeferredState(n)){n.resolve();break}n=s._slots.shift()}0===s._active&&s._trigger("stop")}))};return this._beforeSend(t,l),this.options.sequentialUploads||this.options.limitConcurrentUploads&&this.options.limitConcurrentUploads<=this._sending?(this.options.limitConcurrentUploads>1?(o=e.Deferred(),this._slots.push(o),r=o[s._promisePipe](d)):(this._sequence=this._sequence[s._promisePipe](d,d),r=this._sequence),r.abort=function(){return a=[void 0,"abort","abort"],n?n.abort():(o&&o.rejectWith(l.context,a),d())},this._enhancePromise(r)):d()},_onAdd:function(t,i){var n,a,o,r,s=this,l=!0,d=e.extend({},this.options,i),c=i.files,u=c.length,m=d.limitMultiFileUploads,p=d.limitMultiFileUploadSize,f=d.limitMultiFileUploadSizeOverhead,h=0,g=this._getParamName(d),v=0;if(!u)return!1;if(p&&void 0===c[0].size&&(p=void 0),(d.singleFileUploads||m||p)&&this._isXHRUpload(d))if(d.singleFileUploads||p||!m)if(!d.singleFileUploads&&p)for(o=[],n=[],r=0;rp||m&&r+1-v>=m)&&(o.push(c.slice(v,r+1)),(a=g.slice(v,r+1)).length||(a=g),n.push(a),v=r+1,h=0);else n=g;else for(o=[],n=[],r=0;r").append(n)[0].reset(),i.after(n).detach(),a&&n.trigger("focus"),e.cleanData(i.off("remove")),this.options.fileInput=this.options.fileInput.map((function(e,t){return t===i[0]?n[0]:t})),i[0]===this.element[0]&&(this.element=n)},_handleFileTreeEntry:function(t,i){var n,a=this,o=e.Deferred(),r=[],s=function(e){e&&!e.entry&&(e.entry=t),o.resolve([e])};return i=i||"",t.isFile?t._file?(t._file.relativePath=i,o.resolve(t._file)):t.file((function(e){e.relativePath=i,o.resolve(e)}),s):t.isDirectory?(n=t.createReader(),function e(){n.readEntries((function(n){n.length?(r=r.concat(n),e()):function(e){a._handleFileTreeEntries(e,i+t.name+"/").done((function(e){o.resolve(e)})).fail(s)}(r)}),s)}()):o.resolve([]),o.promise()},_handleFileTreeEntries:function(t,i){var n=this;return e.when.apply(e,e.map(t,(function(e){return n._handleFileTreeEntry(e,i)})))[this._promisePipe]((function(){return Array.prototype.concat.apply([],arguments)}))},_getDroppedFiles:function(t){var i=(t=t||{}).items;return i&&i.length&&(i[0].webkitGetAsEntry||i[0].getAsEntry)?this._handleFileTreeEntries(e.map(i,(function(e){var t;return e.webkitGetAsEntry?((t=e.webkitGetAsEntry())&&(t._file=e.getAsFile()),t):e.getAsEntry()}))):e.Deferred().resolve(e.makeArray(t.files)).promise()},_getSingleFileInputFiles:function(t){var i,n,a=(t=e(t)).prop("entries");if(a&&a.length)return this._handleFileTreeEntries(a);if((i=e.makeArray(t.prop("files"))).length)void 0===i[0].name&&i[0].fileName&&e.each(i,(function(e,t){t.name=t.fileName,t.size=t.fileSize}));else{if(!(n=t.prop("value")))return e.Deferred().resolve([]).promise();i=[{name:n.replace(/^.*\\/,"")}]}return e.Deferred().resolve(i).promise()},_getFileInputFiles:function(t){return t instanceof e&&1!==t.length?e.when.apply(e,e.map(t,this._getSingleFileInputFiles))[this._promisePipe]((function(){return Array.prototype.concat.apply([],arguments)})):this._getSingleFileInputFiles(t)},_onChange:function(t){var i=this,n={fileInput:e(t.target),form:e(t.target.form)};this._getFileInputFiles(n.fileInput).always((function(a){n.files=a,i.options.replaceFileInput&&i._replaceFileInput(n),!1!==i._trigger("change",e.Event("change",{delegatedEvent:t}),n)&&i._onAdd(t,n)}))},_onPaste:function(t){var i=t.originalEvent&&t.originalEvent.clipboardData&&t.originalEvent.clipboardData.items,n={files:[]};i&&i.length&&(e.each(i,(function(e,t){var i=t.getAsFile&&t.getAsFile();i&&n.files.push(i)})),!1!==this._trigger("paste",e.Event("paste",{delegatedEvent:t}),n)&&this._onAdd(t,n))},_onDrop:function(t){t.dataTransfer=t.originalEvent&&t.originalEvent.dataTransfer;var i=this,n=t.dataTransfer,a={};n&&n.files&&n.files.length&&(t.preventDefault(),this._getDroppedFiles(n).always((function(n){a.files=n,!1!==i._trigger("drop",e.Event("drop",{delegatedEvent:t}),a)&&i._onAdd(t,a)})))},_onDragOver:t("dragover"),_onDragEnter:t("dragenter"),_onDragLeave:t("dragleave"),_initEventHandlers:function(){this._isXHRUpload(this.options)&&(this._on(this.options.dropZone,{dragover:this._onDragOver,drop:this._onDrop,dragenter:this._onDragEnter,dragleave:this._onDragLeave}),this._on(this.options.pasteZone,{paste:this._onPaste})),e.support.fileInput&&this._on(this.options.fileInput,{change:this._onChange})},_destroyEventHandlers:function(){this._off(this.options.dropZone,"dragenter dragleave dragover drop"),this._off(this.options.pasteZone,"paste"),this._off(this.options.fileInput,"change")},_destroy:function(){this._destroyEventHandlers()},_setOption:function(t,i){var n=-1!==e.inArray(t,this._specialOptions);n&&this._destroyEventHandlers(),this._super(t,i),n&&(this._initSpecialOptions(),this._initEventHandlers())},_initSpecialOptions:function(){var t=this.options;void 0===t.fileInput?t.fileInput=this.element.is('input[type="file"]')?this.element:this.element.find('input[type="file"]'):t.fileInput instanceof e||(t.fileInput=e(t.fileInput)),t.dropZone instanceof e||(t.dropZone=e(t.dropZone)),t.pasteZone instanceof e||(t.pasteZone=e(t.pasteZone))},_getRegExp:function(e){var t=e.split("/"),i=t.pop();return t.shift(),new RegExp(t.join("/"),i)},_isRegExpOption:function(t,i){return"url"!==t&&"string"===e.type(i)&&/^\/.*\/[igm]{0,3}$/.test(i)},_initDataAttributes:function(){var t=this,i=this.options,n=this.element.data();e.each(this.element[0].attributes,(function(e,a){var o,r=a.name.toLowerCase();/^data-/.test(r)&&(r=r.slice(5).replace(/-[a-z]/g,(function(e){return e.charAt(1).toUpperCase()})),o=n[r],t._isRegExpOption(r,o)&&(o=t._getRegExp(o)),i[r]=o)}))},_create:function(){this._initDataAttributes(),this._initSpecialOptions(),this._slots=[],this._sequence=this._getXHRPromise(!0),this._sending=this._active=0,this._initProgressObject(this),this._initEventHandlers()},active:function(){return this._active},progress:function(){return this._progress},add:function(t){var i=this;t&&!this.options.disabled&&(t.fileInput&&!t.files?this._getFileInputFiles(t.fileInput).always((function(e){t.files=e,i._onAdd(null,t)})):(t.files=e.makeArray(t.files),this._onAdd(null,t)))},send:function(t){if(t&&!this.options.disabled){if(t.fileInput&&!t.files){var i,n,a=this,o=e.Deferred(),r=o.promise();return r.abort=function(){return n=!0,i?i.abort():(o.reject(null,"abort","abort"),r)},this._getFileInputFiles(t.fileInput).always((function(e){n||(e.length?(t.files=e,(i=a._onSend(null,t)).then((function(e,t,i){o.resolve(e,t,i)}),(function(e,t,i){o.reject(e,t,i)}))):o.reject())})),this._enhancePromise(r)}if(t.files=e.makeArray(t.files),t.files.length)return this._onSend(null,t)}return this._getXHRPromise(!1,t&&t.context)}})})),function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?e(require("jquery")):e(window.jQuery)}((function(e){"use strict";var t=0,i=e,n="parseJSON";"JSON"in window&&"parse"in JSON&&(i=JSON,n="parse"),e.ajaxTransport("iframe",(function(i){if(i.async){var n,a,o,r=i.initialIframeSrc||"javascript:false;";return{send:function(s,l){(n=e('
    ')).attr("accept-charset",i.formAcceptCharset),o=/\?/.test(i.url)?"&":"?","DELETE"===i.type?(i.url=i.url+o+"_method=DELETE",i.type="POST"):"PUT"===i.type?(i.url=i.url+o+"_method=PUT",i.type="POST"):"PATCH"===i.type&&(i.url=i.url+o+"_method=PATCH",i.type="POST"),a=e('').on("load",(function(){var t,o=e.isArray(i.paramName)?i.paramName:[i.paramName];a.off("load").on("load",(function(){var t;try{if(!(t=a.contents()).length||!t[0].firstChild)throw new Error}catch(e){t=void 0}l(200,"success",{iframe:t}),e('').appendTo(n),window.setTimeout((function(){n.remove()}),0)})),n.prop("target",a.prop("name")).prop("action",i.url).prop("method",i.type),i.formData&&e.each(i.formData,(function(t,i){e('').prop("name",i.name).val(i.value).appendTo(n)})),i.fileInput&&i.fileInput.length&&"POST"===i.type&&(t=i.fileInput.clone(),i.fileInput.after((function(e){return t[e]})),i.paramName&&i.fileInput.each((function(t){e(this).prop("name",o[t]||i.paramName)})),n.append(i.fileInput).prop("enctype","multipart/form-data").prop("encoding","multipart/form-data"),i.fileInput.removeAttr("form")),window.setTimeout((function(){n.submit(),t&&t.length&&i.fileInput.each((function(i,n){var a=e(t[i]);e(n).prop("name",a.prop("name")).attr("form",a.attr("form")),a.replaceWith(n)}))}),0)})),n.append(a).appendTo(document.body)},abort:function(){a&&a.off("load").prop("src",r),n&&n.remove()}}}})),e.ajaxSetup({converters:{"iframe text":function(t){return t&&e(t[0].body).text()},"iframe json":function(t){return t&&i[n](e(t[0].body).text())},"iframe html":function(t){return t&&e(t[0].body).html()},"iframe xml":function(t){var i=t&&t[0];return i&&e.isXMLDoc(i)?i:e.parseXML(i.XMLDocument&&i.XMLDocument.xml||e(i.body).html())},"iframe script":function(t){return t&&e.globalEval(e(t[0].body).text())}}})})); +function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,n)}return i}function _objectSpread(e){for(var t=1;t-1}));switch(e.sortBy){case"size":t.sort((function(t,i){return e.sortAsc?t.size-i.size:i.size-t.size}));break;case"mime":t.sort((function(t,i){return e.sortAsc?t.mime.toLowerCase().localeCompare(i.mime.toLowerCase()):i.mime.toLowerCase().localeCompare(t.mime.toLowerCase())}));break;case"lastModify":t.sort((function(t,i){return e.sortAsc?t.lastModify-i.lastModify:i.lastModify-t.lastModify}));break;default:t.sort((function(t,i){return e.sortAsc?t.name.toLowerCase().localeCompare(i.name.toLowerCase()):i.name.toLowerCase().localeCompare(t.name.toLowerCase())}))}return t},hiddenCount:function(){return this.mediaItems.length-this.filteredMediaItems.length},thumbSize:function(){return this.smallThumbs?100:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs,selectedFolder:this.selectedFolder,gridView:this.gridView}},set:function(e){e&&(this.smallThumbs=e.smallThumbs,this.selectedFolder=e.selectedFolder,this.gridView=e.gridView)}}},watch:{currentPrefs:function(e){localStorage.setItem("mediaApplicationPrefs",JSON.stringify(e))},selectedFolder:function(e){this.mediaFilter="",this.selectedFolder=e,this.loadFolder(e)}},mounted:function(){this.$refs.rootFolder.toggle()},methods:{uploadUrl:function(){if(!this.selectedFolder)return null;var e=$("#uploadFiles").val();return e+(-1==e.indexOf("?")?"?":"&")+"path="+encodeURIComponent(this.selectedFolder.path)},selectRoot:function(){this.selectedFolder=this.root},loadFolder:function(e){this.errors=[],this.selectedMedias=[];var t=this,i=$("#getMediaItemsUrl").val();console.log(e.path),$.ajax({url:i+(-1==i.indexOf("?")?"?":"&")+"path="+encodeURIComponent(e.path),method:"GET",success:function(e){e.forEach((function(e){e.open=!1})),t.mediaItems=e,t.selectedMedias=[],t.sortBy="",t.sortAsc=!0},error:function(i){console.log("error loading folder:"+e.path),t.selectRoot()}})},selectAll:function(){this.selectedMedias=[];for(var e=0;e-1&&(t.mediaItems.splice(n,1),bus.$emit("mediaDeleted",t.selectedMedias[i]))}t.selectedMedias=[]},error:function(e){console.error(e.responseText)}})}}}))},deleteMediaItem:function(e){var t=this;e&&confirmDialog(_objectSpread(_objectSpread({},$("#deleteMedia").data()),{},{callback:function(i){i&&$.ajax({url:$("#deleteMediaUrl").val()+"?path="+encodeURIComponent(e.mediaPath),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(i){var n=t.mediaItems&&t.mediaItems.indexOf(e);n>-1&&(t.mediaItems.splice(n,1),bus.$emit("mediaDeleted",e))},error:function(e){console.error(e.responseText)}})}}))},handleDragStart:function(e,t){var i=[];this.selectedMedias.forEach((function(e){i.push(e.name)})),0==this.isMediaSelected(e)&&(i.push(e.name),this.selectedMedias.push(e)),t.dataTransfer.setData("mediaNames",JSON.stringify(i)),t.dataTransfer.setData("sourceFolder",this.selectedFolder.path),t.dataTransfer.setDragImage(this.dragDropThumbnail,10,10),t.dataTransfer.effectAllowed="move"},handleScrollWhileDrag:function(e){e.clientY<150&&window.scrollBy(0,-10),e.clientY>window.innerHeight-100&&window.scrollBy(0,10)},changeSort:function(e){this.sortBy==e?this.sortAsc=!this.sortAsc:(this.sortAsc=!0,this.sortBy=e)}}}),$("#create-folder-name").keypress((function(e){if(13==e.which)return $("#modalFooterOk").click(),!1})),$("#modalFooterOk").on("click",(function(e){var t=$("#create-folder-name").val();""!==t&&$.ajax({url:$("#createFolderUrl").val()+"?path="+encodeURIComponent(mediaApp.selectedFolder.path)+"&name="+encodeURIComponent(t),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(e){bus.$emit("addFolder",mediaApp.selectedFolder,e),bootstrap.Modal.getOrCreateInstance($("#createFolderModal")).hide()},error:function(e){$("#createFolderModal-errors").empty();var t=JSON.parse(e.responseText).value;$('').text(t).appendTo($("#createFolderModal-errors"))}})})),$("#renameMediaModalFooterOk").on("click",(function(e){var t=$("#new-item-name").val(),i=$("#old-item-name").val();if(""!==t){var n=mediaApp.selectedFolder.path+"/";"/"===n&&(n="");var a=n+t,o=n+i;if(a.toLowerCase()!==o.toLowerCase())$.ajax({url:$("#renameMediaUrl").val()+"?oldPath="+encodeURIComponent(o)+"&newPath="+encodeURIComponent(a),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val()},success:function(e){bootstrap.Modal.getOrCreateInstance($("#renameMediaModal")).hide(),bus.$emit("mediaRenamed",t,a,o,e.newUrl)},error:function(e){$("#renameMediaModal-errors").empty();var t=JSON.parse(e.responseText).value;$('').text(t).appendTo($("#renameMediaModal-errors"))}});else bootstrap.Modal.getOrCreateInstance($("#renameMediaModal")).hide()}})),e&&(document.getElementById("mediaApp").style.display=""),$(document).trigger("mediaApp:ready")},error:function(e){console.error(e.responseText)}}))}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,n)}return i}function _objectSpread(e){for(var t=1;t\n \n
      \n \n \n
    \n \n '),props:{model:Object,selectedInMediaApp:Object,level:Number},data:function(){return{open:!1,children:null,parent:null,isHovered:!1,padding:0}},computed:{empty:function(){return!this.children||0==this.children.length},isSelected:function(){return this.selectedInMediaApp.name==this.model.name&&this.selectedInMediaApp.path==this.model.path},isRoot:function(){return""===this.model.path},canCreateFolder:function(){return void 0===this.model.canCreateFolder||this.model.canCreateFolder},canDeleteFolder:function(){return void 0===this.model.canDeleteFolder||this.model.canDeleteFolder}},mounted:function(){0==this.isRoot&&this.isAncestorOfSelectedFolder()&&this.toggle(),this.padding=this.level<3?16:16+8*this.level},created:function(){var e=this;bus.$on("deleteFolder",(function(t){if(e.children){var i=e.children&&e.children.indexOf(t);i>-1&&(e.children.splice(i,1),bus.$emit("folderDeleted"))}})),bus.$on("addFolder",(function(t,i){e.model==t&&(null!==e.children&&e.children.push(i),i.parent=e.model,bus.$emit("folderAdded",i))}))},methods:{isAncestorOfSelectedFolder:function(){for(parentFolder=mediaApp.selectedFolder;parentFolder;){if(parentFolder.path==this.model.path)return!0;parentFolder=parentFolder.parent}return!1},toggle:function(){this.open=!this.open,this.open&&!this.children&&this.loadChildren()},select:function(){bus.$emit("folderSelected",this.model),this.loadChildren()},createFolder:function(){bus.$emit("createFolderRequested")},deleteFolder:function(){bus.$emit("deleteFolderRequested")},loadChildren:function(){var e=this;0==this.open&&(this.open=!0),$.ajax({url:$("#getFoldersUrl").val()+"?path="+encodeURIComponent(e.model.path),method:"GET",success:function(t){e.children=t,e.children.forEach((function(t){t.parent=e.model}))},error:function(e){emtpy=!1,console.error(e.responseText)}})},handleDragOver:function(e){this.isHovered=!0},handleDragLeave:function(e){this.isHovered=!1},moveMediaToFolder:function(e,t){this.isHovered=!1;var i=JSON.parse(t.dataTransfer.getData("mediaNames"));if(!(i.length<1)){var n=t.dataTransfer.getData("sourceFolder"),a=e.path;""===n&&(n="root"),""===a&&(a="root"),n!==a?confirmDialog(_objectSpread(_objectSpread({},$("#moveMedia").data()),{},{callback:function(e){e&&$.ajax({url:$("#moveMediaListUrl").val(),method:"POST",data:{__RequestVerificationToken:$("input[name='__RequestVerificationToken']").val(),mediaNames:i,sourceFolder:n,targetFolder:a},success:function(){bus.$emit("mediaListMoved")},error:function(e){console.error(e.responseText),bus.$emit("mediaListMoved",e.responseText)}})}})):alert($("#sameFolderMessage").val())}}}});var faIcons={image:"fa-regular fa-image",pdf:"fa-regular fa-file-pdf",word:"fa-regular fa-file-word",powerpoint:"fa-regular fa-file-powerpoint",excel:"fa-regular fa-file-excel",csv:"fa-regular fa-file",audio:"fa-regular fa-file-audio",video:"fa-regular fa-file-video",archive:"fa-regular fa-file-zipper",code:"fa-regular fa-file-code",text:"fa-regular fa-file-lines",file:"fa-regular fa-file"},faThumbnails={gif:faIcons.image,jpeg:faIcons.image,jpg:faIcons.image,png:faIcons.image,pdf:faIcons.pdf,doc:faIcons.word,docx:faIcons.word,ppt:faIcons.powerpoint,pptx:faIcons.powerpoint,xls:faIcons.excel,xlsx:faIcons.excel,csv:faIcons.csv,aac:faIcons.audio,mp3:faIcons.audio,ogg:faIcons.audio,avi:faIcons.video,flv:faIcons.video,mkv:faIcons.video,mp4:faIcons.video,webm:faIcons.video,gz:faIcons.archive,zip:faIcons.archive,css:faIcons.code,html:faIcons.code,js:faIcons.code,txt:faIcons.text};function getClassNameForExtension(e){return faThumbnails[e.toLowerCase()]||faIcons.file}function getExtensionForFilename(e){return e.slice(2+(e.lastIndexOf(".")-1>>>0))}function getClassNameForFilename(e){return getClassNameForExtension(getExtensionForFilename(e))}function initializeAttachedMediaField(e,t,i,n,a,o,r,s,l){var d,c=$(document.getElementById($(e).data("for"))).data("init"),u=$(e),m=u.attr("id");mediaFieldApps.push(d=new Vue({el:u.get(0),data:{mediaItems:[],selectedMedia:null,smallThumbs:!1,idPrefix:m,initialized:!1,allowMediaText:o,backupMediaText:"",allowAnchors:r,backupAnchor:null,mediaTextmodal:null,anchoringModal:null},created:function(){this.currentPrefs=JSON.parse(localStorage.getItem("mediaFieldPrefs"))},computed:{paths:{get:function(){var e=[];return this.initialized?(this.mediaItems.forEach((function(t){"not-found"!==t.mediaPath&&e.push({path:t.mediaPath,isRemoved:t.isRemoved,isNew:t.isNew,mediaText:t.mediaText,anchor:t.anchor,attachedFileName:t.attachedFileName})})),JSON.stringify(e)):JSON.stringify(c)},set:function(e){var t=this,i=e||[],a=$.Deferred(),o=[],r=0;i.forEach((function(e,i){o.push({name:" "+e.path,mime:"",mediaPath:"",anchor:e.anchor,attachedFileName:e.attachedFileName}),promise=$.when(a).done((function(){$.ajax({url:n+"?path="+encodeURIComponent(e.path),method:"GET",success:function(n){n.vuekey=n.name+i.toString(),n.mediaText=e.mediaText,n.anchor=e.anchor,n.attachedFileName=e.attachedFileName,o.splice(i,1,n),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)},error:function(n){console.log(JSON.stringify(n)),o.splice(i,1,{name:e.path,mime:"",mediaPath:"not-found",mediaText:"",anchor:{x:.5,y:.5},attachedFileName:e.attachedFileName}),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)}})}))})),a.resolve()}},fileSize:function(){return Math.round(this.selectedMedia.size/1024)},canAddMedia:function(){for(var e=[],t=0;t0&&a},thumbSize:function(){return this.smallThumbs?120:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs}},set:function(e){e&&(this.smallThumbs=e.smallThumbs)}}},mounted:function(){var e=this;e.paths=c,e.$on("selectAndDeleteMediaRequested",(function(t){e.selectAndDeleteMedia(t)})),e.$on("selectMediaRequested",(function(t){e.selectMedia(t)}));var n="#"+t,o=u.attr("id"),r=randomUUID();$(n).fileupload({limitConcurrentUploads:20,dropZone:$("#"+o),dataType:"json",url:i,maxChunkSize:l,add:function(t,i){var n,a=i.files.length;for(n=0;n0)for(var o=0;o1&&!1===a?(alert($("#onlyOneItemMessage").val()),d.mediaItems.push(i[0]),d.initialized=!0):(d.mediaItems=d.mediaItems.concat(i),d.initialized=!0)):alert(n)},error:function(e,t,i){console.log("Error on upload."),console.log(e),console.log(t),console.log(i)}}).on("fileuploadchunkbeforesend",(function(e,t){var i=t.files[0];t.blob=new File([t.blob],i.name,{type:i.type,lastModified:i.lastModified})}))},methods:{selectMedia:function(e){this.selectedMedia=e},getUniqueId:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}))},removeSelected:function(e){if(this.selectedMedia){var t=this.mediaItems&&this.mediaItems.indexOf(this.selectedMedia);t>-1&&(this.mediaItems[t].isRemoved=!0,this.mediaItems.splice(t,1))}else 1===this.mediaItems.length&&(this.mediaItems[t].isRemoved=!0,this.mediaItems.splice(0,1));this.selectedMedia=null},showMediaTextModal:function(e){this.mediaTextModal=new bootstrap.Modal(this.$refs.mediaTextModal),this.mediaTextModal.show(),this.backupMediaText=this.selectedMedia.mediaText},cancelMediaTextModal:function(e){this.mediaTextModal.hide(),this.selectedMedia.mediaText=this.backupMediaText},showAnchorModal:function(e){this.anchoringModal=new bootstrap.Modal(this.$refs.anchoringModal),this.anchoringModal.show(),this.selectedMedia.anchor={x:this.selectedMedia.anchor.x,y:this.selectedMedia.anchor.y},this.backupAnchor=this.selectedMedia.anchor},cancelAnchoringModal:function(e){this.anchoringModal.hide(),this.selectedMedia.anchor=this.backupAnchor},resetAnchor:function(e){this.selectedMedia.anchor={x:.5,y:.5}},onAnchorDrop:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},anchorLeft:function(){if(this.$refs.anchorImage&&this.$refs.modalBody&&this.selectedMedia){var e=(this.$refs.modalBody.clientWidth-this.$refs.anchorImage.clientWidth)/2,t=this.selectedMedia.anchor.x*this.$refs.anchorImage.clientWidth+e;return t<17?t=17:t-=8,t+"px"}return"0"},anchorTop:function(){if(this.$refs.anchorImage&&this.selectedMedia){var e=this.selectedMedia.anchor.y*this.$refs.anchorImage.clientHeight;return e<15?e=15:e+=5,e+"px"}return"0"},setAnchor:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},addMediaFiles:function(e){e.length>1&&!1===a?(alert($("#onlyOneItemMessage").val()),d.mediaItems.push(e[0]),d.initialized=!0):(d.mediaItems=d.mediaItems.concat(e),d.initialized=!0)},selectAndDeleteMedia:function(e){var t=this;t.selectedMedia=e,setTimeout((function(){t.removeSelected()}),100)}},watch:{mediaItems:{deep:!0,handler:function(){setTimeout((function(){$(document).trigger("contentpreview:render")}),100)}},currentPrefs:function(e){localStorage.setItem("mediaFieldPrefs",JSON.stringify(e))}}}))}function initializeMediaField(e,t,i,n,a,o){if(null!==e){var r,s=$(document.getElementById($(e).data("for"))).data("init"),l=$(e),d=l.attr("id");t.addEventListener("hidden.bs.modal",(function(e){$("#mediaApp").appendTo("body"),$("#mediaApp").hide()})),mediaFieldApps.push(r=new Vue({el:l.get(0),data:{mediaItems:[],selectedMedia:null,smallThumbs:!1,idPrefix:d,initialized:!1,allowMediaText:a,backupMediaText:"",allowAnchors:o,backupAnchor:null,mediaTextModal:null,anchoringModal:null},created:function(){this.currentPrefs=JSON.parse(localStorage.getItem("mediaFieldPrefs"))},computed:{paths:{get:function(){var e=[];return this.initialized?(this.mediaItems.forEach((function(t){"not-found"!==t.mediaPath&&e.push({path:t.mediaPath,mediaText:t.mediaText,anchor:t.anchor})})),JSON.stringify(e)):JSON.stringify(s)},set:function(e){var t=this,n=e||[],a=$.Deferred(),o=[],r=0;n.forEach((function(e,n){o.push({name:" "+e.path,mime:"",mediaPath:""}),promise=$.when(a).done((function(){$.ajax({url:i+"?path="+encodeURIComponent(e.path),method:"GET",success:function(i){i.vuekey=i.name+n.toString(),i.mediaText=e.mediaText,i.anchor=e.anchor,o.splice(n,1,i),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)},error:function(i){console.log(i),o.splice(n,1,{name:e.path,mime:"",mediaPath:"not-found",mediaText:"",anchor:{x:0,y:0}}),o.length===++r&&(o.forEach((function(e){t.mediaItems.push(e)})),t.initialized=!0)}})}))})),a.resolve()}},fileSize:function(){return Math.round(this.selectedMedia.size/1024)},canAddMedia:function(){return 0===this.mediaItems.length||this.mediaItems.length>0&&n},thumbSize:function(){return this.smallThumbs?120:240},currentPrefs:{get:function(){return{smallThumbs:this.smallThumbs}},set:function(e){e&&(this.smallThumbs=e.smallThumbs)}}},mounted:function(){var e=this;e.paths=s,e.$on("selectAndDeleteMediaRequested",(function(t){e.selectAndDeleteMedia(t)})),e.$on("selectMediaRequested",(function(t){e.selectMedia(t)})),e.$on("filesUploaded",(function(t){e.addMediaFiles(t)}))},methods:{selectMedia:function(e){this.selectedMedia=e},showModal:function(e){var i=this;if(i.canAddMedia){$("#mediaApp").appendTo($(t).find(".modal-body")),$("#mediaApp").show();var n=new bootstrap.Modal(t);n.show(),$(t).find(".mediaFieldSelectButton").off("click").on("click",(function(e){return i.addMediaFiles(mediaApp.selectedMedias),mediaApp.selectedMedias=[],n.hide(),!0}))}},showMediaTextModal:function(e){this.mediaTextModal=new bootstrap.Modal(this.$refs.mediaTextModal),this.mediaTextModal.show(),this.backupMediaText=this.selectedMedia.mediaText},cancelMediaTextModal:function(e){this.mediaTextModal.hide(),this.selectedMedia.mediaText=this.backupMediaText},showAnchorModal:function(e){this.anchoringModal=new bootstrap.Modal(this.$refs.anchoringModal),this.anchoringModal.show(),this.selectedMedia.anchor={x:this.selectedMedia.anchor.x,y:this.selectedMedia.anchor.y},this.backupAnchor=this.selectedMedia.anchor},cancelAnchoringModal:function(e){this.anchoringModal.hide(),this.selectedMedia.anchor=this.backupAnchor},resetAnchor:function(e){this.selectedMedia.anchor={x:.5,y:.5}},onAnchorDrop:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},anchorLeft:function(){if(this.$refs.anchorImage&&this.$refs.modalBody&&this.selectedMedia){var e=(this.$refs.modalBody.clientWidth-this.$refs.anchorImage.clientWidth)/2,t=this.selectedMedia.anchor.x*this.$refs.anchorImage.clientWidth+e,i=Math.round(this.$refs.modalBody.querySelector(".icon-media-anchor").clientWidth);return Number.isInteger(i)&&(t-=i/2),t+"px"}return"0"},anchorTop:function(){return this.$refs.anchorImage&&this.selectedMedia?this.selectedMedia.anchor.y*this.$refs.anchorImage.clientHeight+"px":"0"},setAnchor:function(e){var t=this.$refs.anchorImage;this.selectedMedia.anchor={x:e.offsetX/t.clientWidth,y:e.offsetY/t.clientHeight}},addMediaFiles:function(e){e.length>1&&!1===n?(alert($("#onlyOneItemMessage").val()),r.mediaItems.push(e[0]),r.initialized=!0):(r.mediaItems=r.mediaItems.concat(e),r.initialized=!0)},removeSelected:function(e){if(this.selectedMedia){var t=this.mediaItems&&this.mediaItems.indexOf(this.selectedMedia);t>-1&&this.mediaItems.splice(t,1)}else 1===this.mediaItems.length&&this.mediaItems.splice(0,1);this.selectedMedia=null},selectAndDeleteMedia:function(e){var t=this;t.selectedMedia=e,setTimeout((function(){t.removeSelected()}),100)}},watch:{mediaItems:{deep:!0,handler:function(){setTimeout((function(){$(document).trigger("contentpreview:render")}),100)}},currentPrefs:function(e){localStorage.setItem("mediaFieldPrefs",JSON.stringify(e))}}}))}}Vue.component("media-items-grid",{template:'\n
      \n
    1. \n
      \n \n \n
      \n
      \n \n \n \n {{ media.name }}\n
      \n
    2. \n
    \n ',data:function(){return{T:{}}},props:{filteredMediaItems:Array,selectedMedias:Array,thumbSize:Number},created:function(){this.T.editButton=$("#t-edit-button").val(),this.T.deleteButton=$("#t-delete-button").val()},methods:{isMediaSelected:function(e){return this.selectedMedias.some((function(t,i,n){return t.url.toLowerCase()===e.url.toLowerCase()}))},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},toggleSelectionOfMedia:function(e){bus.$emit("mediaToggleRequested",e)},renameMedia:function(e){bus.$emit("renameMediaRequested",e)},deleteMedia:function(e){bus.$emit("deleteMediaRequested",e)},dragStart:function(e,t){bus.$emit("mediaDragStartRequested",e,t)},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}}),Vue.component("media-items-table",{template:'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{ T.imageHeader }}\n {{ T.nameHeader }}\n \n \n {{ T.lastModifyHeader }} \n \n \n \n {{ T.sizeHeader }}\n \n \n \n \n {{ T.typeHeader }}\n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    {{ printDateTime(media.lastModify) }}
    \n
    \n
    {{ isNaN(media.size)? 0 : Math.round(media.size / 1024) }} KB
    \n
    \n
    {{ media.mime }}
    \n
    \n ',data:function(){return{T:{}}},props:{sortBy:String,sortAsc:Boolean,filteredMediaItems:Array,selectedMedias:Array,thumbSize:Number},created:function(){var e=this;e.T.imageHeader=$("#t-image-header").val(),e.T.nameHeader=$("#t-name-header").val(),e.T.lastModifyHeader=$("#t-lastModify-header").val(),e.T.sizeHeader=$("#t-size-header").val(),e.T.typeHeader=$("#t-type-header").val(),e.T.editButton=$("#t-edit-button").val(),e.T.deleteButton=$("#t-delete-button").val(),e.T.viewButton=$("#t-view-button").val()},methods:{isMediaSelected:function(e){return this.selectedMedias.some((function(t,i,n){return t.url.toLowerCase()===e.url.toLowerCase()}))},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},changeSort:function(e){bus.$emit("sortChangeRequested",e)},toggleSelectionOfMedia:function(e){bus.$emit("mediaToggleRequested",e)},renameMedia:function(e){bus.$emit("renameMediaRequested",e)},deleteMedia:function(e){bus.$emit("deleteMediaRequested",e)},dragStart:function(e,t){bus.$emit("mediaDragStartRequested",e,t)},printDateTime:function(e){return new Date(e).toLocaleString()},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}}),Vue.component("pager",{template:'\n
    \n \n \n
    \n ',props:{sourceItems:Array},data:function(){return{pageSize:10,pageSizeOptions:[10,30,50,100],current:0,T:{}}},created:function(){var e=this;e.T.pagerFirstButton=$("#t-pager-first-button").val(),e.T.pagerPreviousButton=$("#t-pager-previous-button").val(),e.T.pagerNextButton=$("#t-pager-next-button").val(),e.T.pagerLastButton=$("#t-pager-last-button").val(),e.T.pagerPageSizeLabel=$("#t-pager-page-size-label").val(),e.T.pagerPageLabel=$("#t-pager-page-label").val(),e.T.pagerTotalLabel=$("#t-pager-total-label").val()},methods:{next:function(){this.current=this.current+1},previous:function(){this.current=this.current-1},goFirst:function(){this.current=0},goLast:function(){this.current=this.totalPages-1},goTo:function(e){this.current=e}},computed:{total:function(){return this.sourceItems?this.sourceItems.length:0},totalPages:function(){var e=Math.ceil(this.total/this.pageSize);return e>0?e:1},isLastPage:function(){return this.current+1>=this.totalPages},isFirstPage:function(){return 0===this.current},canDoNext:function(){return!this.isLastPage},canDoPrev:function(){return!this.isFirstPage},canDoFirst:function(){return!this.isFirstPage},canDoLast:function(){return!this.isLastPage},itemsInCurrentPage:function(){var e=this.pageSize*this.current,t=e+this.pageSize,i=this.sourceItems.slice(e,t);return bus.$emit("pagerEvent",i),i},pageLinks:function(){var e=[];e.push(this.current+1);var t=this.current>0?this.current:-1;e.unshift(t);var i=this.current>1?this.current-1:-1;e.unshift(i);var n=this.totalPages-this.current>1?this.current+2:-1;e.push(n);var a=this.totalPages-this.current>2?this.current+3:-1;return e.push(a),e}},watch:{sourceItems:function(){this.current=0},pageSize:function(){this.current=0}}}),Vue.component("sortIndicator",{template:'\n
    \n \n \n
    \n ',props:{colname:String,selectedcolname:String,asc:Boolean},computed:{isActive:function(){return this.colname.toLowerCase()==this.selectedcolname.toLowerCase()}}}),Vue.component("mediaFieldThumbsContainer",{template:'
    {{T.noImages}}
  • {{ media.isNew ? media.name.substr(36) : media.name }}
    {{ T.mediaNotFound }} {{ T.discardWarning }}
    {{ media.name }}
  • ',data:function(){return{T:{}}},props:{mediaItems:Array,selectedMedia:Object,thumbSize:Number,idPrefix:String},created:function(){var e=this;e.T.mediaNotFound=$("#t-media-not-found").val(),e.T.discardWarning=$("#t-discard-warning").val(),e.T.noImages=$("#t-no-images").val()},methods:{selectAndDeleteMedia:function(e){this.$parent.$emit("selectAndDeleteMediaRequested",e)},selectMedia:function(e){this.$parent.$emit("selectMediaRequested",e)},buildMediaUrl:function(e,t){return e+(-1==e.indexOf("?")?"?":"&")+"width="+t+"&height="+t},getfontAwesomeClassNameForFileName:function(e,t){return getClassNameForFilename(e)+" "+t}}});var mediaFieldApps=[];function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function randomUUID(){return"object"===("undefined"==typeof crypto?"undefined":_typeof(crypto))&&"function"==typeof crypto.randomUUID?crypto.randomUUID():([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(function(e){return(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)}))}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}function _typeof(e){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_typeof(e)}Vue.component("upload",{template:'

    {{ model.name }}

    Error: {{ model.errorMessage }}
    ',props:{model:Object,uploadInputId:String},mounted:function(){var e,t=this,i=document.getElementById(null!==(e=t.uploadInputId)&&void 0!==e?e:"fileupload");$(i).bind("fileuploadprogress",(function(e,i){i.files[0].name===t.model.name&&(t.model.percentage=parseInt(i.loaded/i.total*100,10))})),$(i).bind("fileuploaddone",(function(e,i){i.files[0].name===t.model.name&&(i.result.files[0].error?t.handleFailure(i.files[0].name,i.result.files[0].error):bus.$emit("removalRequest",t.model))})),$(i).bind("fileuploadfail",(function(e,i){i.files[0].name===t.model.name&&t.handleFailure(i.files[0].name,$("#t-error").val())}))},methods:{handleFailure:function(e,t){e===this.model.name&&(this.model.errorMessage=t,bus.$emit("ErrorOnUpload",this.model))},dismissWarning:function(){bus.$emit("removalRequest",this.model)}}}),Vue.component("uploadList",{template:'
    {{ T.uploads }} (Pending: {{ pendingCount }}) ( {{ T.errors }}: {{ errorCount }} / {{ T.clearErrors }} )
    ',data:function(){return{files:[],T:{},expanded:!1,pendingCount:0,errorCount:0}},props:{uploadInputId:String},created:function(){var e=this;e.T.uploads=$("#t-uploads").val(),e.T.errors=$("#t-errors").val(),e.T.clearErrors=$("#t-clear-errors").val()},computed:{fileCount:function(){return this.files.length}},mounted:function(){var e,t=this,i=document.getElementById(null!==(e=t.uploadInputId)&&void 0!==e?e:"fileupload");$(i).bind("fileuploadadd",(function(e,i){i.files&&i.files.forEach((function(e){t.files.some((function(t){return t.name==e.name}))?console.error("A file with the same name is already on the queue:"+e.name):t.files.push({name:e.name,percentage:0,errorMessage:""})}))})),bus.$on("removalRequest",(function(e){t.files.forEach((function(t,i,n){t.name==e.name&&n.splice(i,1)}))})),bus.$on("ErrorOnUpload",(function(e){t.updateCount()}))},methods:{updateCount:function(){this.errorCount=this.files.filter((function(e){return""!=e.errorMessage})).length,this.pendingCount=this.files.length-this.errorCount,this.files.length<1&&(this.expanded=!1)},clearErrors:function(){this.files=this.files.filter((function(e){return""==e.errorMessage}))}},watch:{files:function(){this.updateCount()}}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery","jquery-ui/ui/widget"],e):"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?e(require("jquery"),require("./vendor/jquery.ui.widget")):e(window.jQuery)}((function(e){"use strict";function t(t){var i="dragover"===t;return function(n){n.dataTransfer=n.originalEvent&&n.originalEvent.dataTransfer;var a=n.dataTransfer;a&&-1!==e.inArray("Files",a.types)&&!1!==this._trigger(t,e.Event(t,{delegatedEvent:n}))&&(n.preventDefault(),i&&(a.dropEffect="copy"))}}var i;e.support.fileInput=!(new RegExp("(Android (1\\.[0156]|2\\.[01]))|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)|(w(eb)?OSBrowser)|(webOS)|(Kindle/(1\\.0|2\\.[05]|3\\.0))").test(window.navigator.userAgent)||e('').prop("disabled")),e.support.xhrFileUpload=!(!window.ProgressEvent||!window.FileReader),e.support.xhrFormDataFileUpload=!!window.FormData,e.support.blobSlice=window.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice),e.widget("blueimp.fileupload",{options:{dropZone:e(document),pasteZone:void 0,fileInput:void 0,replaceFileInput:!0,paramName:void 0,singleFileUploads:!0,limitMultiFileUploads:void 0,limitMultiFileUploadSize:void 0,limitMultiFileUploadSizeOverhead:512,sequentialUploads:!1,limitConcurrentUploads:void 0,forceIframeTransport:!1,redirect:void 0,redirectParamName:void 0,postMessage:void 0,multipart:!0,maxChunkSize:void 0,uploadedBytes:void 0,recalculateProgress:!0,progressInterval:100,bitrateInterval:500,autoUpload:!0,uniqueFilenames:void 0,messages:{uploadedBytes:"Uploaded bytes exceed file size"},i18n:function(t,i){return t=this.messages[t]||t.toString(),i&&e.each(i,(function(e,i){t=t.replace("{"+e+"}",i)})),t},formData:function(e){return e.serializeArray()},add:function(t,i){if(t.isDefaultPrevented())return!1;(i.autoUpload||!1!==i.autoUpload&&e(this).fileupload("option","autoUpload"))&&i.process().done((function(){i.submit()}))},processData:!1,contentType:!1,cache:!1,timeout:0},_promisePipe:(i=e.fn.jquery.split("."),Number(i[0])>1||Number(i[1])>7?"then":"pipe"),_specialOptions:["fileInput","dropZone","pasteZone","multipart","forceIframeTransport"],_blobSlice:e.support.blobSlice&&function(){return(this.slice||this.webkitSlice||this.mozSlice).apply(this,arguments)},_BitrateTimer:function(){this.timestamp=Date.now?Date.now():(new Date).getTime(),this.loaded=0,this.bitrate=0,this.getBitrate=function(e,t,i){var n=e-this.timestamp;return(!this.bitrate||!i||n>i)&&(this.bitrate=(t-this.loaded)*(1e3/n)*8,this.loaded=t,this.timestamp=e),this.bitrate}},_isXHRUpload:function(t){return!t.forceIframeTransport&&(!t.multipart&&e.support.xhrFileUpload||e.support.xhrFormDataFileUpload)},_getFormData:function(t){var i;return"function"===e.type(t.formData)?t.formData(t.form):e.isArray(t.formData)?t.formData:"object"===e.type(t.formData)?(i=[],e.each(t.formData,(function(e,t){i.push({name:e,value:t})})),i):[]},_getTotal:function(t){var i=0;return e.each(t,(function(e,t){i+=t.size||1})),i},_initProgressObject:function(t){var i={loaded:0,total:0,bitrate:0};t._progress?e.extend(t._progress,i):t._progress=i},_initResponseObject:function(e){var t;if(e._response)for(t in e._response)Object.prototype.hasOwnProperty.call(e._response,t)&&delete e._response[t];else e._response={}},_onProgress:function(t,i){if(t.lengthComputable){var n,a=Date.now?Date.now():(new Date).getTime();if(i._time&&i.progressInterval&&a-i._time").prop("href",t.url).prop("host");t.dataType="iframe "+(t.dataType||""),t.formData=this._getFormData(t),t.redirect&&i&&i!==location.host&&t.formData.push({name:t.redirectParamName||"redirect",value:t.redirect})},_initDataSettings:function(e){this._isXHRUpload(e)?(this._chunkedUpload(e,!0)||(e.data||this._initXHRData(e),this._initProgressListener(e)),e.postMessage&&(e.dataType="postmessage "+(e.dataType||""))):this._initIframeSettings(e)},_getParamName:function(t){var i=e(t.fileInput),n=t.paramName;return n?e.isArray(n)||(n=[n]):(n=[],i.each((function(){for(var t=e(this),i=t.prop("name")||"files[]",a=(t.prop("files")||[1]).length;a;)n.push(i),a-=1})),n.length||(n=[i.prop("name")||"files[]"])),n},_initFormSettings:function(t){t.form&&t.form.length||(t.form=e(t.fileInput.prop("form")),t.form.length||(t.form=e(this.options.fileInput.prop("form")))),t.paramName=this._getParamName(t),t.url||(t.url=t.form.prop("action")||location.href),t.type=(t.type||"string"===e.type(t.form.prop("method"))&&t.form.prop("method")||"").toUpperCase(),"POST"!==t.type&&"PUT"!==t.type&&"PATCH"!==t.type&&(t.type="POST"),t.formAcceptCharset||(t.formAcceptCharset=t.form.attr("accept-charset"))},_getAJAXSettings:function(t){var i=e.extend({},this.options,t);return this._initFormSettings(i),this._initDataSettings(i),i},_getDeferredState:function(e){return e.state?e.state():e.isResolved()?"resolved":e.isRejected()?"rejected":"pending"},_enhancePromise:function(e){return e.success=e.done,e.error=e.fail,e.complete=e.always,e},_getXHRPromise:function(t,i,n){var a=e.Deferred(),o=a.promise();return i=i||this.options.context||o,!0===t?a.resolveWith(i,n):!1===t&&a.rejectWith(i,n),o.abort=a.promise,this._enhancePromise(o)},_addConvenienceMethods:function(t,i){var n=this,a=function(t){return e.Deferred().resolveWith(n,t).promise()};i.process=function(t,o){return(t||o)&&(i._processQueue=this._processQueue=(this._processQueue||a([this]))[n._promisePipe]((function(){return i.errorThrown?e.Deferred().rejectWith(n,[i]).promise():a(arguments)}))[n._promisePipe](t,o)),this._processQueue||a([this])},i.submit=function(){return"pending"!==this.state()&&(i.jqXHR=this.jqXHR=!1!==n._trigger("submit",e.Event("submit",{delegatedEvent:t}),this)&&n._onSend(t,this)),this.jqXHR||n._getXHRPromise()},i.abort=function(){return this.jqXHR?this.jqXHR.abort():(this.errorThrown="abort",n._trigger("fail",null,this),n._getXHRPromise(!1))},i.state=function(){return this.jqXHR?n._getDeferredState(this.jqXHR):this._processQueue?n._getDeferredState(this._processQueue):void 0},i.processing=function(){return!this.jqXHR&&this._processQueue&&"pending"===n._getDeferredState(this._processQueue)},i.progress=function(){return this._progress},i.response=function(){return this._response}},_getUploadedBytes:function(e){var t=e.getResponseHeader("Range"),i=t&&t.split("-"),n=i&&i.length>1&&parseInt(i[1],10);return n&&n+1},_chunkedUpload:function(t,i){t.uploadedBytes=t.uploadedBytes||0;var n,a,o=this,r=t.files[0],s=r.size,l=t.uploadedBytes,d=t.maxChunkSize||s,c=this._blobSlice,u=e.Deferred(),m=u.promise();return!(!(this._isXHRUpload(t)&&c&&(l||("function"===e.type(d)?d(t):d)=s?(r.error=t.i18n("uploadedBytes"),this._getXHRPromise(!1,t.context,[null,"error",r.error])):(a=function(){var i=e.extend({},t),m=i._progress.loaded;i.blob=c.call(r,l,l+("function"===e.type(d)?d(i):d),r.type),i.chunkSize=i.blob.size,i.contentRange="bytes "+l+"-"+(l+i.chunkSize-1)+"/"+s,o._trigger("chunkbeforesend",null,i),o._initXHRData(i),o._initProgressListener(i),n=(!1!==o._trigger("chunksend",null,i)&&e.ajax(i)||o._getXHRPromise(!1,i.context)).done((function(n,r,d){l=o._getUploadedBytes(d)||l+i.chunkSize,m+i.chunkSize-i._progress.loaded&&o._onProgress(e.Event("progress",{lengthComputable:!0,loaded:l-i.uploadedBytes,total:l-i.uploadedBytes}),i),t.uploadedBytes=i.uploadedBytes=l,i.result=n,i.textStatus=r,i.jqXHR=d,o._trigger("chunkdone",null,i),o._trigger("chunkalways",null,i),ls._sending)for(var n=s._slots.shift();n;){if("pending"===s._getDeferredState(n)){n.resolve();break}n=s._slots.shift()}0===s._active&&s._trigger("stop")}))};return this._beforeSend(t,l),this.options.sequentialUploads||this.options.limitConcurrentUploads&&this.options.limitConcurrentUploads<=this._sending?(this.options.limitConcurrentUploads>1?(o=e.Deferred(),this._slots.push(o),r=o[s._promisePipe](d)):(this._sequence=this._sequence[s._promisePipe](d,d),r=this._sequence),r.abort=function(){return a=[void 0,"abort","abort"],n?n.abort():(o&&o.rejectWith(l.context,a),d())},this._enhancePromise(r)):d()},_onAdd:function(t,i){var n,a,o,r,s=this,l=!0,d=e.extend({},this.options,i),c=i.files,u=c.length,m=d.limitMultiFileUploads,p=d.limitMultiFileUploadSize,f=d.limitMultiFileUploadSizeOverhead,h=0,g=this._getParamName(d),v=0;if(!u)return!1;if(p&&void 0===c[0].size&&(p=void 0),(d.singleFileUploads||m||p)&&this._isXHRUpload(d))if(d.singleFileUploads||p||!m)if(!d.singleFileUploads&&p)for(o=[],n=[],r=0;rp||m&&r+1-v>=m)&&(o.push(c.slice(v,r+1)),(a=g.slice(v,r+1)).length||(a=g),n.push(a),v=r+1,h=0);else n=g;else for(o=[],n=[],r=0;r").append(n)[0].reset(),i.after(n).detach(),a&&n.trigger("focus"),e.cleanData(i.off("remove")),this.options.fileInput=this.options.fileInput.map((function(e,t){return t===i[0]?n[0]:t})),i[0]===this.element[0]&&(this.element=n)},_handleFileTreeEntry:function(t,i){var n,a=this,o=e.Deferred(),r=[],s=function(e){e&&!e.entry&&(e.entry=t),o.resolve([e])};return i=i||"",t.isFile?t._file?(t._file.relativePath=i,o.resolve(t._file)):t.file((function(e){e.relativePath=i,o.resolve(e)}),s):t.isDirectory?(n=t.createReader(),function e(){n.readEntries((function(n){n.length?(r=r.concat(n),e()):function(e){a._handleFileTreeEntries(e,i+t.name+"/").done((function(e){o.resolve(e)})).fail(s)}(r)}),s)}()):o.resolve([]),o.promise()},_handleFileTreeEntries:function(t,i){var n=this;return e.when.apply(e,e.map(t,(function(e){return n._handleFileTreeEntry(e,i)})))[this._promisePipe]((function(){return Array.prototype.concat.apply([],arguments)}))},_getDroppedFiles:function(t){var i=(t=t||{}).items;return i&&i.length&&(i[0].webkitGetAsEntry||i[0].getAsEntry)?this._handleFileTreeEntries(e.map(i,(function(e){var t;return e.webkitGetAsEntry?((t=e.webkitGetAsEntry())&&(t._file=e.getAsFile()),t):e.getAsEntry()}))):e.Deferred().resolve(e.makeArray(t.files)).promise()},_getSingleFileInputFiles:function(t){var i,n,a=(t=e(t)).prop("entries");if(a&&a.length)return this._handleFileTreeEntries(a);if((i=e.makeArray(t.prop("files"))).length)void 0===i[0].name&&i[0].fileName&&e.each(i,(function(e,t){t.name=t.fileName,t.size=t.fileSize}));else{if(!(n=t.prop("value")))return e.Deferred().resolve([]).promise();i=[{name:n.replace(/^.*\\/,"")}]}return e.Deferred().resolve(i).promise()},_getFileInputFiles:function(t){return t instanceof e&&1!==t.length?e.when.apply(e,e.map(t,this._getSingleFileInputFiles))[this._promisePipe]((function(){return Array.prototype.concat.apply([],arguments)})):this._getSingleFileInputFiles(t)},_onChange:function(t){var i=this,n={fileInput:e(t.target),form:e(t.target.form)};this._getFileInputFiles(n.fileInput).always((function(a){n.files=a,i.options.replaceFileInput&&i._replaceFileInput(n),!1!==i._trigger("change",e.Event("change",{delegatedEvent:t}),n)&&i._onAdd(t,n)}))},_onPaste:function(t){var i=t.originalEvent&&t.originalEvent.clipboardData&&t.originalEvent.clipboardData.items,n={files:[]};i&&i.length&&(e.each(i,(function(e,t){var i=t.getAsFile&&t.getAsFile();i&&n.files.push(i)})),!1!==this._trigger("paste",e.Event("paste",{delegatedEvent:t}),n)&&this._onAdd(t,n))},_onDrop:function(t){t.dataTransfer=t.originalEvent&&t.originalEvent.dataTransfer;var i=this,n=t.dataTransfer,a={};n&&n.files&&n.files.length&&(t.preventDefault(),this._getDroppedFiles(n).always((function(n){a.files=n,!1!==i._trigger("drop",e.Event("drop",{delegatedEvent:t}),a)&&i._onAdd(t,a)})))},_onDragOver:t("dragover"),_onDragEnter:t("dragenter"),_onDragLeave:t("dragleave"),_initEventHandlers:function(){this._isXHRUpload(this.options)&&(this._on(this.options.dropZone,{dragover:this._onDragOver,drop:this._onDrop,dragenter:this._onDragEnter,dragleave:this._onDragLeave}),this._on(this.options.pasteZone,{paste:this._onPaste})),e.support.fileInput&&this._on(this.options.fileInput,{change:this._onChange})},_destroyEventHandlers:function(){this._off(this.options.dropZone,"dragenter dragleave dragover drop"),this._off(this.options.pasteZone,"paste"),this._off(this.options.fileInput,"change")},_destroy:function(){this._destroyEventHandlers()},_setOption:function(t,i){var n=-1!==e.inArray(t,this._specialOptions);n&&this._destroyEventHandlers(),this._super(t,i),n&&(this._initSpecialOptions(),this._initEventHandlers())},_initSpecialOptions:function(){var t=this.options;void 0===t.fileInput?t.fileInput=this.element.is('input[type="file"]')?this.element:this.element.find('input[type="file"]'):t.fileInput instanceof e||(t.fileInput=e(t.fileInput)),t.dropZone instanceof e||(t.dropZone=e(t.dropZone)),t.pasteZone instanceof e||(t.pasteZone=e(t.pasteZone))},_getRegExp:function(e){var t=e.split("/"),i=t.pop();return t.shift(),new RegExp(t.join("/"),i)},_isRegExpOption:function(t,i){return"url"!==t&&"string"===e.type(i)&&/^\/.*\/[igm]{0,3}$/.test(i)},_initDataAttributes:function(){var t=this,i=this.options,n=this.element.data();e.each(this.element[0].attributes,(function(e,a){var o,r=a.name.toLowerCase();/^data-/.test(r)&&(r=r.slice(5).replace(/-[a-z]/g,(function(e){return e.charAt(1).toUpperCase()})),o=n[r],t._isRegExpOption(r,o)&&(o=t._getRegExp(o)),i[r]=o)}))},_create:function(){this._initDataAttributes(),this._initSpecialOptions(),this._slots=[],this._sequence=this._getXHRPromise(!0),this._sending=this._active=0,this._initProgressObject(this),this._initEventHandlers()},active:function(){return this._active},progress:function(){return this._progress},add:function(t){var i=this;t&&!this.options.disabled&&(t.fileInput&&!t.files?this._getFileInputFiles(t.fileInput).always((function(e){t.files=e,i._onAdd(null,t)})):(t.files=e.makeArray(t.files),this._onAdd(null,t)))},send:function(t){if(t&&!this.options.disabled){if(t.fileInput&&!t.files){var i,n,a=this,o=e.Deferred(),r=o.promise();return r.abort=function(){return n=!0,i?i.abort():(o.reject(null,"abort","abort"),r)},this._getFileInputFiles(t.fileInput).always((function(e){n||(e.length?(t.files=e,(i=a._onSend(null,t)).then((function(e,t,i){o.resolve(e,t,i)}),(function(e,t,i){o.reject(e,t,i)}))):o.reject())})),this._enhancePromise(r)}if(t.files=e.makeArray(t.files),t.files.length)return this._onSend(null,t)}return this._getXHRPromise(!1,t&&t.context)}})})),function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?e(require("jquery")):e(window.jQuery)}((function(e){"use strict";var t=0,i=e,n="parseJSON";"JSON"in window&&"parse"in JSON&&(i=JSON,n="parse"),e.ajaxTransport("iframe",(function(i){if(i.async){var n,a,o,r=i.initialIframeSrc||"javascript:false;";return{send:function(s,l){(n=e('
    ')).attr("accept-charset",i.formAcceptCharset),o=/\?/.test(i.url)?"&":"?","DELETE"===i.type?(i.url=i.url+o+"_method=DELETE",i.type="POST"):"PUT"===i.type?(i.url=i.url+o+"_method=PUT",i.type="POST"):"PATCH"===i.type&&(i.url=i.url+o+"_method=PATCH",i.type="POST"),a=e('').on("load",(function(){var t,o=e.isArray(i.paramName)?i.paramName:[i.paramName];a.off("load").on("load",(function(){var t;try{if(!(t=a.contents()).length||!t[0].firstChild)throw new Error}catch(e){t=void 0}l(200,"success",{iframe:t}),e('').appendTo(n),window.setTimeout((function(){n.remove()}),0)})),n.prop("target",a.prop("name")).prop("action",i.url).prop("method",i.type),i.formData&&e.each(i.formData,(function(t,i){e('').prop("name",i.name).val(i.value).appendTo(n)})),i.fileInput&&i.fileInput.length&&"POST"===i.type&&(t=i.fileInput.clone(),i.fileInput.after((function(e){return t[e]})),i.paramName&&i.fileInput.each((function(t){e(this).prop("name",o[t]||i.paramName)})),n.append(i.fileInput).prop("enctype","multipart/form-data").prop("encoding","multipart/form-data"),i.fileInput.removeAttr("form")),window.setTimeout((function(){n.submit(),t&&t.length&&i.fileInput.each((function(i,n){var a=e(t[i]);e(n).prop("name",a.prop("name")).attr("form",a.attr("form")),a.replaceWith(n)}))}),0)})),n.append(a).appendTo(document.body)},abort:function(){a&&a.off("load").prop("src",r),n&&n.remove()}}}})),e.ajaxSetup({converters:{"iframe text":function(t){return t&&e(t[0].body).text()},"iframe json":function(t){return t&&i[n](e(t[0].body).text())},"iframe html":function(t){return t&&e(t[0].body).html()},"iframe xml":function(t){var i=t&&t[0];return i&&e.isXMLDoc(i)?i:e.parseXML(i.XMLDocument&&i.XMLDocument.xml||e(i.body).html())},"iframe script":function(t){return t&&e.globalEval(e(t[0].body).text())}}})})); diff --git a/src/OrchardCore/OrchardCore.Media.Abstractions/Events/IMediaEventHandler.cs b/src/OrchardCore/OrchardCore.Media.Abstractions/Events/IMediaEventHandler.cs index bbe250df793..a58d051483a 100644 --- a/src/OrchardCore/OrchardCore.Media.Abstractions/Events/IMediaEventHandler.cs +++ b/src/OrchardCore/OrchardCore.Media.Abstractions/Events/IMediaEventHandler.cs @@ -13,5 +13,7 @@ public interface IMediaEventHandler Task MediaDeletedDirectoryAsync(MediaDeletedContext context); Task MediaMovingAsync(MediaMoveContext context); Task MediaMovedAsync(MediaMoveContext context); + Task MediaCreatingDirectoryAsync(MediaCreatingContext context); + Task MediaCreatedDirectoryAsync(MediaCreatedContext context); } } diff --git a/src/OrchardCore/OrchardCore.Media.Abstractions/Events/MediaCreatedContext.cs b/src/OrchardCore/OrchardCore.Media.Abstractions/Events/MediaCreatedContext.cs new file mode 100644 index 00000000000..3fa42542fda --- /dev/null +++ b/src/OrchardCore/OrchardCore.Media.Abstractions/Events/MediaCreatedContext.cs @@ -0,0 +1,7 @@ +namespace OrchardCore.Media.Events +{ + public class MediaCreatedContext : MediaContextBase + { + public bool Result { get; set; } + } +} diff --git a/src/OrchardCore/OrchardCore.Media.Abstractions/MediaOptions.cs b/src/OrchardCore/OrchardCore.Media.Abstractions/MediaOptions.cs index 2ea5f743ecb..7d915337c9a 100644 --- a/src/OrchardCore/OrchardCore.Media.Abstractions/MediaOptions.cs +++ b/src/OrchardCore/OrchardCore.Media.Abstractions/MediaOptions.cs @@ -26,6 +26,14 @@ public class MediaOptions /// public int MaxBrowserCacheDays { get; set; } + /// + /// The default number of days for the media cache control header when serving secure files. + /// + /// + /// Set to 0 to disable caching secure files. + /// + public int MaxSecureFilesBrowserCacheDays { get; set; } + /// /// The maximum number of days a cached resized media item will be valid for, before being rebuilt on request. /// diff --git a/src/OrchardCore/OrchardCore.Media.Core/DefaultMediaFileStore.cs b/src/OrchardCore/OrchardCore.Media.Core/DefaultMediaFileStore.cs index 56283cbf9c2..fb5db9b54ae 100644 --- a/src/OrchardCore/OrchardCore.Media.Core/DefaultMediaFileStore.cs +++ b/src/OrchardCore/OrchardCore.Media.Core/DefaultMediaFileStore.cs @@ -62,9 +62,26 @@ public virtual IAsyncEnumerable GetDirectoryContentAsync(string return _fileStore.GetDirectoryContentAsync(path, includeSubDirectories); } - public virtual Task TryCreateDirectoryAsync(string path) + public virtual async Task TryCreateDirectoryAsync(string path) { - return _fileStore.TryCreateDirectoryAsync(path); + var creatingContext = new MediaCreatingContext + { + Path = path + }; + + await _mediaEventHandlers.InvokeAsync((handler, context) => handler.MediaCreatingDirectoryAsync(context), creatingContext, _logger); + + var result = await _fileStore.TryCreateDirectoryAsync(path); + + var createdContext = new MediaCreatedContext + { + Path = path, + Result = result + }; + + await _mediaEventHandlers.InvokeAsync((handler, context) => handler.MediaCreatedDirectoryAsync(context), createdContext, _logger); + + return result; } public virtual async Task TryDeleteFileAsync(string path) diff --git a/src/OrchardCore/OrchardCore.Media.Core/Events/MediaEventHandlerBase.cs b/src/OrchardCore/OrchardCore.Media.Core/Events/MediaEventHandlerBase.cs index 45c25a8486e..62205c3c37d 100644 --- a/src/OrchardCore/OrchardCore.Media.Core/Events/MediaEventHandlerBase.cs +++ b/src/OrchardCore/OrchardCore.Media.Core/Events/MediaEventHandlerBase.cs @@ -34,5 +34,9 @@ public virtual Task MediaMovedAsync(MediaMoveContext context) { return Task.CompletedTask; } + + public virtual Task MediaCreatingDirectoryAsync(MediaCreatingContext context) => Task.CompletedTask; + + public virtual Task MediaCreatedDirectoryAsync(MediaCreatedContext context) => Task.CompletedTask; } } diff --git a/src/docs/reference/modules/Media/README.md b/src/docs/reference/modules/Media/README.md index ddca5341140..bc518964431 100644 --- a/src/docs/reference/modules/Media/README.md +++ b/src/docs/reference/modules/Media/README.md @@ -294,6 +294,10 @@ The following configuration values are used by default and can be customized: // NB: To control cache headers for module static assets, refer to the Orchard Core Modules Section. "MaxBrowserCacheDays": 30, + // The number of days to store secure media files in the browser cache. + // Set to 0 (default) to disable caching secure files. + "MaxSecureFilesBrowserCacheDays": 0, + // The number of days a cached resized media item will be valid for, before being rebuilt on request. "MaxCacheDays": 365, @@ -494,12 +498,32 @@ To set up indexing for Media do the following: 3. Configure the new field to be used for search. You can do this from the admin under Search, Settings, Search, and adding the name of the new field under "Default search fields" (arriving at something like "Content.ContentItem.FullText, BlogPost.File.MediaText, BlogPost.File.FileText"). 4. Try searching for content only available in the Media Text of selected media files, or referenced PDF files. You should see corresponding results. +## Secure Media + +The Secure Media feature enhances security and control over media files within the Media module. + +When enabled, administrators can set view permissions for the root media folder and each first-level folder within the media root. This allows for restricting access to media folders based on user roles, ensuring that only authorized users can view or manage media files within specific folders. + +New permissions to allow users to view their own media files, view media files uploaded by others, or both are created too. You can manage these among the other permissions with the [Roles module](../Roles/README.md). + +Media files attached to content items will also adhere to the `ViewContent` permission of the respective content item automatically. + +### Handling Unauthorized Access + +A middleware component returns a 404 NotFound response for unauthenticated access attempts to secured media files. This not only restricts access but also conceals the existence of the file, enhancing privacy and security. + +### Configurable Cache-Control for Secured Files + +The `Cache-Control` header for secured files is set to `no-store` by default, preventing their caching. This can be changed with the `MaxSecureFilesBrowserCacheDays` configuration, [see above](#configuration). + ## Videos + + ## Media Indexing The `Media Indexing` feature extends the media indexing capability to also encompass searching within files with the following extensions `.txt`, `.md`, `.docx`, and `.pptx`. diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 8c334fe1fda..1c1da3d520a 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -373,6 +373,10 @@ Furthermore, the introduction of the `NotificationOptions` provides configuratio - `TotalUnreadNotifications`: This property determines the maximum number of unread notifications displayed in the navigation bar, with a default setting of 10. - `DisableNotificationHtmlBodySanitizer`: By default, the `HtmlBody` of notifications generated from workflows undergoes a sanitization process. However, this property grants the option to bypass this sanitization process. +### Secure media files + +Introduces a new "Secure Media" feature for additional control over who can access media files. For more info read the [Secure Media](../reference/modules/Media/README.md#secure-media) docs. + ### Users Module Enhanced functionality has been implemented, giving developers the ability to control the expiration time of different tokens, such as those for password reset, email confirmation, and email change, which are sent through the email service. Below, you'll find a comprehensive list of configurable options along with their default values: diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs new file mode 100644 index 00000000000..0e6f6cbf288 --- /dev/null +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs @@ -0,0 +1,361 @@ +using OrchardCore.ContentManagement; +using OrchardCore.FileStorage; +using OrchardCore.Media; +using OrchardCore.Media.Services; +using OrchardCore.Security; +using OrchardCore.Security.AuthorizationHandlers; +using OrchardCore.Tests.Security; + +namespace OrchardCore.Tests.Modules.OrchardCore.Media.SecureMedia; + +public class ViewMediaFolderAuthorizationHandlerTests +{ + private const string UsersFolder = "_users"; + private const string MediafieldsFolder = "mediafields"; + + // Note: The handler must normalize the path (i.e. remove leading slashes). This is only tested for the root view permission. + + [Theory] + [InlineData("ViewRootMediaContent", "")] + [InlineData("ViewRootMediaContent", "/")] + [InlineData("ViewRootMediaContent", "filename.png")] + [InlineData("ViewRootMediaContent", "/filename.png")] + + // ViewMediaContent must allow root access as well. + [InlineData("ViewMediaContent", "")] + [InlineData("ViewMediaContent", "/")] + [InlineData("ViewMediaContent", "filename.png")] + [InlineData("ViewMediaContent", "/filename.png")] + + // ManageMediaFolder must also allow viewing, because it allows to manage all folders. + [InlineData("ManageMediaFolder", "")] + [InlineData("ManageMediaFolder", "/")] + [InlineData("ManageMediaFolder", "filename.png")] + [InlineData("ManageMediaFolder", "/filename.png")] + public async Task GrantsRootViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData("NotAllowed", "")] + [InlineData("NotAllowed", "/")] + [InlineData("NotAllowed", "filename.png")] + [InlineData("NotAllowed", "/filename.png")] + [InlineData("NotAllowed", "folder")] + [InlineData("NotAllowed", "/folder")] + [InlineData("NotAllowed", "non-existent-folder")] + [InlineData("NotAllowed", "/non-existent-folder")] + [InlineData("NotAllowed", "folder/filename.png")] + [InlineData("NotAllowed", "/folder/filename.png")] + [InlineData("ViewRootMediaContent", "folder")] + [InlineData("ViewRootMediaContent", "/folder")] + [InlineData("ViewRootMediaContent", "non-existent-folder")] + [InlineData("ViewRootMediaContent", "/non-existent-folder")] + [InlineData("ViewRootMediaContent", "folder/filename.png")] + [InlineData("ViewRootMediaContent", "/folder/filename.png")] + [InlineData("ViewRootMediaContent", "non-existent-folder/filename.png")] + [InlineData("ViewRootMediaContent", "/non-existent-folder/filename.png")] + + [InlineData("ViewRootMediaContent", UsersFolder)] + [InlineData("ViewRootMediaContent", "/" + UsersFolder)] + [InlineData("ViewRootMediaContent", UsersFolder + "/filename.png")] + [InlineData("ViewRootMediaContent", "/" + UsersFolder + "/filename.png")] + + [InlineData("ViewRootMediaContent", MediafieldsFolder)] + [InlineData("ViewRootMediaContent", "/" + MediafieldsFolder)] + [InlineData("ViewRootMediaContent", MediafieldsFolder + "/filename.png")] + [InlineData("ViewRootMediaContent", "/" + MediafieldsFolder + "/filename.png")] + public async Task DoesNotGrantRootViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory] + [InlineData("ViewMediaContent", "folder")] + [InlineData("ViewMediaContent", "folder/filename.png")] + + [InlineData("ViewMediaContent", "otherfolder")] + [InlineData("ViewMediaContent", "otherfolder/filename.png")] + + [InlineData("ViewMediaContent", "non-existent-folder")] + [InlineData("ViewMediaContent", "non-existent-folder/filename.png")] + + // ManageMediaFolder must also allow viewing, because it allows to manage all folders + [InlineData("ManageMediaFolder", "folder")] + [InlineData("ManageMediaFolder", "folder/filename.png")] + + [InlineData("ManageMediaFolder", "otherfolder")] + [InlineData("ManageMediaFolder", "otherfolder/filename.png")] + + [InlineData("ManageMediaFolder", "non-existent-folder")] + [InlineData("ManageMediaFolder", "non-existent-folder/filename.png")] + public async Task GrantsAllFoldersViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory] + + // Users and mediafields folders are not directly allowed by the ViewMediaContent permission. + + [InlineData("ViewMediaContent", UsersFolder)] + [InlineData("ViewMediaContent", UsersFolder + "/filename.png")] + [InlineData("ViewMediaContent", MediafieldsFolder)] + [InlineData("ViewMediaContent", MediafieldsFolder + "/filename.png")] + public async Task DoesNotGrantSpecialFoldersViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory] + [InlineData("ViewMediaContent_folder", "folder")] + [InlineData("ViewMediaContent_folder", "folder/filename.png")] + [InlineData("ViewMediaContent_folder", "/folder")] + [InlineData("ViewMediaContent_folder", "/folder/filename.png")] + public async Task GrantsFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData("ViewMediaContent_folder", "otherfolder")] + [InlineData("ViewMediaContent_folder", "otherfolder/filename.png")] + [InlineData("ViewMediaContent_folder", "/otherfolder")] + [InlineData("ViewMediaContent_folder", "/otherfolder/filename.png")] + + [InlineData("ViewMediaContent_otherfolder", "folder")] + [InlineData("ViewMediaContent_otherfolder", "folder/filename.png")] + [InlineData("ViewMediaContent_otherfolder", "/folder")] + [InlineData("ViewMediaContent_otherfolder", "/folder/filename.png")] + + [InlineData("ViewMediaContent_folder", "non-existent-folder")] + [InlineData("ViewMediaContent_folder", "non-existent-folder/filename.png")] + [InlineData("ViewMediaContent_folder", "/non-existent-folder")] + [InlineData("ViewMediaContent_folder", "/non-existent-folder/filename.png")] + + [InlineData("ViewMediaContent_folder", UsersFolder)] + [InlineData("ViewMediaContent_folder", UsersFolder + "/filename.png")] + + [InlineData("ViewMediaContent_folder", MediafieldsFolder)] + [InlineData("ViewMediaContent_folder", MediafieldsFolder + "/filename.png")] + public async Task DoesNotGrantFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + // Attached media fields folder is using content permissions, but user permissions for temp folder (tested below). + + [Theory] + [InlineData("ViewContent", MediafieldsFolder + "/content-type/content-item-id")] + [InlineData("ViewContent", MediafieldsFolder + "/content-type/content-item-id" + "/filename.png")] + public async Task GrantsMediafieldsFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData("NotAllowed", MediafieldsFolder + "/content_type/content-item-id")] + [InlineData("NotAllowed", MediafieldsFolder + "/content_type/content-item-id" + "/filename.png")] + + [InlineData("ViewMediaContent_folder", MediafieldsFolder)] + [InlineData("ViewMediaContent_folder", MediafieldsFolder + "/filename.png")] + + [InlineData("ManageMediaFolder", MediafieldsFolder)] + [InlineData("ManageMediaFolder", MediafieldsFolder + "/filename.png")] + public async Task DoesNotGrantMediafieldsFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + // User folders + // Note: Temp files for attached media fields are also handled like _Users folder. + [Theory] + [InlineData("ViewOwnMediaContent", UsersFolder + "/user-folder/")] + [InlineData("ViewOwnMediaContent", UsersFolder + "/user-folder/filename.png")] + + [InlineData("ViewOwnMediaContent", MediafieldsFolder + "/temp/user-folder/")] + [InlineData("ViewOwnMediaContent", MediafieldsFolder + "/temp/user-folder/filename.png")] + public async Task GrantsOwnUserFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory] + [InlineData("ViewOwnMediaContent", UsersFolder + "/other-user-folder/")] + [InlineData("ViewOwnMediaContent", UsersFolder + "/other-user-folder/filename.png")] + + [InlineData("ViewOwnMediaContent", MediafieldsFolder + "/temp/other-user-folder/")] + [InlineData("ViewOwnMediaContent", MediafieldsFolder + "/temp/other-user-folder/filename.png")] + public async Task DoesNotGrantOwnUserFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory] + [InlineData("ViewOthersMediaContent", UsersFolder + "/user-folder/")] + [InlineData("ViewOthersMediaContent", UsersFolder + "/user-folder/filename.png")] + + [InlineData("ViewOthersMediaContent", MediafieldsFolder + "/temp/user-folder/")] + [InlineData("ViewOthersMediaContent", MediafieldsFolder + "/temp/user-folder/filename.png")] + + [InlineData("ViewOthersMediaContent", UsersFolder + "/other-user-folder/")] + [InlineData("ViewOthersMediaContent", UsersFolder + "/other-user-folder/filename.png")] + + [InlineData("ViewOthersMediaContent", MediafieldsFolder + "/temp/other-user-folder/")] + [InlineData("ViewOthersMediaContent", MediafieldsFolder + "/temp/other-user-folder/filename.png")] + public async Task GrantsOtherUserFolderViewPermission(string permission, string resource) + { + // Arrange + var handler = CreateHandler(); + var context = PermissionHandlerHelper.CreateTestAuthorizationHandlerContext(SecureMediaPermissions.ViewMedia, [permission], true, resource); + + // Act + await handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + private static ViewMediaFolderAuthorizationHandler CreateHandler() + { + var defaultHttpContext = new DefaultHttpContext(); + var httpContextAccessor = Mock.Of(hca => hca.HttpContext == defaultHttpContext); + + var mockMediaFileStore = new Mock(); + mockMediaFileStore.Setup(fs => fs.GetDirectoryInfoAsync(It.IsAny())); + mockMediaFileStore.Setup(fs => fs.GetDirectoryInfoAsync(It.Is("folder", StringComparer.Ordinal))).ReturnsAsync(Mock.Of(e => e.IsDirectory == true)); + mockMediaFileStore.Setup(fs => fs.GetDirectoryInfoAsync(It.Is("otherfolder", StringComparer.Ordinal))).ReturnsAsync(Mock.Of(e => e.IsDirectory == true)); + mockMediaFileStore.Setup(fs => fs.GetFileInfoAsync(It.IsAny())); + mockMediaFileStore.Setup(fs => fs.GetFileInfoAsync(It.Is("filename.png", StringComparer.Ordinal))).ReturnsAsync(Mock.Of(e => e.IsDirectory == false)); + + var mockMediaOptions = new Mock>(); + mockMediaOptions.Setup(o => o.Value).Returns(new MediaOptions + { + AssetsUsersFolder = UsersFolder, + AllowedFileExtensions = [".png"] + }); + + var mockUserAssetFolderNameProvider = new Mock(); + mockUserAssetFolderNameProvider.Setup(afp => afp.GetUserAssetFolderName(It.Is(ci => ci.Identity.AuthenticationType == "Test"))).Returns("user-folder"); + + var mockContentManager = new Mock(); + mockContentManager.Setup(cm => cm.GetAsync(It.IsAny())).ReturnsAsync(Mock.Of()); // Pretends an existing content item. + + var attachedMediaFieldFileService = new AttachedMediaFieldFileService( + mockMediaFileStore.Object, + httpContextAccessor, + mockUserAssetFolderNameProvider.Object, + NullLogger< AttachedMediaFieldFileService>.Instance); + + // Create an IAuthorizationService mock that mimics how OC is granting permissions. + var mockAuthorizationService = new Mock(); + mockAuthorizationService + .Setup(authorizeService => authorizeService.AuthorizeAsync(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns>(async (user, resource, requirements) => + { + var context = new AuthorizationHandlerContext(requirements, user, resource); + var permissionGrantingService = new DefaultPermissionGrantingService(); + var handler = new PermissionHandler(permissionGrantingService); + + await handler.HandleAsync(context); + + return new DefaultAuthorizationEvaluator().Evaluate(context); + }); + + var services = new ServiceCollection(); + services.AddTransient(sp => mockAuthorizationService.Object); + var serviceProvider = services.BuildServiceProvider(); + + return new ViewMediaFolderAuthorizationHandler( + serviceProvider, + httpContextAccessor, + attachedMediaFieldFileService, + mockMediaFileStore.Object, + mockMediaOptions.Object, + mockUserAssetFolderNameProvider.Object, + mockContentManager.Object + ); + } +} + diff --git a/test/OrchardCore.Tests/Security/PermissionHandlerHelper.cs b/test/OrchardCore.Tests/Security/PermissionHandlerHelper.cs index 7da5a4f5a6c..86f6609d3c9 100644 --- a/test/OrchardCore.Tests/Security/PermissionHandlerHelper.cs +++ b/test/OrchardCore.Tests/Security/PermissionHandlerHelper.cs @@ -5,7 +5,7 @@ namespace OrchardCore.Tests.Security { public static class PermissionHandlerHelper { - public static AuthorizationHandlerContext CreateTestAuthorizationHandlerContext(Permission required, string[] allowed = null, bool authenticated = false) + public static AuthorizationHandlerContext CreateTestAuthorizationHandlerContext(Permission required, string[] allowed = null, bool authenticated = false, object resource = null) { var identity = authenticated ? new ClaimsIdentity("Test") : new ClaimsIdentity(); @@ -24,7 +24,7 @@ public static AuthorizationHandlerContext CreateTestAuthorizationHandlerContext( return new AuthorizationHandlerContext( new[] { new PermissionRequirement(required) }, principal, - null); + resource); } public static async Task SuccessAsync(this AuthorizationHandlerContext context, params string[] permissionNames)