Compare commits

...
Author SHA1 Message Date
Andy ButlandandCopilot 9b2fd1253b Optimize initialization of document URLs on start-up (#19498)
* Optimize initialization of document URLs on startup.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-10 10:31:26 +02:00
Andy Butland 024697f3bf Bumped version to 15.4.3 2025-06-10 10:28:55 +02:00
Andy ButlandandGitHub d920e93d1e Merge commit from fork 2025-06-03 05:21:11 +02:00
Andy Butland 853c1acf7e Bumped version to 15.4.2 2025-05-20 13:10:02 +01:00
Andy ButlandandGitHub 0ad020f0ce Restored interface methods on obsolete IBackgroundTaskQueue (#19223)
* Restore interface methods on obsolete IBackgroundTaskQueue.

* Fixed typos in comment.
2025-05-05 07:38:31 +02:00
Andy Butland fc1455e0d8 Bumped version to 15.4.1. 2025-05-03 17:28:44 +02:00
Andy Butland 15a8b4066a Bumped version to 15.4.0. 2025-04-30 15:04:08 +02:00
Andy ButlandandGitHub 38f6e2b0da Updates Examine to latest patch release. (#19193) 2025-04-30 09:57:55 +02:00
Andy Butland 14f60a108a V13: Clear Member Username Cache in Load Balanced Environments (#19191)
* Clear usernamekey

* Odd explaining comment

* Update src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Make UserNameCachePrefix readonly for better immutabilityly

* Move prefix to CacheKeys constants

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
# Conflicts:
#	src/Umbraco.Core/Cache/CacheKeys.cs
2025-04-29 18:10:44 +02:00
Jacob Overgaard 375a0b4388 bump to 15.4.0-rc3 2025-04-29 15:34:59 +02:00
Jacob OvergaardandGitHub 6300ccc211 fix: The server path is not appended to stylesheets before load in Backoffice (#19192)
* build(deps): bump tiptap from 2.11.5 to 2.11.7

* fix: prepends the system `/css` folder to stylesheets before attempting to load them

* fix: adds more safety around path assumptions

* chore: eslint fix

* fix: prepend only the system path to picked stylesheets
2025-04-29 13:33:26 +00:00
Jacob Overgaard 5b6f544d2a build: restores some of the behavior from V13 in relation to StaticAssets (#19189)
In v13, the StaticAssets build was only triggered based on the existence of either the output folder or a preserve.* marker file. Here, we also additionally check for the node_modules/.package-lock.json file before reinstalling npm dependencies. We also now only run `npm install` rather than `npm ci` to optimise the build.
2025-04-29 13:10:29 +02:00
e932fa5404 Webhook log authorization and file system path checks (#19177)
* Add authorization for webhooks to item and log endpoints.

* Remove full path details from exception when requesting a path outside of the physical file system's root.

* Added missing usings.

* Revert changes to the webhook items API

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-04-28 12:10:43 +00:00
mole 83e580c3a7 Fix nuget publish stage 2025-04-25 13:31:34 +02:00
Andy Butlandandmole 31ee35e721 Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project (#19077)
* Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project.

* Fix nullability

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-04-24 13:18:57 +02:00
MoleandKenn Jacobsen cf0f3f1380 V15: Ensure elements cache is cleared on subscribers in load balanced scenarios (#19128)
* Clear elementscache from cache refreshers

* Add very simple test ensuring the elements cache is cleared

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-24 13:18:53 +02:00
Sven Geusensandkjac 31c3f5ae0c Remove fake null checks as they are no longer needed after merge into v16 (#19109) 2025-04-24 12:28:54 +02:00
24 changed files with 721 additions and 259 deletions
+3 -3
View File
@@ -45,8 +45,8 @@
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.0" />
<PackageVersion Include="Examine.Core" Version="3.7.0" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
@@ -100,4 +100,4 @@
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
</Project>
+4
View File
@@ -829,6 +829,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to pre-release feed
steps:
- checkout: none
@@ -890,6 +892,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
@@ -5,11 +6,13 @@ using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
[ApiExplorerSettings(GroupName = "Webhook")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)]
public class WebhookLogControllerBase : ManagementApiControllerBase
{
protected PagedViewModel<WebhookLogResponseModel> CreatePagedWebhookLogResponseModel(PagedModel<WebhookLog> logs, IWebhookPresentationFactory webhookPresentationFactory)
@@ -26,23 +26,36 @@
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
</ItemGroup>
<!-- Restore and build backoffice project -->
<!-- General ignored files -->
<ItemGroup>
<Content Remove="wwwroot\umbraco\assets\README.md" />
</ItemGroup>
<!-- BEGIN: Restore and build backoffice project -->
<PropertyGroup>
<BackofficeProjectDirectory Condition="'$(BackofficeProjectDirectory)' == ''">..\Umbraco.Web.UI.Client\</BackofficeProjectDirectory>
<BackofficeAssetsPath>wwwroot\umbraco\backoffice</BackofficeAssetsPath>
<BackofficeAssetsPath>$(ProjectDir)wwwroot\umbraco\backoffice</BackofficeAssetsPath>
</PropertyGroup>
<ItemGroup>
<BackofficeAssetsInputs Include="$(BackofficeProjectDirectory)package.json;$(BackofficeProjectDirectory)package-lock.json;$(BackofficeProjectDirectory)src\**" Exclude="$(DefaultItemExcludes)" />
<Content Remove="$(BackofficeAssetsPath)\**" />
</ItemGroup>
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(BackofficeProjectDirectory)" />
<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
<Message Text="Skip BuildBackoffice target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
<Message Text="Skip BuildBackoffice target because '$(BackofficeAssetsPath)' already exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)')" />
<Message Text="Call BuildBackoffice target because UmbracoBuild is empty (this is Visual Studio) and '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
<CallTarget Targets="BuildBackoffice" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
</Target>
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice" BeforeTargets="AssignTargetPaths" Inputs="@(BackofficeAssetsInputs)" Outputs="$(IntermediateOutputPath)backoffice.complete.txt">
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(BackofficeProjectDirectory)" />
</Target>
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice">
<Message Importance="high" Text="Executing Backoffice NPM build script..." />
<Exec Command="npm run build:for:cms" WorkingDirectory="$(BackofficeProjectDirectory)" />
<ItemGroup>
@@ -61,27 +74,46 @@
</DefineStaticWebAssets>
</Target>
<!-- Restore and build login project -->
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
<Message Text="Skip CleanBackoffice target because '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(BackofficeAssetsPath)')" />
<Message Text="Skip CleanBackoffice target because preserve.backoffice marker file exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and Exists('$(SolutionDir)preserve.backoffice')" />
<Message Text="Call CleanBackoffice target because '$(BackofficeAssetsPath)' exists and preserve.backoffice marker file doesn't exist" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
<CallTarget Targets="CleanBackoffice" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
</Target>
<Target Name="CleanBackoffice">
<ItemGroup>
<BackofficeDirectories Include="$(BackofficeAssetsPath)" />
</ItemGroup>
<RemoveDir Directories="@(BackofficeDirectories)" />
</Target>
<!-- END: Restore and build backoffice project -->
<!-- BEGIN: Restore and build login project -->
<PropertyGroup>
<LoginProjectDirectory Condition="'$(LoginProjectDirectory)' == ''">..\Umbraco.Web.UI.Login\</LoginProjectDirectory>
<LoginAssetsPath>wwwroot\umbraco\login</LoginAssetsPath>
<LoginAssetsPath>$(ProjectDir)wwwroot\umbraco\login</LoginAssetsPath>
</PropertyGroup>
<ItemGroup>
<LoginAssetsInputs Include="$(LoginProjectDirectory)**" Exclude="$(DefaultItemExcludes)" />
<Content Remove="$(LoginAssetsPath)\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="wwwroot\umbraco\assets\README.md" />
</ItemGroup>
<Target Name="BuildLoginStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
<Message Text="Skip BuildLogin target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
<Message Text="Skip BuildLogin target because '$(LoginAssetsPath)' already exists" Importance="high" Condition="Exists('$(LoginAssetsPath)')" />
<Message Text="Call BuildLogin target because UmbracoBuild is empty (this is Visual Studio) and '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
<CallTarget Targets="BuildLogin" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
</Target>
<Target Name="RestoreLogin" Inputs="$(LoginProjectDirectory)package-lock.json" Outputs="$(LoginProjectDirectory)node_modules/.package-lock.json">
<Message Importance="high" Text="Restoring Login NPM packages..." />
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(LoginProjectDirectory)" />
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(LoginProjectDirectory)" />
</Target>
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin" BeforeTargets="AssignTargetPaths" Inputs="@(LoginAssetsInputs)" Outputs="$(IntermediateOutputPath)login.complete.txt">
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin">
<Message Importance="high" Text="Executing Login NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="$(LoginProjectDirectory)" />
<ItemGroup>
@@ -99,4 +131,19 @@
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
</DefineStaticWebAssets>
</Target>
<Target Name="CleanLoginStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
<Message Text="Skip CleanLogin target because '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(LoginAssetsPath)')" />
<Message Text="Skip CleanLogin target because preserve.login marker file exists" Importance="high" Condition="Exists('$(LoginAssetsPath)') and Exists('$(SolutionDir)preserve.login')" />
<Message Text="Call CleanLogin target because '$(LoginAssetsPath)' exists and preserve.login marker file doesn't exist" Importance="high" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
<CallTarget Targets="CleanLogin" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
</Target>
<Target Name="CleanLogin">
<ItemGroup>
<LoginDirectories Include="$(LoginAssetsPath)" />
</ItemGroup>
<RemoveDir Directories="@(LoginDirectories)" />
</Target>
<!-- END: Restore and build login project -->
</Project>
+2
View File
@@ -22,4 +22,6 @@ public static class CacheKeys
public const string PreviewPropertyCacheKeyPrefix = "Cache.Property.CacheValues[D:";
public const string PropertyCacheKeyPrefix = "Cache.Property.CacheValues[P:";
public const string MemberUserNameCachePrefix = "uRepo_userNameKey+";
}
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -21,9 +23,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private readonly IContentService _contentService;
private readonly IDocumentCacheService _documentCacheService;
private readonly ICacheManager _cacheManager;
private readonly IPublishStatusManagementService _publishStatusManagementService;
private readonly IIdKeyMap _idKeyMap;
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -38,6 +42,39 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
IContentService contentService,
IPublishStatusManagementService publishStatusManagementService,
IDocumentCacheService documentCacheService)
: this(
appCaches,
serializer,
idKeyMap,
domainService,
eventAggregator,
factory,
documentUrlService,
domainCacheService,
documentNavigationQueryService,
documentNavigationManagementService,
contentService,
publishStatusManagementService,
documentCacheService,
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
{
}
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IDomainService domainService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IDocumentUrlService documentUrlService,
IDomainCacheService domainCacheService,
IDocumentNavigationQueryService documentNavigationQueryService,
IDocumentNavigationManagementService documentNavigationManagementService,
IContentService contentService,
IPublishStatusManagementService publishStatusManagementService,
IDocumentCacheService documentCacheService,
ICacheManager cacheManager)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
@@ -49,6 +86,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
_contentService = contentService;
_documentCacheService = documentCacheService;
_publishStatusManagementService = publishStatusManagementService;
// TODO: Ideally we should inject IElementsCache
// this interface is in infrastructure, and changing this is very breaking
// so as long as we have the cache manager, which casts the IElementsCache to a simple AppCache we might as well use that.
_cacheManager = cacheManager;
}
#region Indirect
@@ -83,6 +125,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager.ElementsCache.Clear();
var idsRemoved = new HashSet<int>();
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IContent>();
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -18,7 +20,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
private readonly IMediaService _mediaService;
private readonly IMediaCacheService _mediaCacheService;
private readonly ICacheManager _cacheManager;
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
public MediaCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -29,6 +33,31 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
IMediaNavigationManagementService mediaNavigationManagementService,
IMediaService mediaService,
IMediaCacheService mediaCacheService)
: this(
appCaches,
serializer,
idKeyMap,
eventAggregator,
factory,
mediaNavigationQueryService,
mediaNavigationManagementService,
mediaService,
mediaCacheService,
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
{
}
public MediaCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IMediaNavigationQueryService mediaNavigationQueryService,
IMediaNavigationManagementService mediaNavigationManagementService,
IMediaService mediaService,
IMediaCacheService mediaCacheService,
ICacheManager cacheManager)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
@@ -36,6 +65,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
_mediaNavigationManagementService = mediaNavigationManagementService;
_mediaService = mediaService;
_mediaCacheService = mediaCacheService;
// TODO: Use IElementsCache instead of ICacheManager, see ContentCacheRefresher for more information.
_cacheManager = cacheManager;
}
#region Indirect
@@ -87,6 +119,13 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager.ElementsCache.Clear();
foreach (JsonPayload payload in payloads)
{
if (payload.ChangeTypes == TreeChangeTypes.Remove)
@@ -71,11 +71,22 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
foreach (JsonPayload p in payloads)
{
_idKeyMap.ClearCache(p.Id);
if (memberCache.Success)
if (memberCache.Success is false)
{
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
continue;
}
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
// This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided.
// This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here.
// Ideally, we want to use a shared way of generating the key between this and the repository.
// Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository
// See:
// https://github.com/umbraco/Umbraco-CMS/pull/17350
// https://github.com/umbraco/Umbraco-CMS/pull/17815
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
}
}
}
@@ -5,14 +5,17 @@ namespace Umbraco.Extensions;
public static class ContentSettingsExtensions
{
/// <summary>
/// Determines if file extension is allowed for upload based on (optional) white list and black list
/// held in settings.
/// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted.
/// Determines if file extension is allowed for upload based on (optional) allow list and deny list held in settings.
/// Disallowed file extensions are only considered if there are no allowed file extensions.
/// </summary>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) =>
contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false &&
contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) == false);
/// <param name="contentSettings">The content settings.</param>
/// <param name="extension">The file extension.</param>
/// <returns>
/// <c>true</c> if the file extension is allowed for upload; otherwise, <c>false</c>.
/// </returns>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension)
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) == false);
/// <summary>
/// Gets the auto-fill configuration for a specified property alias.
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.HostedServices;
public interface IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed on in the background.
/// Enqueue a work item to be executed in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
+1 -1
View File
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
}
/// <summary>
+45 -25
View File
@@ -44,26 +44,51 @@ public class DocumentUrlService : IDocumentUrlService
/// <summary>
/// Model used to cache a single published document along with all it's URL segments.
/// </summary>
private class PublishedDocumentUrlSegments
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
internal class PublishedDocumentUrlSegments
{
/// <summary>
/// Gets or sets the document key.
/// </summary>
public required Guid DocumentKey { get; set; }
/// <summary>
/// Gets or sets the language Id.
/// </summary>
public required int LanguageId { get; set; }
/// <summary>
/// Gets or sets the collection of <see cref="UrlSegment"/> for the document, language and state.
/// </summary>
public required IList<UrlSegment> UrlSegments { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the document is a draft version or not.
/// </summary>
public required bool IsDraft { get; set; }
/// <summary>
/// Model used to represent a URL segment for a document in the cache.
/// </summary>
public class UrlSegment
{
/// <summary>
/// Initializes a new instance of the <see cref="UrlSegment"/> class.
/// </summary>
public UrlSegment(string segment, bool isPrimary)
{
Segment = segment;
IsPrimary = isPrimary;
}
/// <summary>
/// Gets the URL segment string.
/// </summary>
public string Segment { get; }
/// <summary>
/// Gets a value indicating whether this URL segment is the primary one for the document, language and state.
/// </summary>
public bool IsPrimary { get; }
}
}
@@ -168,45 +193,40 @@ public class DocumentUrlService : IDocumentUrlService
scope.Complete();
}
private static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
/// <summary>
/// Converts a collection of <see cref="PublishedDocumentUrlSegment"/> to a collection of <see cref="PublishedDocumentUrlSegments"/> for caching purposes.
/// </summary>
/// <param name="publishedDocumentUrlSegments">The collection of <see cref="PublishedDocumentUrlSegment"/> retrieved from the database on startup.</param>
/// <returns>The collection of cache models.</returns>
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
internal static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
{
var cacheModels = new List<PublishedDocumentUrlSegments>();
var cacheModels = new Dictionary<(Guid DocumentKey, int LanguageId, bool IsDraft), PublishedDocumentUrlSegments>();
foreach (PublishedDocumentUrlSegment model in publishedDocumentUrlSegments)
{
PublishedDocumentUrlSegments? existingCacheModel = GetModelFromCache(cacheModels, model);
if (existingCacheModel is null)
(Guid DocumentKey, int LanguageId, bool IsDraft) key = (model.DocumentKey, model.LanguageId, model.IsDraft);
if (!cacheModels.TryGetValue(key, out PublishedDocumentUrlSegments? existingCacheModel))
{
cacheModels.Add(new PublishedDocumentUrlSegments
cacheModels[key] = new PublishedDocumentUrlSegments
{
DocumentKey = model.DocumentKey,
LanguageId = model.LanguageId,
UrlSegments = [new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary)],
IsDraft = model.IsDraft,
});
};
}
else
{
existingCacheModel.UrlSegments = GetUpdatedUrlSegments(existingCacheModel.UrlSegments, model.UrlSegment, model.IsPrimary);
if (existingCacheModel.UrlSegments.Any(x => x.Segment == model.UrlSegment) is false)
{
existingCacheModel.UrlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary));
}
}
}
return cacheModels;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static PublishedDocumentUrlSegments? GetModelFromCache(List<PublishedDocumentUrlSegments> cacheModels, PublishedDocumentUrlSegment model)
=> cacheModels
.SingleOrDefault(x => x.DocumentKey == model.DocumentKey && x.LanguageId == model.LanguageId && x.IsDraft == model.IsDraft);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IList<PublishedDocumentUrlSegments.UrlSegment> GetUpdatedUrlSegments(IList<PublishedDocumentUrlSegments.UrlSegment> urlSegments, string segment, bool isPrimary)
{
if (urlSegments.FirstOrDefault(x => x.Segment == segment) is null)
{
urlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(segment, isPrimary));
}
return urlSegments;
return cacheModels.Values;
}
private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode, bool isDraft)
@@ -9,4 +9,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
[Obsolete("This has been relocated into Umbraco.Cms.Core. This definition in Umbraco.Cms.Infrastructure is scheduled for removal in Umbraco 17.")]
public interface IBackgroundTaskQueue : Core.HostedServices.IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}
@@ -39,7 +39,6 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
private readonly ITagRepository _tagRepository;
private bool _passwordConfigInitialized;
private string? _passwordConfigJson;
private const string UsernameCacheKey = "uRepo_userNameKey+";
public MemberRepository(
IScopeAccessor scopeAccessor,
@@ -327,7 +326,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
}
public IMember? GetByUsername(string? username) =>
_memberByUsernameCachePolicy.GetByUserName(UsernameCacheKey, username, PerformGetByUsername, PerformGetAllByUsername);
_memberByUsernameCachePolicy.GetByUserName(CacheKeys.MemberUserNameCachePrefix, username, PerformGetByUsername, PerformGetAllByUsername);
public int[] GetMemberIds(string[] usernames)
{
@@ -609,7 +608,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
protected override void PersistDeletedItem(IMember entity)
{
_memberByUsernameCachePolicy.DeleteByUserName(UsernameCacheKey, entity.Username);
_memberByUsernameCachePolicy.DeleteByUserName(CacheKeys.MemberUserNameCachePrefix, entity.Username);
base.PersistDeletedItem(entity);
}
@@ -943,7 +942,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages()));
_memberByUsernameCachePolicy.DeleteByUserName(UsernameCacheKey, entity.Username);
_memberByUsernameCachePolicy.DeleteByUserName(CacheKeys.MemberUserNameCachePrefix, entity.Username);
entity.ResetDirtyProperties();
}
@@ -25,65 +25,40 @@ internal sealed class CacheRefreshingNotificationHandler :
{
private readonly IDocumentCacheService _documentCacheService;
private readonly IMediaCacheService _mediaCacheService;
private readonly IElementsCache _elementsCache;
private readonly IRelationService _relationService;
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
public CacheRefreshingNotificationHandler(
IDocumentCacheService documentCacheService,
IMediaCacheService mediaCacheService,
IElementsCache elementsCache,
IRelationService relationService,
IPublishedContentTypeCache publishedContentTypeCache)
{
_documentCacheService = documentCacheService;
_mediaCacheService = mediaCacheService;
_elementsCache = elementsCache;
_relationService = relationService;
_publishedContentTypeCache = publishedContentTypeCache;
}
public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken)
{
ClearElementsCache();
await _documentCacheService.RefreshContentAsync(notification.Entity);
}
=> await _documentCacheService.RefreshContentAsync(notification.Entity);
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken)
{
foreach (IContent deletedEntity in notification.DeletedEntities)
{
ClearElementsCache();
await _documentCacheService.DeleteItemAsync(deletedEntity);
}
}
public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken)
{
ClearElementsCache();
await _mediaCacheService.RefreshMediaAsync(notification.Entity);
}
=> await _mediaCacheService.RefreshMediaAsync(notification.Entity);
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken)
{
foreach (IMedia deletedEntity in notification.DeletedEntities)
{
ClearElementsCache();
await _mediaCacheService.DeleteItemAsync(deletedEntity);
}
}
private void ClearElementsCache()
{
// Ideally we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes. or what their keys are.
// This is because currently published elements lives exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys are to actually parse this data with a specific value converter, and for all cultures, which is not feasible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this,
_elementsCache.Clear();
}
public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
+141 -141
View File
@@ -1,32 +1,32 @@
{
"name": "@umbraco-cms/backoffice",
"version": "15.4.0-rc2",
"version": "15.4.0-rc3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "15.4.0-rc2",
"version": "15.4.0-rc3",
"license": "MIT",
"workspaces": [
"./src/packages/*"
],
"dependencies": {
"@tiptap/core": "2.11.5",
"@tiptap/extension-character-count": "^2.11.5",
"@tiptap/extension-image": "2.11.5",
"@tiptap/extension-link": "2.11.5",
"@tiptap/extension-placeholder": "2.11.5",
"@tiptap/extension-subscript": "2.11.5",
"@tiptap/extension-superscript": "2.11.5",
"@tiptap/extension-table": "2.11.5",
"@tiptap/extension-table-cell": "2.11.5",
"@tiptap/extension-table-header": "2.11.5",
"@tiptap/extension-table-row": "2.11.5",
"@tiptap/extension-text-align": "2.11.5",
"@tiptap/extension-underline": "2.11.5",
"@tiptap/pm": "2.11.5",
"@tiptap/starter-kit": "2.11.5",
"@tiptap/core": "2.11.7",
"@tiptap/extension-character-count": "2.11.7",
"@tiptap/extension-image": "2.11.7",
"@tiptap/extension-link": "2.11.7",
"@tiptap/extension-placeholder": "2.11.7",
"@tiptap/extension-subscript": "2.11.7",
"@tiptap/extension-superscript": "2.11.7",
"@tiptap/extension-table": "2.11.7",
"@tiptap/extension-table-cell": "2.11.7",
"@tiptap/extension-table-header": "2.11.7",
"@tiptap/extension-table-row": "2.11.7",
"@tiptap/extension-text-align": "2.11.7",
"@tiptap/extension-underline": "2.11.7",
"@tiptap/pm": "2.11.7",
"@tiptap/starter-kit": "2.11.7",
"@types/diff": "^7.0.1",
"@umbraco-ui/uui": "^1.13.0",
"@umbraco-ui/uui-css": "^1.13.0",
@@ -2534,9 +2534,9 @@
}
},
"node_modules/@tiptap/core": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.5.tgz",
"integrity": "sha512-jb0KTdUJaJY53JaN7ooY3XAxHQNoMYti/H6ANo707PsLXVeEqJ9o8+eBup1JU5CuwzrgnDc2dECt2WIGX9f8Jw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.7.tgz",
"integrity": "sha512-zN+NFFxLsxNEL8Qioc+DL6b8+Tt2bmRbXH22Gk6F6nD30x83eaUSFlSv3wqvgyCq3I1i1NO394So+Agmayx6rQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2547,9 +2547,9 @@
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.11.5.tgz",
"integrity": "sha512-MZfcRIzKRD8/J1hkt/eYv49060GTL6qGR3NY/oTDuw2wYzbQXXLEbjk8hxAtjwNn7G+pWQv3L+PKFzZDxibLuA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.11.7.tgz",
"integrity": "sha512-liD8kWowl3CcYCG9JQlVx1eSNc/aHlt6JpVsuWvzq6J8APWX693i3+zFqyK2eCDn0k+vW62muhSBe3u09hA3Zw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2560,9 +2560,9 @@
}
},
"node_modules/@tiptap/extension-bold": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.11.5.tgz",
"integrity": "sha512-OAq03MHEbl7MtYCUzGuwb0VpOPnM0k5ekMbEaRILFU5ZC7cEAQ36XmPIw1dQayrcuE8GZL35BKub2qtRxyC9iA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.11.7.tgz",
"integrity": "sha512-VTR3JlldBixXbjpLTFme/Bxf1xeUgZZY3LTlt5JDlCW3CxO7k05CIa+kEZ8LXpog5annytZDUVtWqxrNjmsuHQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2573,9 +2573,9 @@
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.5.tgz",
"integrity": "sha512-VXwHlX6A/T6FAspnyjbKDO0TQ+oetXuat6RY1/JxbXphH42nLuBaGWJ6pgy6xMl6XY8/9oPkTNrfJw/8/eeRwA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.7.tgz",
"integrity": "sha512-WbPogE2/Q3e3/QYgbT1Sj4KQUfGAJNc5pvb7GrUbvRQsAh7HhtuO8hqdDwH8dEdD/cNUehgt17TO7u8qV6qeBw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2586,9 +2586,9 @@
}
},
"node_modules/@tiptap/extension-character-count": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.11.5.tgz",
"integrity": "sha512-Da2VGb7ClmKwXdQdQC2735qylYD8/MQAPA0skPEcHxcDTDuI8ibyIDnMPnczgS/hR5g0TYE2DQp/dkhJXeovkQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.11.7.tgz",
"integrity": "sha512-gcVbKou+uxzg8N0BBKceLwtpWvN8g2TIjTuCdyAcAPukX63DqVWOkofFHn1RqZbstJmtF4pTGZs9OH/GJrp27Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2600,9 +2600,9 @@
}
},
"node_modules/@tiptap/extension-code": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.5.tgz",
"integrity": "sha512-xOvHevNIQIcCCVn9tpvXa1wBp0wHN/2umbAZGTVzS+AQtM7BTo0tz8IyzwxkcZJaImONcUVYLOLzt2AgW1LltA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.7.tgz",
"integrity": "sha512-VpPO1Uy/eF4hYOpohS/yMOcE1C07xmMj0/D989D9aS1x95jWwUVrSkwC+PlWMUBx9PbY2NRsg1ZDwVvlNKZ6yQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2613,9 +2613,9 @@
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.5.tgz",
"integrity": "sha512-ksxMMvqLDlC+ftcQLynqZMdlJT1iHYZorXsXw/n+wuRd7YElkRkd6YWUX/Pq/njFY6lDjKiqFLEXBJB8nrzzBA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.7.tgz",
"integrity": "sha512-To/y/2H04VWqiANy53aXjV7S6fA86c2759RsH1hTIe57jA1KyE7I5tlAofljOLZK/covkGmPeBddSPHGJbz++Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2627,9 +2627,9 @@
}
},
"node_modules/@tiptap/extension-document": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.5.tgz",
"integrity": "sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.7.tgz",
"integrity": "sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2640,9 +2640,9 @@
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.5.tgz",
"integrity": "sha512-uIN7L3FU0904ec7FFFbndO7RQE/yiON4VzAMhNn587LFMyWO8US139HXIL4O8dpZeYwYL3d1FnDTflZl6CwLlg==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.7.tgz",
"integrity": "sha512-63mL+nxQILizsr5NbmgDeOjFEWi34BLt7evwL6UUZEVM15K8V1G8pD9Y0kCXrZYpHWz0tqFRXdrhDz0Ppu8oVw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2654,9 +2654,9 @@
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.5.tgz",
"integrity": "sha512-kcWa+Xq9cb6lBdiICvLReuDtz/rLjFKHWpW3jTTF3FiP3wx4H8Rs6bzVtty7uOVTfwupxZRiKICAMEU6iT0xrQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.7.tgz",
"integrity": "sha512-EceesmPG7FyjXZ8EgeJPUov9G1mAf2AwdypxBNH275g6xd5dmU/KvjoFZjmQ0X1ve7mS+wNupVlGxAEUYoveew==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2668,9 +2668,9 @@
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.5.tgz",
"integrity": "sha512-q9doeN+Yg9F5QNTG8pZGYfNye3tmntOwch683v0CCVCI4ldKaLZ0jG3NbBTq+mosHYdgOH2rNbIORlRRsQ+iYQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.7.tgz",
"integrity": "sha512-zTkZSA6q+F5sLOdCkiC2+RqJQN0zdsJqvFIOVFL/IDVOnq6PZO5THzwRRLvOSnJJl3edRQCl/hUgS0L5sTInGQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2681,9 +2681,9 @@
}
},
"node_modules/@tiptap/extension-heading": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.11.5.tgz",
"integrity": "sha512-x/MV53psJ9baRcZ4k4WjnCUBMt8zCX7mPlKVT+9C/o+DEs/j/qxPLs95nHeQv70chZpSwCQCt93xMmuF0kPoAg==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.11.7.tgz",
"integrity": "sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2694,9 +2694,9 @@
}
},
"node_modules/@tiptap/extension-history": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.5.tgz",
"integrity": "sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.7.tgz",
"integrity": "sha512-Cu5x3aS13I040QSRoLdd+w09G4OCVfU+azpUqxufZxeNs9BIJC+0jowPLeOxKDh6D5GGT2A8sQtxc6a/ssbs8g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2708,9 +2708,9 @@
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.5.tgz",
"integrity": "sha512-3up2r1Du8/5/4ZYzTC0DjTwhgPI3dn8jhOCLu73m5F3OGvK/9whcXoeWoX103hYMnGDxBlfOje71yQuN35FL4A==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.7.tgz",
"integrity": "sha512-uVmQwD2dzZ5xwmvUlciy0ItxOdOfQjH6VLmu80zyJf8Yu7mvwP8JyxoXUX0vd1xHpwAhgQ9/ozjIWYGIw79DPQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2722,9 +2722,9 @@
}
},
"node_modules/@tiptap/extension-image": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.11.5.tgz",
"integrity": "sha512-HbUq9AL8gb8eSuQfY/QKkvMc66ZFN/b6jvQAILGArNOgalUfGizoC6baKTJShaExMSPjBZlaAHtJiQKPaGRHaA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.11.7.tgz",
"integrity": "sha512-YvCmTDB7Oo+A56tR4S/gcNaYpqU4DDlSQcRp5IQvmQV5EekSe0lnEazGDoqOCwsit9qQhj4MPQJhKrnaWrJUrg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2735,9 +2735,9 @@
}
},
"node_modules/@tiptap/extension-italic": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.11.5.tgz",
"integrity": "sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.11.7.tgz",
"integrity": "sha512-r985bkQfG0HMpmCU0X0p/Xe7U1qgRm2mxvcp6iPCuts2FqxaCoyfNZ8YnMsgVK1mRhM7+CQ5SEg2NOmQNtHvPw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2748,9 +2748,9 @@
}
},
"node_modules/@tiptap/extension-link": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.11.5.tgz",
"integrity": "sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.11.7.tgz",
"integrity": "sha512-qKIowE73aAUrnQCIifYP34xXOHOsZw46cT/LBDlb0T60knVfQoKVE4ku08fJzAV+s6zqgsaaZ4HVOXkQYLoW7g==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.2.0"
@@ -2765,9 +2765,9 @@
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.5.tgz",
"integrity": "sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.7.tgz",
"integrity": "sha512-6ikh7Y+qAbkSuIHXPIINqfzmWs5uIGrylihdZ9adaIyvrN1KSnWIqrZIk/NcZTg5YFIJlXrnGSRSjb/QM3WUhw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2778,9 +2778,9 @@
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.5.tgz",
"integrity": "sha512-Cu8KwruBNWAaEfshRQR0yOSaUKAeEwxW7UgbvF9cN/zZuKgK5uZosPCPTehIFCcRe+TBpRtZQh+06f/gNYpYYg==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.7.tgz",
"integrity": "sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2791,9 +2791,9 @@
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.5.tgz",
"integrity": "sha512-YFBWeg7xu/sBnsDIF/+nh9Arf7R0h07VZMd0id5Ydd2Qe3c1uIZwXxeINVtH0SZozuPIQFAT8ICe9M0RxmE+TA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.7.tgz",
"integrity": "sha512-Pl3B4q6DJqTvvAdraqZaNP9Hh0UWEHL5nNdxhaRNuhKaUo7lq8wbDSIxIW3lvV0lyCs0NfyunkUvSm1CXb6d4Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2804,9 +2804,9 @@
}
},
"node_modules/@tiptap/extension-placeholder": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.11.5.tgz",
"integrity": "sha512-Pr+0Ju/l2ZvXMd9VQxtaoSZbs0BBp1jbBDqwms88ctpyvQFRfLSfSkqudQcSHyw2ROOz2E31p/7I7fpI8Y0CLA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.11.7.tgz",
"integrity": "sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2818,9 +2818,9 @@
}
},
"node_modules/@tiptap/extension-strike": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.11.5.tgz",
"integrity": "sha512-PVfUiCqrjvsLpbIoVlegSY8RlkR64F1Rr2RYmiybQfGbg+AkSZXDeO0eIrc03//4gua7D9DfIozHmAKv1KN3ow==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.11.7.tgz",
"integrity": "sha512-D6GYiW9F24bvAY7XMOARNZbC8YGPzdzWdXd8VOOJABhf4ynMi/oW4NNiko+kZ67jn3EGaKoz32VMJzNQgYi1HA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2831,9 +2831,9 @@
}
},
"node_modules/@tiptap/extension-subscript": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.11.5.tgz",
"integrity": "sha512-VpaSzxku/Bcvf4SgDB2K5d0E+FNA/56iJHMygg/WXsq2F4tMMUEivQHI/n+17ndUEO4Wybz0wItnM1G2JfRuLQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.11.7.tgz",
"integrity": "sha512-I25ZexCddFJ9701DCCtQbX3Vtxzj5d9ss2GAXVweIUCdATCScaebsznyUQoN5papmhTxXsw5OD+K2ZHxP82pew==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2844,9 +2844,9 @@
}
},
"node_modules/@tiptap/extension-superscript": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.11.5.tgz",
"integrity": "sha512-sK6v2G0zFfGW+j9CmYp2e+tyZ3FTa3dP0xY4kJzefgZcHhMJLlLnjxBRwHCSi/jj5ie6WdZT4KoEooxnPs1Vzw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.11.7.tgz",
"integrity": "sha512-dNRpCcRJs0Qvv0sZRgbH7Y5hDVbWsGSZjtwFCs/mysPrvHqmXjzo7568kYWTggxEYxnXw6n0FfkCAEHlt0N90Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2857,9 +2857,9 @@
}
},
"node_modules/@tiptap/extension-table": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.11.5.tgz",
"integrity": "sha512-NKXLhKWdAdURklm98YkCd2ai4fh8jY8HS/+X2s/2QiQt8Z98CU1keCm35fJEEExM234iB/hCqG5vY4JgTc0Tvw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.11.7.tgz",
"integrity": "sha512-rfwWkNXz/EZuhc8lylsCWPbx0Xr5FlIhreWFyeoXYrDEO3x4ytYcVOpNmbabJYP2semfM0PvPR5o84zfFkLZyg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2871,9 +2871,9 @@
}
},
"node_modules/@tiptap/extension-table-cell": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.11.5.tgz",
"integrity": "sha512-S967Au0pgeULstP3FaasOf/LEh72p61Ooh1PcUMF/az4x8EeGgpcEUARpVUxsGxLFvogv6LmhPHZdtcGgdHcBw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.11.7.tgz",
"integrity": "sha512-JMOkSYRckc5SJP86yGGiHzCxCR8ecrRENvTWAKib6qer2tutxs5u42W+Z8uTcHC2dRz7Fv54snOkDoqPwkf6cw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2884,9 +2884,9 @@
}
},
"node_modules/@tiptap/extension-table-header": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.11.5.tgz",
"integrity": "sha512-O1iBtzZP1XZDi4h1Xmgq1T63il+fpKPvBIMZ0JJH9TyCw5i5rcrMLL2dyy5zaWK3BFRJuYBNSke4c+VWnr/g6w==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.11.7.tgz",
"integrity": "sha512-wPRKpliS5QQXgsp//ZjXrHMdLICMkjg2fUrQinOiBa7wDL5C7Y+SehtuK4s2tjeAkyAdj+nepfftyBRIlUSMXg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2897,9 +2897,9 @@
}
},
"node_modules/@tiptap/extension-table-row": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.11.5.tgz",
"integrity": "sha512-+/VWhCuW24BcM5aaIc/f0bC6ZR1Q5gnuqw13MIo7gyPx7iIY6BXK8roGiZSs8wYAN4uBEf3EKFm0bSZwQuAeyg==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.11.7.tgz",
"integrity": "sha512-K254RiXWGXGjz5Cm835hqfQiwnYXm8aw6oOa3isDh4A1B+1Ev4DB2vEDKMrgaOor3nbTsSYmAx2iEMrZSbpaRg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2910,9 +2910,9 @@
}
},
"node_modules/@tiptap/extension-text": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.5.tgz",
"integrity": "sha512-Gq1WwyhFpCbEDrLPIHt5A8aLSlf8bfz4jm417c8F/JyU0J5dtYdmx0RAxjnLw1i7ZHE7LRyqqAoS0sl7JHDNSQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.7.tgz",
"integrity": "sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2923,9 +2923,9 @@
}
},
"node_modules/@tiptap/extension-text-align": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.11.5.tgz",
"integrity": "sha512-Ei0zDpH5N9EV59ogydK4HTKa4lCPicCsQllM5n/Nf2tUJPir3aiYxzJ73FzhComD4Hpo1ANYnmssBhy8QeoPZA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.11.7.tgz",
"integrity": "sha512-3M8zd9ROADXazVNpgR6Ejs1evSvBveN36qN4GgV71GqrNlTcjqYgQcXFLQrsd2hnE+aXir8/8bLJ+aaJXDninA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2936,9 +2936,9 @@
}
},
"node_modules/@tiptap/extension-text-style": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.11.5.tgz",
"integrity": "sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.11.7.tgz",
"integrity": "sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2949,9 +2949,9 @@
}
},
"node_modules/@tiptap/extension-underline": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.11.5.tgz",
"integrity": "sha512-YpWHXNIkSoRSuzT2cvgKpyJ2tTz3LzqkTM64uC+uTJ8cUkvXIWUWejJR42q8ma/mTlQe4lHff4IQ0Sf58Digtw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.11.7.tgz",
"integrity": "sha512-NtoQw6PGijOAtXC6G+0Aq0/Z5wwEjPhNHs8nsjXogfWIgaj/aI4/zfBnA06eI3WT+emMYQTl0fTc4CUPnLVU8g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2962,9 +2962,9 @@
}
},
"node_modules/@tiptap/pm": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.5.tgz",
"integrity": "sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz",
"integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.2.1",
@@ -2981,7 +2981,7 @@
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.37.0"
@@ -2992,32 +2992,32 @@
}
},
"node_modules/@tiptap/starter-kit": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.11.5.tgz",
"integrity": "sha512-SLI7Aj2ruU1t//6Mk8f+fqW+18uTqpdfLUJYgwu0CkqBckrkRZYZh6GVLk/02k3H2ki7QkFxiFbZrdbZdng0JA==",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.11.7.tgz",
"integrity": "sha512-K+q51KwNU/l0kqRuV5e1824yOLVftj6kGplGQLvJG56P7Rb2dPbM/JeaDbxQhnHT/KDGamG0s0Po0M3pPY163A==",
"license": "MIT",
"dependencies": {
"@tiptap/core": "^2.11.5",
"@tiptap/extension-blockquote": "^2.11.5",
"@tiptap/extension-bold": "^2.11.5",
"@tiptap/extension-bullet-list": "^2.11.5",
"@tiptap/extension-code": "^2.11.5",
"@tiptap/extension-code-block": "^2.11.5",
"@tiptap/extension-document": "^2.11.5",
"@tiptap/extension-dropcursor": "^2.11.5",
"@tiptap/extension-gapcursor": "^2.11.5",
"@tiptap/extension-hard-break": "^2.11.5",
"@tiptap/extension-heading": "^2.11.5",
"@tiptap/extension-history": "^2.11.5",
"@tiptap/extension-horizontal-rule": "^2.11.5",
"@tiptap/extension-italic": "^2.11.5",
"@tiptap/extension-list-item": "^2.11.5",
"@tiptap/extension-ordered-list": "^2.11.5",
"@tiptap/extension-paragraph": "^2.11.5",
"@tiptap/extension-strike": "^2.11.5",
"@tiptap/extension-text": "^2.11.5",
"@tiptap/extension-text-style": "^2.11.5",
"@tiptap/pm": "^2.11.5"
"@tiptap/core": "^2.11.7",
"@tiptap/extension-blockquote": "^2.11.7",
"@tiptap/extension-bold": "^2.11.7",
"@tiptap/extension-bullet-list": "^2.11.7",
"@tiptap/extension-code": "^2.11.7",
"@tiptap/extension-code-block": "^2.11.7",
"@tiptap/extension-document": "^2.11.7",
"@tiptap/extension-dropcursor": "^2.11.7",
"@tiptap/extension-gapcursor": "^2.11.7",
"@tiptap/extension-hard-break": "^2.11.7",
"@tiptap/extension-heading": "^2.11.7",
"@tiptap/extension-history": "^2.11.7",
"@tiptap/extension-horizontal-rule": "^2.11.7",
"@tiptap/extension-italic": "^2.11.7",
"@tiptap/extension-list-item": "^2.11.7",
"@tiptap/extension-ordered-list": "^2.11.7",
"@tiptap/extension-paragraph": "^2.11.7",
"@tiptap/extension-strike": "^2.11.7",
"@tiptap/extension-text": "^2.11.7",
"@tiptap/extension-text-style": "^2.11.7",
"@tiptap/pm": "^2.11.7"
},
"funding": {
"type": "github",
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "15.4.0-rc2",
"version": "15.4.0-rc3",
"type": "module",
"exports": {
".": null,
@@ -202,21 +202,21 @@
"npm": ">=10.9"
},
"dependencies": {
"@tiptap/core": "2.11.5",
"@tiptap/extension-character-count": "^2.11.5",
"@tiptap/extension-image": "2.11.5",
"@tiptap/extension-link": "2.11.5",
"@tiptap/extension-placeholder": "2.11.5",
"@tiptap/extension-subscript": "2.11.5",
"@tiptap/extension-superscript": "2.11.5",
"@tiptap/extension-table": "2.11.5",
"@tiptap/extension-table-cell": "2.11.5",
"@tiptap/extension-table-header": "2.11.5",
"@tiptap/extension-table-row": "2.11.5",
"@tiptap/extension-text-align": "2.11.5",
"@tiptap/extension-underline": "2.11.5",
"@tiptap/pm": "2.11.5",
"@tiptap/starter-kit": "2.11.5",
"@tiptap/core": "2.11.7",
"@tiptap/extension-character-count": "2.11.7",
"@tiptap/extension-image": "2.11.7",
"@tiptap/extension-link": "2.11.7",
"@tiptap/extension-placeholder": "2.11.7",
"@tiptap/extension-subscript": "2.11.7",
"@tiptap/extension-superscript": "2.11.7",
"@tiptap/extension-table": "2.11.7",
"@tiptap/extension-table-cell": "2.11.7",
"@tiptap/extension-table-header": "2.11.7",
"@tiptap/extension-table-row": "2.11.7",
"@tiptap/extension-text-align": "2.11.7",
"@tiptap/extension-underline": "2.11.7",
"@tiptap/pm": "2.11.7",
"@tiptap/starter-kit": "2.11.7",
"@types/diff": "^7.0.1",
"@umbraco-ui/uui": "^1.13.0",
"@umbraco-ui/uui-css": "^1.13.0",
@@ -1,6 +1,15 @@
import type { UmbTiptapExtensionApi } from '../../extensions/types.js';
import type { UmbTiptapStatusbarValue, UmbTiptapToolbarValue } from '../types.js';
import { css, customElement, html, map, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit';
import {
css,
customElement,
html,
property,
repeat,
state,
unsafeCSS,
when,
} from '@umbraco-cms/backoffice/external/lit';
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { Editor } from '@umbraco-cms/backoffice/external/tiptap';
@@ -16,6 +25,12 @@ import './tiptap-statusbar.element.js';
const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';
/**
* The root path for the stylesheets on the server.
* This is used to load the stylesheets from the server as a workaround until the server supports virtual paths.
*/
const STYLESHEET_ROOT_PATH = '/css';
@customElement('umb-input-tiptap')
export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof UmbLitElement, string>(UmbLitElement) {
#stylesheets = new Set(['/umbraco/backoffice/css/rte-content.css']);
@@ -129,7 +144,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
const stylesheets = this.configuration?.getValueByAlias<Array<string>>('stylesheets');
if (stylesheets?.length) {
stylesheets.forEach((x) => this.#stylesheets.add(x));
stylesheets.forEach((stylesheet) => {
const linkHref =
stylesheet.startsWith('http') || stylesheet.startsWith(STYLESHEET_ROOT_PATH)
? stylesheet
: `${STYLESHEET_ROOT_PATH}${stylesheet}`;
this.#stylesheets.add(linkHref);
});
}
this._toolbar = this.configuration?.getValueByAlias<UmbTiptapToolbarValue>('toolbar') ?? [[[]]];
@@ -182,7 +203,11 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
#renderStyles() {
if (!this._styles?.length) return;
return html`
${map(this.#stylesheets, (stylesheet) => html`<link rel="stylesheet" href=${stylesheet} />`)}
${repeat(
this.#stylesheets,
(stylesheet) => stylesheet,
(stylesheet) => html`<link rel="stylesheet" href="${stylesheet}" />`,
)}
<style>
${this._styles.map((style) => unsafeCSS(style))}
</style>
@@ -189,25 +189,48 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
private void ExecuteBuilderAttributes(IUmbracoBuilder builder)
{
// todo better errors
Type? testClassType = GetTestClassType()
?? throw new Exception($"Could not find test class for {TestContext.CurrentContext.Test.FullName} in order to execute builder attributes.");
// execute builder attributes defined on method
foreach (ConfigureBuilderAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName)
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
.GetCustomAttributes(typeof(ConfigureBuilderAttribute), true))
// Execute builder attributes defined on method.
foreach (ConfigureBuilderAttribute builderAttribute in GetConfigureBuilderAttributes<ConfigureBuilderAttribute>(testClassType))
{
builderAttribute.Execute(builder);
}
// execute builder attributes defined on method with param value passtrough from testcase
foreach (ConfigureBuilderTestCaseAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName)
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
.GetCustomAttributes(typeof(ConfigureBuilderTestCaseAttribute), true))
// Execute builder attributes defined on method with param value pass through from test case.
foreach (ConfigureBuilderTestCaseAttribute builderAttribute in GetConfigureBuilderAttributes<ConfigureBuilderTestCaseAttribute>(testClassType))
{
builderAttribute.Execute(builder);
}
}
private static Type? GetTestClassType()
{
string testClassName = TestContext.CurrentContext.Test.ClassName;
// Try resolving the type name directly (which will work for tests in this assembly).
Type testClass = Type.GetType(testClassName);
if (testClass is not null)
{
return testClass;
}
// Try scanning the loaded assemblies to see if we can find the class by full name. This will be necessary
// for integration test projects using the base classess provided by Umbraco.
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
return assemblies
.SelectMany(a => a.GetTypes().Where(t => t.FullName == testClassName))
.FirstOrDefault();
}
private static IEnumerable<TAttribute> GetConfigureBuilderAttributes<TAttribute>(Type testClassType)
where TAttribute : Attribute =>
testClassType
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
.GetCustomAttributes(typeof(TAttribute), true)
.Cast<TAttribute>();
/// <summary>
/// Hook for altering UmbracoBuilder setup
/// </summary>
@@ -0,0 +1,51 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Infrastructure.HybridCache;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache;
// We need to make sure that it's the distributed cache refreshers that refresh the elements cache
// see: https://github.com/umbraco/Umbraco-CMS/issues/18467
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
internal sealed class DistributedCacheRefresherTests : UmbracoIntegrationTest
{
private IElementsCache ElementsCache => GetRequiredService<IElementsCache>();
private ContentCacheRefresher ContentCacheRefresher => GetRequiredService<ContentCacheRefresher>();
private MediaCacheRefresher MediaCacheRefresher => GetRequiredService<MediaCacheRefresher>();
[Test]
public void DistributedContentCacheRefresherClearsElementsCache()
{
var cacheKey = "test";
PopulateCache("test");
ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload()]);
Assert.IsNull(ElementsCache.Get(cacheKey));
}
[Test]
public void DistributedMediaCacheRefresherClearsElementsCache()
{
var cacheKey = "test";
PopulateCache("test");
MediaCacheRefresher.Refresh([new MediaCacheRefresher.JsonPayload(1, Guid.NewGuid(), TreeChangeTypes.RefreshAll)]);
Assert.IsNull(ElementsCache.Get(cacheKey));
}
private void PopulateCache(string key)
{
ElementsCache.Get(key, () => new object());
// Just making sure something is in the cache now.
Assert.IsNotNull(ElementsCache.Get(key));
}
}
@@ -555,8 +555,7 @@ internal partial class BlockListElementLevelVariationTests
content = ContentService.GetById(content.Key);
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
Assert.NotNull(savedBlocksValue);
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
blockListValue = savedBlocksValue is null ? null : JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
// limited user access means invariant data is inaccessible since AllowEditInvariantFromNonDefault is disabled
if (updateWithLimitedUserAccess)
@@ -973,7 +972,7 @@ internal partial class BlockListElementLevelVariationTests
}
else
{
Assert.AreEqual("null", savedBlocksValue);
Assert.IsNull(savedBlocksValue);
}
}
@@ -0,0 +1,76 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration;
[TestFixture]
public class ContentSettingsExtensionsTests
{
[TestCase("jpg")]
[TestCase("JPG")]
[TestCase("jpg ")]
public void IsFileAllowedForUpload_Allows_File_In_Allow_List(string extension)
{
var contentSettings = new ContentSettings
{
AllowedUploadedFileExtensions = ["jpg", "png"],
};
Assert.IsTrue(contentSettings.IsFileAllowedForUpload(extension));
}
[TestCase("gif")]
[TestCase("GIF")]
[TestCase("gif ")]
public void IsFileAllowedForUpload_Rejects_File_Not_In_Allow_List(string extension)
{
var contentSettings = new ContentSettings
{
AllowedUploadedFileExtensions = ["jpg", "png"],
};
Assert.IsFalse(contentSettings.IsFileAllowedForUpload(extension));
}
[TestCase("jpg")]
[TestCase("JPG")]
[TestCase("jpg ")]
public void IsFileAllowedForUpload_Allows_File_Not_In_Disallow_List(string extension)
{
var contentSettings = new ContentSettings
{
DisallowedUploadedFileExtensions = ["gif", "png"],
};
Assert.IsTrue(contentSettings.IsFileAllowedForUpload(extension));
}
[TestCase("gif")]
[TestCase("GIF")]
[TestCase("gif ")]
public void IsFileAllowedForUpload_Rejects_File_In_Disallow_List(string extension)
{
var contentSettings = new ContentSettings
{
DisallowedUploadedFileExtensions = ["gif", "png"],
};
Assert.IsFalse(contentSettings.IsFileAllowedForUpload(extension));
}
[Test]
public void IsFileAllowedForUpload_Allows_File_In_Allow_List_Even_If_Also_In_Disallow_List()
{
var contentSettings = new ContentSettings
{
AllowedUploadedFileExtensions = ["jpg", "png"],
DisallowedUploadedFileExtensions = ["jpg"],
};
Assert.IsTrue(contentSettings.IsFileAllowedForUpload("jpg"));
}
}
@@ -0,0 +1,127 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
[TestFixture]
public class DocumentUrlServiceTests
{
[Test]
public void ConvertToCacheModel_Converts_Single_Document_With_Single_Segment_To_Expected_Cache_Model()
{
var segments = new List<PublishedDocumentUrlSegment>
{
new()
{
DocumentKey = Guid.NewGuid(),
IsDraft = false,
IsPrimary = true,
LanguageId = 1,
UrlSegment = "test-segment",
},
};
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
Assert.AreEqual(1, cacheModels.Count);
Assert.AreEqual(segments[0].DocumentKey, cacheModels[0].DocumentKey);
Assert.AreEqual(1, cacheModels[0].LanguageId);
Assert.AreEqual(1, cacheModels[0].UrlSegments.Count);
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
}
[Test]
public void ConvertToCacheModel_Converts_Multiple_Documents_With_Single_Segment_To_Expected_Cache_Model()
{
var segments = new List<PublishedDocumentUrlSegment>
{
new()
{
DocumentKey = Guid.NewGuid(),
IsDraft = false,
IsPrimary = true,
LanguageId = 1,
UrlSegment = "test-segment",
},
new()
{
DocumentKey = Guid.NewGuid(),
IsDraft = false,
IsPrimary = true,
LanguageId = 1,
UrlSegment = "test-segment-2",
},
};
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
Assert.AreEqual(2, cacheModels.Count);
Assert.AreEqual(segments[0].DocumentKey, cacheModels[0].DocumentKey);
Assert.AreEqual(segments[1].DocumentKey, cacheModels[1].DocumentKey);
Assert.AreEqual(1, cacheModels[0].LanguageId);
Assert.AreEqual(1, cacheModels[1].LanguageId);
Assert.AreEqual(1, cacheModels[0].UrlSegments.Count);
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
Assert.AreEqual(1, cacheModels[1].UrlSegments.Count);
Assert.AreEqual("test-segment-2", cacheModels[1].UrlSegments[0].Segment);
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
Assert.IsTrue(cacheModels[1].UrlSegments[0].IsPrimary);
}
[Test]
public void ConvertToCacheModel_Converts_Single_Document_With_Multiple_Segments_To_Expected_Cache_Model()
{
var documentKey = Guid.NewGuid();
var segments = new List<PublishedDocumentUrlSegment>
{
new()
{
DocumentKey = documentKey,
IsDraft = false,
IsPrimary = true,
LanguageId = 1,
UrlSegment = "test-segment",
},
new()
{
DocumentKey = documentKey,
IsDraft = false,
IsPrimary = false,
LanguageId = 1,
UrlSegment = "test-segment-2",
},
};
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
Assert.AreEqual(1, cacheModels.Count);
Assert.AreEqual(documentKey, cacheModels[0].DocumentKey);
Assert.AreEqual(1, cacheModels[0].LanguageId);
Assert.AreEqual(2, cacheModels[0].UrlSegments.Count);
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
Assert.AreEqual("test-segment-2", cacheModels[0].UrlSegments[1].Segment);
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
Assert.IsFalse(cacheModels[0].UrlSegments[1].IsPrimary);
}
[Test]
public void ConvertToCacheModel_Performance_Test()
{
const int NumberOfSegments = 1;
var segments = Enumerable.Range(0, NumberOfSegments)
.Select((x, i) => new PublishedDocumentUrlSegment
{
DocumentKey = Guid.NewGuid(),
IsDraft = false,
IsPrimary = true,
LanguageId = 1,
UrlSegment = $"test-segment-{x + 1}",
});
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
Assert.AreEqual(NumberOfSegments, cacheModels.Count);
// Benchmarking (for NumberOfSegments = 50000):
// - Initial implementation (15.4): ~28s
// - Current implementation: ~100ms
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "15.4.0-rc2",
"version": "15.4.3",
"assemblyVersion": {
"precision": "build"
},