Compare commits

...
Author SHA1 Message Date
Andy Butland 0a98a47db0 Update Examine to 3.8.0. 2026-06-07 21:17:09 +02:00
Andy Butland 18a9da080a Bumped version to 13.15.0-rc 2026-04-30 08:07:45 +02:00
Andy Butland 6104efdd47 Merge branch 'release/13.14.0' into v13/dev 2026-04-30 08:06:57 +02:00
Andy Butland 7d400e7742 Bumped version to 13.14.0 2026-04-30 06:36:08 +02:00
Andy Butland 8295f833f8 Bump version to 13.14.0-rc3. 2026-04-23 16:40:12 +02:00
8d6645b0ef Surface controllers: validate redirect url in public surface controllers (v13) (#22565)
* Fix: prevent open redirect in public surface controllers by validating RedirectUrl with Url.IsLocalUrl

* Fixed spacing in comment.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-23 16:39:31 +02:00
a0cb9b2826 Surface controllers: validate redirect url in public surface controllers (v13) (#22565)
* Fix: prevent open redirect in public surface controllers by validating RedirectUrl with Url.IsLocalUrl

* Fixed spacing in comment.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-23 16:37:53 +02:00
Andy Butland b2ff910a92 Bumped version to 13.14.0-rc2. 2026-04-17 14:03:37 +02:00
GinoandAndy Butland b70d2b482d Dependencies: Pin System.Security.Cryptography.Xml to resolve vulnerability warning (Umbraco 13) (#22513)
* Update System.Security.Cryptography.Xml - fix vulnerability

* Update Directory.Packages.props

use correct/latest version of the system.security.cryptohraphy.xml package.

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-17 14:02:46 +02:00
ac22eef92f Dependencies: Pin System.Security.Cryptography.Xml to resolve vulnerability warning (Umbraco 13) (#22513)
* Update System.Security.Cryptography.Xml - fix vulnerability

* Update Directory.Packages.props

use correct/latest version of the system.security.cryptohraphy.xml package.

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-17 11:59:55 +00:00
24ef23e133 Backport #21910 to V13 (#22314)
* Backport #21910 to V13

* Update src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Backport #21856 as well

* Fix issue where removing a property type wasn't detecting and returning the expected change type.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-07 09:07:07 +02:00
Andy Butland c8fb306e67 Bumped version to 13.14.0-rc. 2026-03-12 06:46:44 +01:00
Andy Butland 667c2f5a47 Set version to 13.13.1. 2026-03-06 11:58:38 +01:00
Andy ButlandandGitHub 8230efd722 Media Picker: Fix media picker folder navigation with "ignore user start nodes" enabled (closes #21840 for 13) (#21899)
Fix media picker folder navigation with "ignore user start nodes" enabled.
2026-03-06 11:02:35 +01:00
Ronald BarendseandGitHub f420b0b894 Remove legacy Windows path length checks and related tests (#21884)
Removed explicit 260-character path length checks from PhysicalFileSystem.GetFullPath and deleted associated unit tests. Updated tests to focus on path normalization and validity, and improved path assertions for clarity and cross-platform compatibility. No longer enforce or test for legacy Windows path length restrictions.
2026-02-24 11:18:11 +01:00
Andy Butland b196ee89b7 Merge branch 'release/13.13' into v13/dev
# Conflicts:
#	version.json
2025-12-18 06:39:51 +01:00
Andy Butland b998afcd93 Merge branch 'release/13.13' of https://github.com/umbraco/Umbraco-CMS into release/13.13 2025-12-17 15:59:36 +01:00
Andy Butland 8c0bdb4b0b Bump version to 13.13.0. 2025-12-17 15:59:23 +01:00
66d090b4e3 Media picker: Fix image selection after upload when media picker presents multiple pages (closes #21115) (#21117)
* Fix image selection after upload when media picker presents multiple pages.

* 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-12-17 08:25:03 +01:00
Andy Butland b779123946 Members: Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max (closes #21155) (#21157)
Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max, to align with type of function result and avoid chance of overflow when timezone is minus UTC.
2025-12-16 07:04:56 +01:00
Andy ButlandandGitHub ed36b31576 Members: Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max (closes #21155) (#21157)
Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max, to align with type of function result and avoid chance of overflow when timezone is minus UTC.
2025-12-16 05:44:49 +00:00
Andy Butland 4871c0bab1 Merge branch 'release/13.13' into v13/dev
# Conflicts:
#	version.json
2025-12-11 07:17:23 +01:00
bfe567f075 Updates dependencies to latest patch for 13.13 (#21108)
* Update server-side dependencies to latest patch releases.

* Update client-side dependencies.

* Bumped version to 13.13.0-rc3.

* Reverted change to node version.

* 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-12-10 14:12:44 +00:00
Andy Butland 328c43c225 Merge branch 'release/13.12.1' into release/13.13
# Conflicts:
#	version.json
2025-12-09 06:25:00 +01:00
Andy Butland 56de595457 Merge branch 'release/13.12.1' into v13/dev
# Conflicts:
#	version.json
2025-12-09 06:24:24 +01:00
Andy ButlandandGitHub 7505efd433 Merge commit from fork
* Ensure imported dictionary item is only processed from the expected temporary uploads folder.

* Ensured content type upload input is only a file and not a string that can be interpretted as a file path.

* Amend dictionary import to extract file name and prepend path rather than rely on provided path.

* Tidied usings.

* Ensure file name cannot contain path separator characters.
2025-12-09 05:15:53 +01:00
Andy Butland 934126ae6e Bump version to 13.13.0-rc2. 2025-12-05 15:25:16 +01:00
Bjarne FyrstenborgandAndy Butland 632f04afd2 Rich text Editor: Ensure readonly attribute has been observed (#21004)
* Set toolbar and readonly when readonly attribute has been observed

* Simplify
2025-12-05 15:24:27 +01:00
Bjarne FyrstenborgandGitHub edae01a152 Rich text Editor: Ensure readonly attribute has been observed (#21004)
* Set toolbar and readonly when readonly attribute has been observed

* Simplify
2025-12-05 15:22:12 +01:00
d418f4f4d5 Media Picker: Present root nodes for users with more than one media root node assigned (closes #20967) (#20993)
* Presented root nodes in media picker for users with more than one media root node assigned.

* Apply suggestions from code review

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

* Improved variable names.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 13:11:34 +00:00
Andy ButlandandGitHub da677c42ed Media Picker Fixed folder navigation when returning from an empty folder (closes #20975) (#20987)
* Fixed media picker folder navigation when returning from an empty folder.

* Applied suggestions from code review.
2025-12-01 12:59:12 +00:00
Andy ButlandandGitHub 467db7736c Content picker: Remove multi-node content picker entries by UDI to account for index not aligning with stored values (#20950)
* Remove multi-node content picker entries by udi to account for index not aligning with stored values.

* Handled issues raised in code review.
2025-12-01 12:49:19 +00:00
Bjarne FyrstenborgandGitHub aced6916ff Content editing: Correct handling of read-only of name field when A action is removed in SendingContentNotification (#20072)
Set header name to readonly
2025-12-01 06:36:31 +00:00
Bjarne FyrstenborgandGitHub dc0005f394 Block Grid: Streamline readonly with other property editors (#21007)
* Align cursor style and readonly mode for other property editors

* Show disabled actions as in other property editors when readonly

* Disable sortmode as well when readonly

* Formatting

* Ensure delete action remains disabled if readonly
2025-12-01 05:53:28 +00:00
Andy ButlandandGitHub 0da1146fcd Content picker: Allow selections on a content picker on a read-only document to be opened (closes #20816) (#20819)
Allow selections on a content picker on a read-only document to be opened.
2025-11-24 16:50:28 +00:00
a7b4481c6c Redirects: Fix self referencing redirects for 13 (#20908)
* Ported fix to 13

* Mocking context and cache

* Removing unused parameter in constructor

* Removed a couple of unused variables in the tests.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-11-20 15:29:58 +00:00
Andy Butland 90ec85386b Merge branch 'release/13.12' into release/13.12.1
# Conflicts:
#	version.json
2025-11-20 06:47:36 +01:00
Andy Butland 9baaad40d1 Merge branch 'release/13.12' into v13/dev
# Conflicts:
#	version.json
2025-11-20 06:46:39 +01:00
Andy Butland 7dcc7303c2 Bumped version to 13.12.0. 2025-11-19 18:49:49 +01:00
Andy Butland 7edc6ec11d Bump version to 13.12.1. 2025-11-17 09:30:40 +01:00
Andy ButlandandGitHub 23b09b1a7e Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components (closes #20709 for 13) (#20722)
* Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components.

* Added XML docs.

* Apply fix to DeliveryApiContentIndexingNotificationHandler.

* Dispose disposable data editors in ValueEditorCache.

* Removed unnecessary refactoring and clarified code comments.
2025-11-06 15:02:57 +00:00
Andy ButlandandGitHub 5d6f4f2135 Dependencies: Update dotnet sdk and node development dependency to latest secure version of current major (13) (#20729)
Update dotnet sdk and node development dependency to latest secure version of current major.
2025-11-05 21:24:27 +01:00
Sven GeusensandGitHub 87e20ceec6 Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (closes #20272 for 13) (#20721)
V13 implementation off #20559 (Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled)
2025-11-03 19:10:14 +01:00
ArjanandGitHub 8110b80318 Backoffice Caching: Added cache buster for block list stylesheets (closes #20683) (#20686)
Fixed, added cache buster to Block List Editor stylesheets
2025-10-30 16:23:37 +01:00
Andy Butland 9de905cba3 Bumped version to 13.13.0-rc. 2025-10-29 08:02:54 +01:00
35 changed files with 882 additions and 190 deletions
+16 -16
View File
@@ -12,24 +12,24 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.22" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.22" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
@@ -46,9 +46,9 @@
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="Examine" Version="3.8.0" />
<PackageVersion Include="Examine.Core" Version="3.8.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
@@ -56,7 +56,7 @@
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NPoco" Version="5.7.1" />
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
@@ -74,7 +74,7 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
<PackageVersion Include="Smidge.InMemory" Version="4.6.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.6.0" />
@@ -83,11 +83,11 @@
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
@@ -97,8 +97,8 @@
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
</ItemGroup>
</Project>
@@ -346,13 +346,6 @@ namespace Umbraco.Cms.Core.IO
// our root path, due to relative segments, so better check
if (_ioHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
{
// this says that 4.7.2 supports long paths - but Windows does not
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
if (path.Length > 260)
{
throw new PathTooLongException($"Path {path} is too long.");
}
return path;
}
+30 -5
View File
@@ -132,6 +132,30 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
}
}
/// <summary>
/// Detects if any persisted property types have been removed by comparing old and new collections,
/// and sets <see cref="HasPropertyTypeBeenRemoved"/> accordingly.
/// </summary>
private void DetectPropertyTypeRemovals(IEnumerable<IPropertyType> oldPropertyTypes, IEnumerable<IPropertyType> newPropertyTypes)
{
if (HasPropertyTypeBeenRemoved)
{
return;
}
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Count == 0)
{
return;
}
var newIds = new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Any(id => !newIds.Contains(id)))
{
HasPropertyTypeBeenRemoved = true;
}
}
/// <summary>
/// PropertyTypes that are not part of a PropertyGroup
/// </summary>
@@ -273,6 +297,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
{
if (PropertyTypeCollection != null)
{
DetectPropertyTypeRemovals(PropertyTypeCollection, value);
PropertyTypeCollection.ClearCollectionChangedEvents();
}
@@ -443,21 +468,21 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
/// Please note that resetting the dirty properties could potentially
/// obstruct the saving of a new or updated entity.
/// </remarks>
public override void ResetDirtyProperties()
public override void ResetDirtyProperties(bool rememberDirty)
{
base.ResetDirtyProperties();
base.ResetDirtyProperties(rememberDirty);
// loop through each property group to reset the property types
var propertiesReset = new List<int>();
foreach (PropertyGroup propertyGroup in PropertyGroups)
{
propertyGroup.ResetDirtyProperties();
propertyGroup.ResetDirtyProperties(rememberDirty);
if (propertyGroup.PropertyTypes is not null)
{
foreach (IPropertyType propertyType in propertyGroup.PropertyTypes)
{
propertyType.ResetDirtyProperties();
propertyType.ResetDirtyProperties(rememberDirty);
propertiesReset.Add(propertyType.Id);
}
}
@@ -467,7 +492,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
// but don't re-reset ones we've already done.
foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false))
{
propertyType.ResetDirtyProperties();
propertyType.ResetDirtyProperties(rememberDirty);
}
}
@@ -13,6 +13,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
{
private List<IContentTypeComposition> _contentTypeComposition = new();
private List<int> _removedContentTypeKeyTracker = new();
private bool _hasCompositionBeenRemoved;
protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId)
: base(shortStringHelper, parentId)
@@ -104,6 +105,24 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
}
}
/// <summary>
/// A boolean flag indicating if a composition has been removed from this instance.
/// </summary>
/// <remarks>
/// This is currently (specifically) used in order to know that we need to refresh the content cache which
/// needs to occur when a composition has been removed from a content type
/// </remarks>
[IgnoreDataMember]
internal bool HasCompositionTypeBeenRemoved
{
get => _hasCompositionBeenRemoved;
private set
{
_hasCompositionBeenRemoved = value;
OnPropertyChanged(nameof(HasCompositionTypeBeenRemoved));
}
}
/// <inheritdoc />
public IEnumerable<IPropertyType> GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes();
@@ -179,6 +198,8 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
_removedContentTypeKeyTracker.AddRange(compositionIdsToRemove);
}
HasCompositionTypeBeenRemoved = true;
OnPropertyChanged(nameof(ContentTypeComposition));
return _contentTypeComposition.Remove(contentTypeComposition);
+39
View File
@@ -20,6 +20,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
private string _alias;
private string? _name;
private bool _hasPropertyTypeBeenRemoved;
private PropertyTypeCollection? _propertyTypes;
private int _sortOrder;
@@ -94,6 +95,20 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder));
}
/// <summary>
/// A boolean flag indicating if a property type has been removed from this group.
/// </summary>
[IgnoreDataMember]
internal bool HasPropertyTypeBeenRemoved
{
get => _hasPropertyTypeBeenRemoved;
private set
{
_hasPropertyTypeBeenRemoved = value;
OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved));
}
}
/// <summary>
/// Gets or sets a collection of property types for the group.
/// </summary>
@@ -112,6 +127,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
{
if (_propertyTypes != null)
{
DetectPropertyTypeRemovals(_propertyTypes, value);
_propertyTypes.ClearCollectionChangedEvents();
}
@@ -155,4 +171,27 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
OnPropertyChanged(nameof(PropertyTypes));
private void DetectPropertyTypeRemovals(PropertyTypeCollection oldPropertyTypes, PropertyTypeCollection? newPropertyTypes)
{
if (HasPropertyTypeBeenRemoved)
{
return;
}
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Count == 0)
{
return;
}
var newIds = newPropertyTypes != null
? new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0))
: new HashSet<int>();
if (oldIds.Any(id => !newIds.Contains(id)))
{
HasPropertyTypeBeenRemoved = true;
}
}
}
@@ -18,4 +18,20 @@ public static class ContentTypeChangeExtensions
public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
(change & types) == ContentTypeChangeTypes.None;
/// <summary>
/// Determines whether the change has structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has structural impact; otherwise, <c>false</c>.</returns>
public static bool IsStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshMain);
/// <summary>
/// Determines whether the change has non-structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has non-structural impact; otherwise, <c>false</c>.</returns>
public static bool IsNonStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshOther) && !change.HasType(ContentTypeChangeTypes.RefreshMain);
}
@@ -11,14 +11,29 @@ public enum ContentTypeChangeTypes : byte
Create = 1,
/// <summary>
/// Content type changes impact only the Content type being saved
/// Content type changes directly impact existing content of this content type.
/// </summary>
/// <remarks>
/// These changes are "destructive" of nature. They include:
/// - Changing the content type alias.
/// - Removing a property type or a composition.
/// - Changing the alias of a property type (this effectively corresponds to removing a property type).
/// - Changing variance, either at property or content type level.
/// </remarks>
RefreshMain = 2,
/// <summary>
/// Content type changes impacts the content type being saved and others used that are composed of it
/// Content type changes that do not directly impact existing content of this content type.
/// </summary>
RefreshOther = 4, // changed, other change
/// <remarks>
/// These changes are "constructive" of nature, and include all changes not included in
/// <see cref="RefreshMain"/> - for example:
/// - Adding a property type or a composition.
/// - Rearranging property types or groups.
/// - Changes to name, description, icon etc.
/// - Changes to other content type settings, i.e. allowed child types and version cleanup.
/// </remarks>
RefreshOther = 4,
/// <summary>
/// Content type was removed
@@ -226,7 +226,10 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
});
// removed properties?
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
// check both the content type level flag (set by RemovePropertyType) and
// individual property group flags (set when PropertyTypes collection is replaced, e.g. by the mapper)
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved")
|| contentType.PropertyGroups.Any(g => g.WasPropertyDirty("HasPropertyTypeBeenRemoved"));
// removed compositions?
var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
@@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -146,5 +145,5 @@ internal class RedirectUrlService : RepositoryService, IRedirectUrlService
{
return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture);
}
}
}
}
@@ -95,6 +95,7 @@ namespace Umbraco.Cms.Infrastructure.Routing
using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext();
IPublishedContentCache? contentCache = reference.UmbracoContext.Content;
if (contentCache == null)
{
_logger.LogWarning("Could not track redirects because there is no published content cache available on the current published snapshot.");
@@ -106,11 +107,16 @@ namespace Umbraco.Cms.Infrastructure.Routing
try
{
var newRoute = contentCache.GetRouteById(contentId, culture);
if (!IsValidRoute(newRoute) || oldRoute == newRoute)
{
continue;
}
// Ensure we don't create a self-referencing redirect. This can occur if a document is renamed and then the name is reverted back
// to the original. We resolve this by removing any existing redirect that points to the new route.
RemoveSelfReferencingRedirect(contentKey, newRoute);
_redirectUrlService.Register(oldRoute, contentKey, culture);
}
catch (Exception ex)
@@ -121,5 +127,17 @@ namespace Umbraco.Cms.Infrastructure.Routing
}
private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/");
private void RemoveSelfReferencingRedirect(Guid contentKey, string route)
{
IEnumerable<IRedirectUrl> allRedirectUrls = _redirectUrlService.GetContentRedirectUrls(contentKey);
foreach (IRedirectUrl redirectUrl in allRedirectUrls)
{
if (redirectUrl.Url == route)
{
_redirectUrlService.Delete(redirectUrl.Key);
}
}
}
}
}
@@ -52,16 +52,16 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
throw new NotSupportedException();
}
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)>();
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds)>();
foreach (ContentTypeCacheRefresher.JsonPayload payload in (ContentTypeCacheRefresher.JsonPayload[])args
.MessageObject)
{
if (!changedIds.TryGetValue(
payload.ItemType,
out (List<int> removedIds, List<int> refreshedIds, List<int> otherIds) idLists))
out (List<int> removedIds, List<int> refreshedIds) idLists))
{
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>(), otherIds: new List<int>());
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>());
changedIds.Add(payload.ItemType, idLists);
}
@@ -73,28 +73,24 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
{
idLists.refreshedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
{
idLists.otherIds.Add(payload.Id);
}
}
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)> ci in
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds)> ci in
changedIds)
{
if (ci.Value.refreshedIds.Count > 0 || ci.Value.otherIds.Count > 0)
if (ci.Value.refreshedIds.Count > 0)
{
switch (ci.Key)
{
case var itemType when itemType == typeof(IContentType).Name:
RefreshContentOfContentTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshContentOfContentTypes(ci.Value.refreshedIds.Distinct()
.ToArray());
break;
case var itemType when itemType == typeof(IMediaType).Name:
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct().ToArray());
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Distinct().ToArray());
break;
case var itemType when itemType == typeof(IMemberType).Name:
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Distinct()
.ToArray());
break;
}
@@ -154,6 +150,10 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
const int pageSize = 500;
var page = 0;
var total = long.MaxValue;
// track which Ids have their paths are published
var publishChecked = new Dictionary<int, bool>();
while (page * pageSize < total)
{
IEnumerable<IContent> contentToRefresh = _contentService.GetPagedOfTypes(
@@ -165,20 +165,20 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
// order by shallowest to deepest, this allows us to check it's published state without checking every item
Ordering.By("Path"));
// track which Ids have their paths are published
var publishChecked = new Dictionary<int, bool>();
foreach (IContent c in contentToRefresh)
{
var isPublished = false;
if (c.Published)
{
if (!publishChecked.TryGetValue(c.ParentId, out isPublished))
if (publishChecked.TryGetValue(c.ParentId, out isPublished) is false)
{
// nothing by parent id, so query the service and cache the result for the next child to check against
isPublished = _contentService.IsPathPublished(c);
publishChecked[c.Id] = isPublished;
// the parent *must* be published if the entire path is published
publishChecked[c.ParentId] = isPublished;
}
publishChecked[c.Id] = isPublished;
}
_umbracoIndexingHandler.ReIndexForContent(c, isPublished);
@@ -167,7 +167,7 @@ public class IdentityMapDefinition : IMapDefinition
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
if (lockedOutUntil.HasValue is false)
{
return DateTime.MaxValue;
return DateTimeOffset.MaxValue;
}
return EnsureUtcWithServerTime(lockedOutUntil.Value);
@@ -53,9 +53,7 @@ public class PublishedSnapshotServiceEventHandler :
public void Handle(ContentTypeRefreshedNotification notification)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id)
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.IsStructuralChange()).Select(x => x.Item.Id)
.ToArray();
if (contentTypeIds.Any())
{
@@ -674,8 +674,21 @@ public class ContentTypeController : ContentTypeControllerBase<IContentType>
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult Import(string file)
{
if (string.IsNullOrWhiteSpace(file))
{
return NotFound();
}
// The incoming 'file' parameter we expect to contain the just a file name and extension.
// We accept only this and no input parameters containing paths, to prevent any path based security exploits.
var invalidFileNameChars = Path.GetInvalidFileNameChars();
if (file.IndexOfAny(invalidFileNameChars) >= 0 || file.Contains(Path.DirectorySeparatorChar) || file.Contains(Path.AltDirectorySeparatorChar))
{
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads), file);
if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(filePath))
if (System.IO.File.Exists(filePath) is false)
{
return NotFound();
}
@@ -1,27 +1,27 @@
using System.Xml;
using System.Globalization;
using System.Net.Mime;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Packaging;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.Packaging;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -460,7 +460,17 @@ public class DictionaryController : BackOfficeNotificationsController
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
// The incoming 'file' parameter we expect to contain the full path to the uploaded file.
// We accept only files coming from the uploads folder to prevent any path based security exploits.
var fileName = Path.GetFileName(file);
var invalidFileNameChars = Path.GetInvalidFileNameChars();
if (fileName.IndexOfAny(invalidFileNameChars) >= 0 || fileName.Contains(Path.DirectorySeparatorChar) || fileName.Contains(Path.AltDirectorySeparatorChar))
{
return NotFound();
}
var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
var filePath = Path.Combine(root, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
@@ -847,6 +847,21 @@ public class EntityController : UmbracoAuthorizedJsonController
return NotFound();
}
EntityBasic? MapEntityBasic(IEntitySlim source, string? culture)
{
EntityBasic? target = _umbracoMapper.Map<IEntitySlim, EntityBasic>(source, context =>
{
context.SetCulture(culture);
});
if (target is not null)
{
target.AdditionalData["hasChildren"] = source.HasChildren;
}
return target;
}
UmbracoObjectTypes? objectType = ConvertToObjectType(type);
if (objectType.HasValue)
{
@@ -857,12 +872,25 @@ public class EntityController : UmbracoAuthorizedJsonController
var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey);
var culture = ClientCulture();
// root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes
if (id == Constants.System.Root && startNodeIds.Length > 0 &&
startNodeIds.Contains(Constants.System.Root) == false && !ignoreUserStartNodes)
startNodeIds.Contains(Constants.System.Root) == false &&
ignoreUserStartNodes == false)
{
return new PagedResult<EntityBasic>(0, 0, 0);
var startNodeEntities = _entityService.GetAll(objectType.Value, startNodeIds).ToList();
IEnumerable<IEntitySlim> pagedStartNodeEntities = startNodeEntities
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return new PagedResult<EntityBasic>(startNodeEntities.Count, pageNumber, pageSize)
{
Items = pagedStartNodeEntities
.Select(source => MapEntityBasic(source, culture))
.WhereNotNull(),
};
}
//adding multiple conditions ,considering id,key & name as filter param
//for id as int
int.TryParse(filter, out int filterAsIntId);
@@ -882,13 +910,11 @@ public class EntityController : UmbracoAuthorizedJsonController
|| x.Key == filterAsGuid),
Ordering.By(orderBy, orderDirection));
if (totalRecords == 0)
{
return new PagedResult<EntityBasic>(0, 0, 0);
}
var culture = ClientCulture();
var pagedResult = new PagedResult<EntityBasic>(totalRecords, pageNumber, pageSize)
{
Items = entities
@@ -898,22 +924,8 @@ public class EntityController : UmbracoAuthorizedJsonController
(objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) is false ||
(ContentPermissions.IsInBranchOfStartNode(entity.Path, startNodeIds, startNodePaths, out var hasPathAccess) &&
hasPathAccess))
.Select(source =>
{
EntityBasic? target = _umbracoMapper.Map<IEntitySlim, EntityBasic>(source, context =>
{
context.SetCulture(culture);
context.SetCulture(culture);
});
if (target is not null)
{
//TODO: Why is this here and not in the mapping?
target.AdditionalData["hasChildren"] = source.HasChildren;
}
return target;
}).WhereNotNull()
.Select(source => MapEntityBasic(source, culture))
.WhereNotNull(),
};
return pagedResult;
@@ -1126,7 +1138,7 @@ public class EntityController : UmbracoAuthorizedJsonController
.Select(s => int.Parse(s, CultureInfo.InvariantCulture)).Distinct().ToArray();
var ignoreUserStartNodes =
IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue<Guid?>("dataTypeId"));
IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue<Guid?>("dataTypeKey"));
if (ignoreUserStartNodes == false)
{
int[]? aids = null;
+26 -21
View File
@@ -24,9 +24,9 @@
"angular-sanitize": "1.8.3",
"angular-touch": "1.8.3",
"angular-ui-sortable": "0.19.0",
"animejs": "3.2.1",
"animejs": "3.2.2",
"bootstrap-social": "5.1.1",
"chart.js": "^2.9.3",
"chart.js": "^2.9.4",
"clipboard": "2.0.11",
"diff": "5.1.0",
"flatpickr": "4.6.13",
@@ -37,12 +37,12 @@
"lazyload-js": "1.0.0",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.7.1",
"nouislider": "15.7.2",
"spectrum-colorpicker2": "2.0.10",
"tinymce": "6.8.2",
"tinymce": "6.8.6",
"typeahead.js": "0.11.1",
"underscore": "1.13.6",
"wicg-inert": "3.1.2"
"underscore": "1.13.7",
"wicg-inert": "3.1.3"
},
"devDependencies": {
"@babel/core": "7.21.8",
@@ -3360,9 +3360,10 @@
}
},
"node_modules/animejs": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.1.tgz",
"integrity": "sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A=="
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz",
"integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==",
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "1.1.0",
@@ -12971,9 +12972,10 @@
}
},
"node_modules/nouislider": {
"version": "15.7.1",
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.1.tgz",
"integrity": "sha512-5N7C1ru/i8y3dg9+Z6ilj6+m1EfabvOoaRa7ztpxBSKKRZso4vA52DGSbBJjw5XLtFr/LZ9SgGAXqyVtlVHO5w=="
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.2.tgz",
"integrity": "sha512-Jn1xuJZk0cs7XdzFD2tKucedlvi5/eEAsyewPS70Qqba3a1vcyEey2VZFmp7JiS06QfgkD0kEFk8oMnJrLDWXQ==",
"license": "MIT"
},
"node_modules/now-and-later": {
"version": "2.0.1",
@@ -16694,9 +16696,10 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tinymce": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.2.tgz",
"integrity": "sha512-Lho79o2Y1Yn+XdlTEkHTEkEmzwYWTXz7IUsvPwxJF3VTtgHUIAAuBab29kik+f2KED3rZvQavr9D7sHVMJ9x4A=="
"version": "6.8.6",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.6.tgz",
"integrity": "sha512-++XYEs8lKWvZxDCjrr8Baiw7KiikraZ5JkLMg6EdnUVNKJui0IsrAADj5MsyUeFkcEryfn2jd3p09H7REvewyg==",
"license": "MIT"
},
"node_modules/to-absolute-glob": {
"version": "2.0.2",
@@ -17026,9 +17029,10 @@
}
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"license": "MIT"
},
"node_modules/undertaker": {
"version": "1.3.0",
@@ -17738,9 +17742,10 @@
"dev": true
},
"node_modules/wicg-inert": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.2.tgz",
"integrity": "sha512-Ba9tGNYxXwaqKEi9sJJvPMKuo063umUPsHN0JJsjrs2j8KDSzkWLMZGZ+MH1Jf1Fq4OWZ5HsESJID6nRza2ang=="
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.3.tgz",
"integrity": "sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ==",
"license": "W3C-20150513"
},
"node_modules/word-wrap": {
"version": "1.2.4",
+6 -6
View File
@@ -36,9 +36,9 @@
"angular-sanitize": "1.8.3",
"angular-touch": "1.8.3",
"angular-ui-sortable": "0.19.0",
"animejs": "3.2.1",
"animejs": "3.2.2",
"bootstrap-social": "5.1.1",
"chart.js": "^2.9.3",
"chart.js": "^2.9.4",
"clipboard": "2.0.11",
"diff": "5.1.0",
"flatpickr": "4.6.13",
@@ -49,12 +49,12 @@
"lazyload-js": "1.0.0",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.7.1",
"nouislider": "15.7.2",
"spectrum-colorpicker2": "2.0.10",
"tinymce": "6.8.2",
"tinymce": "6.8.6",
"typeahead.js": "0.11.1",
"underscore": "1.13.6",
"wicg-inert": "3.1.2"
"underscore": "1.13.7",
"wicg-inert": "3.1.3"
},
"devDependencies": {
"@babel/core": "7.21.8",
@@ -109,7 +109,7 @@
function onAppChanged(activeApp) {
// set the name field to readonly if the user don't have update permissions or the active content app is not "Content" or "Info"
const allowUpdate = vm.editor.content.allowedActions.includes('A');
const allowUpdate = vm.content.allowedActions.includes('A');
const isContentBasedApp = activeApp && contentAppHelper.isContentBasedApp(activeApp);
vm.nameReadonly = !allowUpdate || !isContentBasedApp;
}
@@ -261,7 +261,9 @@ angular.module("umbraco")
$scope.path = _.filter(anc, function (f) {
return f.path.indexOf($scope.startNodeId) !== -1;
});
folder.path = $scope.path[0].path;
if ($scope.path.length > 0) {
folder.path = $scope.path[0].path;
}
})
: Promise.resolve().then(function () {
$scope.path = [];
@@ -368,7 +370,20 @@ angular.module("umbraco")
}
function onUploadComplete(files) {
// Refresh the current folder to get updated pagination info.
gotoFolder($scope.currentFolder).then(function () {
// If there are multiple pages, navigate to the last page where new uploads appear.
if (vm.searchOptions.totalPages > 1) {
vm.searchOptions.pageNumber = vm.searchOptions.totalPages;
return getChildren($scope.currentFolder.id);
} else {
return Promise.resolve();
}
}).then(function () {
// Select the newly uploaded items.
$timeout(function () {
if ($scope.multiPicker) {
var images = _.rest(_.sortBy($scope.images, 'id'), $scope.images.length - files.length);
@@ -378,6 +393,7 @@ angular.module("umbraco")
clickHandler(image);
}
});
});
}
@@ -595,8 +611,13 @@ angular.module("umbraco")
function syncPagination(opts, data) {
var d = data || {};
if (d.pageNumber > 0) {
opts.pageNumber = d.pageNumber;
}
opts.pageNumber = d.pageNumber;
opts.pageSize = d.pageSize;
if (d.pageSize > 0) {
opts.pageSize = d.pageSize;
}
opts.totalItems = d.totalItems;
opts.totalPages = d.totalPages;
}
@@ -527,7 +527,7 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl
&[disabled]:hover {
color: @gray-7;
border-color: @gray-7;
cursor: default;
cursor: not-allowed;
}
&.umb-block-grid__create-button {
@@ -65,12 +65,12 @@
ng-click="vm.blockEditorApi.requestShowCreate(vm.parentBlock, vm.areaKey, vm.entries.length, $event)">
</uui-button-inline-create>
<div class="umb-block-grid__actions" ng-if="!vm.blockEditorApi.readonly && vm.depth === '0'">
<div class="umb-block-grid__actions" ng-if="vm.depth === '0'">
<button
id="{{vm.model.alias}}"
type="button"
class="btn-reset umb-block-grid__create-button umb-outline"
ng-disabled="vm.locallyAvailableBlockTypes.length === 0"
ng-disabled="vm.locallyAvailableBlockTypes.length === 0 || vm.blockEditorApi.readonly"
ng-click="vm.blockEditorApi.requestShowCreate(vm.parentBlock, vm.areaKey, vm.entries.length, $event)">
<span ng-if="vm.createLabel">{{vm.createLabel}}</span>
<localize ng-if="!vm.createLabel && vm.locallyAvailableBlockTypes.length !== 1" key="blockEditor_addBlock">Add content</localize>
@@ -79,7 +79,7 @@
<button type="button"
class="btn-reset umb-block-grid__clipboard-button umb-outline"
ng-class="{'--jump': vm.jumpClipboardButton}"
ng-disabled="vm.clipboardItems.length === 0"
ng-disabled="vm.clipboardItems.length === 0 || vm.blockEditorApi.readonly"
ng-click="vm.blockEditorApi.requestShowClipboard(vm.parentBlock, vm.areaKey, vm.entries.length, $event)"
localize="title"
title="@blockEditor_tabClipboard">
@@ -83,7 +83,6 @@
var shadowRoot;
var firstLayoutContainer;
var vm = this;
vm.readonly = false;
@@ -96,6 +95,10 @@
if (deleteAllBlocksAction) {
deleteAllBlocksAction.isDisabled = vm.readonly;
}
if (enterSortModeAction) {
enterSortModeAction.isDisabled = vm.readonly;
}
});
vm.loading = true;
@@ -1367,8 +1370,9 @@
if (copyAllBlocksAction) {
copyAllBlocksAction.isDisabled = vm.layout.length === 0;
}
if (deleteAllBlocksAction) {
deleteAllBlocksAction.isDisabled = vm.layout.length === 0;
deleteAllBlocksAction.isDisabled = vm.layout.length === 0 || vm.readonly;
}
// validate limits:
@@ -1,4 +1,3 @@
/**
* The controller that is used for a couple different Property Editors: Multi Node Tree Picker, Content Picker,
* since this is used by MNTP and it supports content, media and members, there is code to deal with all 3 of those types
@@ -345,17 +344,24 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso
};
$scope.remove = function (index) {
$scope.remove = function (id) {
if (!$scope.allowRemove) return;
var currIds = $scope.model.value ? $scope.model.value.split(',') : [];
if (currIds.length > 0) {
currIds.splice(index, 1);
setDirty();
$scope.model.value = currIds.join();
var currUdis = $scope.model.value ? $scope.model.value.split(',') : [];
if (currUdis.length > 0) {
// Remove the node with the provided UDI.
var index = currUdis.indexOf(id.toString()); // id may be an integer ID or a UDI string, split() always returns strings.
if (index >= 0) {
currUdis.splice(index, 1);
setDirty();
// If no ids left, set value to null to match `clear()` behavior.
$scope.model.value = currUdis.length > 0 ? currUdis.join() : null;
}
}
removeAllEntriesAction.isDisabled = currIds.length === 0;
removeAllEntriesAction.isDisabled = currUdis.length === 0;
};
$scope.showNode = function (index) {
@@ -13,8 +13,8 @@
description="node.url"
sortable="!sortableOptions.disabled"
allow-remove="allowRemove"
allow-open="model.config.showOpenButton && allowOpen && !dialogEditor && !readonly"
on-remove="remove($index)"
allow-open="model.config.showOpenButton && allowOpen && !dialogEditor"
on-remove="remove(model.config.idType === 'udi' ? node.udi : node.id)"
on-open="openEditor(node)">
</umb-node-preview>
</div>
@@ -276,15 +276,16 @@
};
Utilities.extend(baseLineConfigObj, standardConfig);
// Readonly mode
baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar;
baseLineConfigObj.readonly = vm.readonly ? true : baseLineConfigObj.readonly;
const options = Utilities.extend({}, baseLineConfigObj, standardConfig);
// We need to wait for DOM to have rendered before we can find the element by ID.
$timeout(function () {
tinymce.init(baseLineConfigObj);
// Readonly mode - ensure observe of readonly attribute has triggered, see https://github.com/umbraco/Umbraco-CMS/issues/20952
options.toolbar = vm.readonly ? false : options.toolbar;
options.readonly = vm.readonly ? true : options.readonly;
tinymce.init(options);
}, 50);
//listen for formSubmitting event (the result is callback used to remove the event subscription)
@@ -81,7 +81,7 @@ describe('Content picker controller tests', function () {
});
it("Removing an item should update renderModel, ids and model.value", function(){
scope.remove(1);
scope.remove(1231);
scope.$apply();
expect(scope.renderModel.length).toBe(2);
expect(scope.model.value).toBe("1233,23121");
@@ -51,8 +51,8 @@ public class UmbLoginStatusController : SurfaceController
TempData["LogoutSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
{
return Redirect(model.RedirectUrl!);
}
@@ -70,8 +70,8 @@ public class UmbProfileController : SurfaceController
TempData["FormSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
{
return Redirect(model.RedirectUrl!);
}
@@ -59,8 +59,8 @@ public class UmbRegisterController : SurfaceController
{
TempData["FormSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
// If there is a specified path to redirect to and it is validated as a local URL, then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() is false && Url.IsLocalUrl(model.RedirectUrl!))
{
return Redirect(model.RedirectUrl!);
}
@@ -0,0 +1,141 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Routing;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Routing;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class RedirectTrackerTests : UmbracoIntegrationTestWithContent
{
private IRedirectUrlService RedirectUrlService => GetRequiredService<IRedirectUrlService>();
private IContent _rootPage;
private IContent _testPage;
public override void CreateTestData()
{
base.CreateTestData();
var rootContent = ContentService.GetRootContent().First();
_rootPage = rootContent;
var subPages = ContentService.GetPagedChildren(rootContent.Id, 0, 3, out _).ToList();
_testPage = subPages[0];
}
[Test]
public void Can_Create_Redirects()
{
IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> dict =
new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>
{
[(_testPage.Id, "en")] = (_testPage.Key, "/old-route"),
};
var redirectTracker = CreateRedirectTracker();
redirectTracker.CreateRedirects(dict);
var redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
Assert.AreEqual(1, redirects.Count());
var redirect = redirects.First();
Assert.AreEqual("/old-route", redirect.Url);
}
[Test]
public void Will_Remove_Self_Referencing_Redirects()
{
CreateExistingRedirect();
var redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
Assert.IsTrue(redirects.Any(x => x.Url == "/new-route")); // Ensure self referencing redirect exists.
IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> dict =
new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>
{
[(_testPage.Id, "en")] = (_testPage.Key, "/old-route"),
};
var redirectTracker = CreateRedirectTracker();
redirectTracker.CreateRedirects(dict);
redirects = RedirectUrlService.GetContentRedirectUrls(_testPage.Key);
Assert.AreEqual(1, redirects.Count());
var redirect = redirects.First();
Assert.AreEqual("/old-route", redirect.Url);
}
private RedirectUrlRepository CreateRedirectUrlRepository() =>
new(
(IScopeAccessor)ScopeProvider,
AppCaches.Disabled,
new NullLogger<RedirectUrlRepository>());
private IRedirectTracker CreateRedirectTracker()
{
var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
var cultures = new Dictionary<string, PublishedCultureInfo>
{
{ "en", new PublishedCultureInfo("en", "english", "/en/", DateTime.UtcNow) },
};
var rootContent = new Mock<IPublishedContent>();
rootContent.SetupGet(c => c.Id).Returns(_rootPage.Id);
rootContent.SetupGet(c => c.Key).Returns(_rootPage.Key);
rootContent.SetupGet(c => c.Name).Returns(_rootPage.Name);
rootContent.SetupGet(c => c.Path).Returns(_rootPage.Path);
var content = new Mock<IPublishedContent>();
content.SetupGet(c => c.Id).Returns(_testPage.Id);
content.SetupGet(c => c.Key).Returns(_testPage.Key);
content.SetupGet(c => c.Name).Returns(_testPage.Name);
content.SetupGet(c => c.Path).Returns(_testPage.Path);
content.SetupGet(c => c.ContentType).Returns(contentType.Object);
content.SetupGet(c => c.Cultures).Returns(cultures);
IPublishedContentCache contentCache = Mock.Of<IPublishedContentCache>();
Mock.Get(contentCache)
.Setup(x => x.GetRouteById(_testPage.Id, "en"))
.Returns("/new-route");
UmbracoContextReference contextReference = new UmbracoContextReference(Mock.Of<IUmbracoContext>(), false, Mock.Of<IUmbracoContextAccessor>());
Mock.Get(contextReference.UmbracoContext)
.Setup(x => x.Content)
.Returns(contentCache);
IUmbracoContextFactory contextFactory = Mock.Of<IUmbracoContextFactory>();
Mock.Get(contextFactory)
.Setup(x => x.EnsureUmbracoContext())
.Returns(contextReference);
return new RedirectTracker(
contextFactory,
GetRequiredService<IVariationContextAccessor>(),
GetRequiredService<ILocalizationService>(),
RedirectUrlService,
GetRequiredService<ILogger<RedirectTracker>>());
}
private void CreateExistingRedirect()
{
using var scope = ScopeProvider.CreateScope();
var repository = CreateRedirectUrlRepository();
repository.Save(new RedirectUrl { ContentKey = _testPage.Key, Url = "/new-route", Culture = "en" });
scope.Complete();
}
}
@@ -5,12 +5,16 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
@@ -35,6 +39,10 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
{
builder.AddNotificationHandler<ContentMovedToRecycleBinNotification, ContentNotificationHandler>();
builder.AddNotificationHandler<ContentTypeDeletedNotification, ContentTypeNotificationHandler>();
builder.AddNotificationHandler<ContentTypeChangedNotification, ContentTypeChangedDistributedCacheNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, ContentTypeCacheRefreshedNotificationHandler>();
builder.Services.AddUnique<IServerMessenger, ContentEventsTests.LocalServerMessenger>();
}
[Test]
@@ -2026,6 +2034,328 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
}
[Test]
public void Adding_ContentType_Composition_Yields_RefreshOther()
{
var cts = ContentTypeService;
// Arrange
IContentType component = CreateComponent();
cts.Save(component);
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
Assert.IsTrue(site.AddContentType(component));
Assert.IsTrue(site.ContentTypeCompositionExists(component.Alias));
cts.Save(site);
// Assert; expect RefreshOther when adding a compostion
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
}
[Test]
public void Adding_PropertyType_Yields_RefreshOther()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
var propertyType =
new PropertyType(ShortStringHelper, Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext,
"title")
{
Name = "Title",
Description = string.Empty,
Mandatory = false,
SortOrder = 1,
DataTypeId = -88
};
Assert.IsTrue(site.AddPropertyType(propertyType));
cts.Save(site);
// Assert; expect RefreshOther when adding a property
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
}
[Test]
public void Removing_ContentType_Composition_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType component = CreateComponent();
cts.Save(component);
IContentType site = CreateSite();
Assert.IsTrue(site.AddContentType(component));
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
Assert.IsTrue(site.ContentTypeCompositionExists(component.Alias));
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
Assert.IsTrue(site.RemoveContentType(component.Alias));
cts.Save(site);
// Assert; expect RefreshMain when removing a composition
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Removing_PropertyType_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.RemovePropertyType(site.PropertyTypes.First().Alias);
cts.Save(site);
// Assert; expect RefreshMain when removing a property
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Removing_PropertyType_By_Replacing_Collection_Yields_RefreshMain()
{
// This test simulates how the backoffice mapper removes a property: rather than calling
// RemovePropertyType(), it rebuilds the property collection without the removed property
// and assigns it to the group. This is the code path in ContentTypeMapDefinition.MapSaveToTypeBase().
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
// Simulate the mapper path: replace the group's PropertyTypes with an empty collection
// (as if the property was removed via the backoffice UI)
PropertyGroup group = site.PropertyGroups.First();
group.PropertyTypes = new PropertyTypeCollection(true);
cts.Save(site);
// Assert; expect RefreshMain when removing a property, regardless of how it was removed
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Removing_PropertyTypeGroup_Yields_RefreshOther()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.RemovePropertyGroup(site.PropertyTypes.First().Alias);
cts.Save(site);
// Assert; removing a group does not cause the contained properties to be removed, so expect RefreshOther
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
}
[Test]
public void Changing_PropertyType_Alias_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.PropertyTypes.First().Alias += "_updated";
cts.Save(site);
// Assert; expect RefreshMain when changing the alias of a property (it corresponds to removing the property)
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Changing_ContentType_Alias_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.Alias += "_updated";
cts.Save(site);
// Assert; expect RefreshMain when changing the alias of a content type
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Changing_PropertyType_Variance_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
site.Variations = ContentVariation.Culture;
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.PropertyTypes.First().Variations = ContentVariation.Culture;
cts.Save(site);
// Assert; expect RefreshMain when changing the variance of a property
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Changing_ContentType_Variance_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.Variations = ContentVariation.Culture;
cts.Save(site);
// Assert; expect RefreshMain when changing the variance of a content type
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshMain);
}
[Test]
public void Changing_User_Interface_Settings_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.Name += "_updated";
site.Description += "_updated";
site.Icon += "_updated";
site.PropertyTypes.First().Name += "_updated";
cts.Save(site);
// Assert; expect RefreshOther when making UI changes only (names, icon, description etc.)
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
}
[Test]
public void Changing_History_Cleanup_And_Basic_Structure_Settings_Yields_RefreshMain()
{
var cts = ContentTypeService;
// Arrange
IContentType site = CreateSite();
cts.Save(site);
// re-fetch before acting
site = cts.Get(site.Id)!;
// Act
ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null;
ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads
=> refreshedPayloads = payloads;
site.HistoryCleanup = new HistoryCleanup
{
KeepAllVersionsNewerThanDays = 12,
KeepLatestVersionPerDayForDays = 32,
PreventCleanup = false,
};
site.AllowedAsRoot = !site.AllowedAsRoot;
site.AllowedContentTypes = [new ContentTypeSort(site.Id, 1)];
cts.Save(site);
// Assert; expect RefreshOther when making UI changes only (names, icon, description etc.)
AssertContentTypeRefreshPayload(refreshedPayloads, site.Id, ContentTypeChangeTypes.RefreshOther);
}
private ContentType CreateComponent()
{
var component = new ContentType(ShortStringHelper, -1)
@@ -2169,4 +2499,32 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
public static Action<ContentTypeDeletedNotification> Deleted { get; set; }
public void Handle(ContentTypeDeletedNotification notification) => Deleted?.Invoke(notification);
}
public class ContentTypeCacheRefreshedNotificationHandler : INotificationHandler<ContentTypeCacheRefresherNotification>
{
public static Action<ContentTypeCacheRefresher.JsonPayload[]>? ContentTypeCacheRefreshed { get; set; }
public void Handle(ContentTypeCacheRefresherNotification notification)
{
if (notification.MessageType != MessageType.RefreshByPayload || notification.MessageObject is not ContentTypeCacheRefresher.JsonPayload[] payloads)
{
throw new NotSupportedException();
}
ContentTypeCacheRefreshed?.Invoke(payloads);
}
}
private static void AssertContentTypeRefreshPayload(ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads, int expectedContentTypeId, ContentTypeChangeTypes expectedChangeTypes)
{
Assert.IsNotNull(refreshedPayloads);
Assert.AreEqual(1, refreshedPayloads.Length);
Assert.Multiple(() =>
{
var payload = refreshedPayloads.First();
Assert.AreEqual(expectedContentTypeId, payload.Id);
Assert.AreEqual(expectedChangeTypes, payload.ChangeTypes);
Assert.AreEqual(nameof(IContentType), payload.ItemType);
});
}
}
@@ -0,0 +1,31 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
[TestFixture]
public class ContentTypeChangeExtensionsTests
{
[TestCase(ContentTypeChangeTypes.RefreshMain, true)]
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther, true)]
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.Create, true)]
[TestCase(ContentTypeChangeTypes.RefreshOther, false)]
[TestCase(ContentTypeChangeTypes.None, false)]
[TestCase(ContentTypeChangeTypes.Create, false)]
[TestCase(ContentTypeChangeTypes.Remove, false)]
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove, false)]
public void IsStructuralChange(ContentTypeChangeTypes change, bool expected) =>
Assert.AreEqual(expected, change.IsStructuralChange());
[TestCase(ContentTypeChangeTypes.RefreshOther, true)]
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Create, true)]
[TestCase(ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove, true)]
[TestCase(ContentTypeChangeTypes.RefreshMain, false)]
[TestCase(ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther, false)]
[TestCase(ContentTypeChangeTypes.None, false)]
[TestCase(ContentTypeChangeTypes.Create, false)]
[TestCase(ContentTypeChangeTypes.Remove, false)]
public void IsNonStructuralChange(ContentTypeChangeTypes change, bool expected) =>
Assert.AreEqual(expected, change.IsNonStructuralChange());
}
@@ -1,7 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
using Moq;
@@ -14,11 +13,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.IO;
[TestFixture]
public class PhysicalFileSystemTests : AbstractFileSystemTests
{
[SetUp]
public void Setup()
{
}
[TearDown]
public void TearDown()
{
@@ -44,22 +38,10 @@ public class PhysicalFileSystemTests : AbstractFileSystemTests
Mock.Of<ILogger<PhysicalFileSystem>>(),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"),
"/Media/"))
{
}
{ }
protected override string ConstructUrl(string path) => "/Media/" + path;
private string Repeat(string pattern, int count)
{
var text = new StringBuilder();
for (var i = 0; i < count; i++)
{
text.Append(pattern);
}
return text.ToString();
}
[Test]
public void SaveFileTest()
{
@@ -70,16 +52,7 @@ public class PhysicalFileSystemTests : AbstractFileSystemTests
_fileSystem.AddFile("sub/f3.txt", ms);
}
Assert.IsTrue(File.Exists(Path.Combine(basePath, "sub/f3.txt")));
var path = Repeat("bah/bah/", 50);
Assert.Less(260, path.Length);
Assert.Throws<PathTooLongException>(() =>
{
using var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"));
_fileSystem.AddFile(path + "f3.txt", ms);
});
Assert.IsTrue(File.Exists(Path.Combine(basePath, "sub", "f3.txt")));
}
[Test]
@@ -98,28 +71,18 @@ public class PhysicalFileSystemTests : AbstractFileSystemTests
// - does throw on invalid paths
// works
var path = _fileSystem.GetFullPath("foo.tmp");
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
Assert.AreEqual(Path.Combine(basePath, "foo.tmp"), path);
// a very long relative path, which ends up being a short path, works
path = Repeat("bah/../", 50);
Assert.Less(260, path.Length);
// normalize path with parent directory references
path = "foo/../bar/../bah/../";
path = _fileSystem.GetFullPath(path + "foo.tmp");
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
Assert.AreEqual(Path.Combine(basePath, "foo.tmp"), path);
// works too
path = _fileSystem.GetFullPath("foo/bar.tmp");
Assert.AreEqual(Path.Combine(basePath, @$"foo{Path.DirectorySeparatorChar}bar.tmp"), path);
Assert.AreEqual(Path.Combine(basePath, "foo", "bar.tmp"), path);
// that path is invalid as it would be outside the root directory
Assert.Throws<UnauthorizedAccessException>(() => _fileSystem.GetFullPath("../../foo.tmp"));
// a very long path, which ends up being very long, works
path = Repeat("bah/bah/", 50);
Assert.Less(260, path.Length);
Assert.Throws<PathTooLongException>(() =>
{
path = _fileSystem.GetFullPath(path + "foo.tmp");
Assert.Less(260, path.Length); // gets a >260 path and it's fine (but Windows will not like it)
});
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "13.12.0-rc2",
"version": "13.15.0-rc",
"assemblyVersion": {
"precision": "build"
},