From ca267047d3cb42be0cedb1f0f8d8e7b5458e5b81 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 25 Nov 2025 12:32:28 +0100 Subject: [PATCH 01/45] Bumped version to 16.4.0. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 4fee7cb51d33..c1988caffe4d 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.4.0-rc3", + "version": "16.4.0", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index 027018923c0c..0bd9f3962820 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "16.4.0-rc3", + "version": "16.4.0", "assemblyVersion": { "precision": "build" }, From 58182b32115192c16e6bf843ed6c6f707e0c3651 Mon Sep 17 00:00:00 2001 From: NillasKA Date: Wed, 26 Nov 2025 14:05:58 +0100 Subject: [PATCH 02/45] Adding a ton of missing configurations --- .../PropertyEditors/BlockGridConfiguration.cs | 45 ++++++++++++++++--- .../PropertyEditors/BlockListConfiguration.cs | 12 +++++ .../PropertyEditors/IBlockConfiguration.cs | 12 +++++ .../PropertyEditors/RichTextConfiguration.cs | 18 +++++++- .../IBlockGridConfiguration.cs | 16 +++++++ .../IRichTextBlockConfiguration.cs | 6 +++ 6 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs diff --git a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs index ade1da8b8af2..029920846bde 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs @@ -1,6 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; + namespace Umbraco.Cms.Core.PropertyEditors; /// @@ -17,19 +19,39 @@ public class BlockGridConfiguration [ConfigurationField("gridColumns")] public int? GridColumns { get; set; } - public class BlockGridBlockConfiguration : IBlockConfiguration + public class BlockGridBlockConfiguration : IBlockGridConfiguration { - public int? AreaGridColumns { get; set; } - public BlockGridAreaConfiguration[] Areas { get; set; } = Array.Empty(); public Guid ContentElementTypeKey { get; set; } public Guid? SettingsElementTypeKey { get; set; } - public bool AllowAtRoot { get; set; } + public string? Label { get; set; } + + public string? EditorSize { get; set; } + + public string? IconColor { get; set; } + + public string? BackgroundColor { get; set; } + + public string? Thumbnail { get; set; } + + public bool? ForceHideContentEditorInOverlay { get; set; } + + public bool? DisplayInline { get; set; } + + public bool? AllowAtRoot { get; set; } + + public bool? AllowInAreas { get; set; } + + public bool? HideContentEditor { get; set; } - public bool AllowInAreas { get; set; } + public int? RowMinSpan { get; set; } + + public int? RowMaxSpan { get; set; } + + public int? AreaGridColumns { get; set; } } public class NumberRange @@ -52,5 +74,18 @@ public class BlockGridAreaConfiguration public int? MinAllowed { get; set; } public int? MaxAllowed { get; set; } + + public SpecifiedAllowanceConfiguration[] SpecifiedAllowance { get; set; } = Array.Empty(); + + public string? CreateLabel { get; set; } + } + + public class SpecifiedAllowanceConfiguration + { + public int? MinAllowed { get; set; } + + public int? MaxAllowed { get; set; } + + public Guid? ElementTypeKey { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index 0e4c5fe00f0b..b920de48b17a 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -23,6 +23,18 @@ public class BlockConfiguration : IBlockConfiguration public Guid ContentElementTypeKey { get; set; } public Guid? SettingsElementTypeKey { get; set; } + + public string? Label { get; set; } + + public string? EditorSize { get; set; } + + public string? IconColor { get; set; } + + public string? BackgroundColor { get; set; } + + public string? Thumbnail { get; set; } + + public bool? ForceHideContentEditorInOverlay { get; set; } } public class NumberRange diff --git a/src/Umbraco.Core/PropertyEditors/IBlockConfiguration.cs b/src/Umbraco.Core/PropertyEditors/IBlockConfiguration.cs index 350f4a084301..25691fcf8254 100644 --- a/src/Umbraco.Core/PropertyEditors/IBlockConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/IBlockConfiguration.cs @@ -8,4 +8,16 @@ public interface IBlockConfiguration public Guid ContentElementTypeKey { get; set; } public Guid? SettingsElementTypeKey { get; set; } + + public string? Label { get; set; } + + public string? EditorSize { get; set; } + + public string? IconColor { get; set; } + + public string? BackgroundColor { get; set; } + + public string? Thumbnail { get; set; } + + public bool? ForceHideContentEditorInOverlay { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index 5ebcb13b5de1..4dbbad33cca3 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -1,3 +1,5 @@ +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; + namespace Umbraco.Cms.Core.PropertyEditors; /// @@ -14,10 +16,24 @@ public class RichTextConfiguration : IIgnoreUserStartNodesConfig [ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)] public bool IgnoreUserStartNodes { get; set; } - public class RichTextBlockConfiguration : IBlockConfiguration + public class RichTextBlockConfiguration : IRichTextBlockConfiguration { public Guid ContentElementTypeKey { get; set; } public Guid? SettingsElementTypeKey { get; set; } + + public string? Label { get; set; } + + public string? EditorSize { get; set; } + + public string? IconColor { get; set; } + + public string? BackgroundColor { get; set; } + + public string? Thumbnail { get; set; } + + public bool? ForceHideContentEditorInOverlay { get; set; } + + public bool? DisplayInline { get; set; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs new file mode 100644 index 000000000000..6d9a76bb24d2 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public interface IBlockGridConfiguration : IBlockConfiguration +{ + public bool? AllowAtRoot { get; set; } + + public bool? AllowInAreas { get; set; } + + public bool? HideContentEditor { get; set; } + + public int? RowMinSpan { get; set; } + + public int? RowMaxSpan { get; set; } + + public int? AreaGridColumns { get; set; } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs new file mode 100644 index 000000000000..f0b6e05c7012 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public interface IRichTextBlockConfiguration : IBlockConfiguration +{ + public bool? DisplayInline { get; set; } +} From c61bcca0669e55b9f702022bab3fb8d772285a21 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 27 Nov 2025 10:47:19 +0100 Subject: [PATCH 03/45] Add Claude memory files for all relevant project files (#20959) * Regenerate delivery api claud memory file for updated file lines and inclusion of Secure Cookie-Based Token Storage * Add delivery api memory file * claude memory file for in memory modelsbuilder project * Claud memory file for Imagesharp project * Claude memory file for legacy image sharp project * Claude memory files for Persistence projects * Remaining claude memory files --- src/Umbraco.Cms.Api.Common/CLAUDE.md | 145 +++---- src/Umbraco.Cms.Api.Delivery/CLAUDE.md | 382 ++++++++++++++++++ .../CLAUDE.md | 258 ++++++++++++ src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md | 234 +++++++++++ src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md | 139 +++++++ .../CLAUDE.md | 132 ++++++ .../CLAUDE.md | 134 ++++++ src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md | 255 ++++++++++++ .../CLAUDE.md | 235 +++++++++++ src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md | 204 ++++++++++ src/Umbraco.Cms.StaticAssets/CLAUDE.md | 260 ++++++++++++ src/Umbraco.Examine.Lucene/CLAUDE.md | 293 ++++++++++++++ .../CLAUDE.md | 362 +++++++++++++++++ src/Umbraco.Web.Common/CLAUDE.md | 380 +++++++++++++++++ src/Umbraco.Web.UI.Login/CLAUDE.md | 266 ++++++++++++ src/Umbraco.Web.UI/CLAUDE.md | 320 +++++++++++++++ src/Umbraco.Web.Website/CLAUDE.md | 245 +++++++++++ 17 files changed, 4166 insertions(+), 78 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Delivery/CLAUDE.md create mode 100644 src/Umbraco.Cms.DevelopmentMode.Backoffice/CLAUDE.md create mode 100644 src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md create mode 100644 src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md create mode 100644 src/Umbraco.Cms.Persistence.EFCore.SqlServer/CLAUDE.md create mode 100644 src/Umbraco.Cms.Persistence.EFCore.Sqlite/CLAUDE.md create mode 100644 src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/CLAUDE.md create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md create mode 100644 src/Umbraco.Cms.StaticAssets/CLAUDE.md create mode 100644 src/Umbraco.Examine.Lucene/CLAUDE.md create mode 100644 src/Umbraco.PublishedCache.HybridCache/CLAUDE.md create mode 100644 src/Umbraco.Web.Common/CLAUDE.md create mode 100644 src/Umbraco.Web.UI.Login/CLAUDE.md create mode 100644 src/Umbraco.Web.UI/CLAUDE.md create mode 100644 src/Umbraco.Web.Website/CLAUDE.md diff --git a/src/Umbraco.Cms.Api.Common/CLAUDE.md b/src/Umbraco.Cms.Api.Common/CLAUDE.md index a40488c9fa7d..9829f32bbcde 100644 --- a/src/Umbraco.Cms.Api.Common/CLAUDE.md +++ b/src/Umbraco.Cms.Api.Common/CLAUDE.md @@ -23,7 +23,7 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery). - `Umbraco.Core` - Domain models and service contracts - `Umbraco.Web.Common` - Web functionality -### Project Structure (45 files) +### Project Structure (46 files) ``` Umbraco.Cms.Api.Common/ @@ -55,20 +55,7 @@ Umbraco.Cms.Api.Common/ ## 2. Commands -```bash -# Build -dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj - -# Pack for NuGet -dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release - -# Run tests (integration tests in consuming APIs) -dotnet test tests/Umbraco.Tests.Integration/ - -# Check for outdated/vulnerable packages -dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated -dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable -``` +See "Quick Reference" section at bottom for common commands. --- @@ -89,20 +76,22 @@ public class SchemaIdHandler : ISchemaIdHandler **Why**: Management and Delivery APIs can customize schema/operation ID generation. -### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:32) +### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 32) ```csharp -// Remove invalid characters to prevent OpenAPI generation errors -return Regex.Replace(name, @"[^\w]", string.Empty); - -// Add "Model" suffix to avoid TypeScript name clashes (line 24) +// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29) if (name.EndsWith("Model") == false) { + // because some models names clash with common classes in TypeScript (i.e. Document), + // we need to add a "Model" postfix to all models name = $"{name}Model"; } + +// Remove invalid characters to prevent OpenAPI generation errors (line 32) +return Regex.Replace(name, @"[^\w]", string.Empty); ``` -### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:31-34) +### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35) ```csharp // IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference @@ -144,8 +133,10 @@ dotnet test tests/Umbraco.Tests.Integration/ ### Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs) -**Reference Tokens over JWT** (line 73-74): +**Reference Tokens over JWT** (line 76-80): ```csharp +// Enable reference tokens +// - see https://documentation.openiddict.com/configuration/token-storage.html options .UseReferenceAccessTokens() .UseReferenceRefreshTokens(); @@ -153,23 +144,44 @@ options **Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection. -**Token Lifetime** (line 84-85): +**Token Lifetime** (line 88-91): ```csharp -// Access token: 25% of refresh token lifetime +// Make the access token lifetime 25% of the refresh token lifetime options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4)); options.SetRefreshTokenLifetime(timeOut); ``` -**PKCE Required** (line 54-56): +**PKCE Required** (line 59-63): ```csharp +// Enable authorization code flow with PKCE options .AllowAuthorizationCodeFlow() - .RequireProofKeyForCodeExchange(); + .RequireProofKeyForCodeExchange() + .AllowRefreshTokenFlow(); +``` + +**Endpoints**: Backoffice `/umbraco/management/api/v1/security/*`, Member `/umbraco/member/api/v1/security/*` + +### Secure Cookie-Based Token Storage (v17+) + +**Implementation** (DependencyInjection/HideBackOfficeTokensHandler.cs): + +Back-office tokens are hidden from client-side JavaScript via HTTP-only cookies: + +```csharp +private const string AccessTokenCookieKey = "__Host-umbAccessToken"; +private const string RefreshTokenCookieKey = "__Host-umbRefreshToken"; + +// Tokens are encrypted via Data Protection and stored in cookies +SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken); +context.Response.AccessToken = "[redacted]"; // Client sees redacted value ``` -**Endpoints**: -- Backoffice: `/umbraco/management/api/v1/security/*` -- Member: `/umbraco/member/api/v1/security/*` +**Key Security Features** (lines 143-165): `HttpOnly`, `IsEssential`, `Path="/"`, `Secure` (HTTPS), `__Host-` prefix + +**Configuration**: `BackOfficeTokenCookieSettings.Enabled` (default: true in v17+) + +**Implications**: Client-side cannot access tokens; encrypted with Data Protection; load balancing needs shared key ring; API requests need `credentials: include` --- @@ -183,10 +195,9 @@ options ```csharp catch (NotSupportedException exception) { - // This happens when trying to deserialize to an interface, - // without sending the $type as part of the request - context.ModelState.TryAddModelException(string.Empty, - new InputFormatterException(exception.Message, exception)); + // This happens when trying to deserialize to an interface, without sending the $type as part of the request + context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception)); + return await InputFormatterResult.FailureAsync(); } ``` @@ -196,23 +207,23 @@ catch (NotSupportedException exception) **Issue**: Type names like `Document` clash with TypeScript built-ins. -**Solution** (OpenApi/SchemaIdHandler.cs:24-29): -```csharp -if (name.EndsWith("Model") == false) -{ - // Add "Model" postfix to all models - name = $"{name}Model"; -} -``` +**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29) ### Generic Type Handling **Issue**: `PagedViewModel` needs flattened schema name. -**Solution** (OpenApi/SchemaIdHandler.cs:41-49): +**Solution** (OpenApi/SchemaIdHandler.cs:41-50): ```csharp -// Turns "PagedViewModel" into "PagedRelationItemModel" -return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}"; +private string HandleGenerics(string name, Type type) +{ + if (!type.IsGenericType) + return name; + + // use attribute custom name or append the generic type names + // turns "PagedViewModel" into "PagedRelationItem" + return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}"; +} ``` --- @@ -258,16 +269,6 @@ return BadRequest(problemDetails); ## 8. Project-Specific Notes -### Why Reference Tokens Instead of JWT? - -**Decision**: Use `UseReferenceAccessTokens()` and ASP.NET Core Data Protection. - -**Tradeoffs**: -- ✅ **Pros**: Revocable, simpler key management, better security -- ❌ **Cons**: Requires database lookup (slower than JWT), needs shared Data Protection key ring - -**Load Balancing Requirement**: All servers must share the same Data Protection key ring and application name. - ### Why Virtual Handlers? **Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual. @@ -278,12 +279,7 @@ return BadRequest(problemDetails); ### Performance: Subtype Caching -**Implementation** (Serialization/UmbracoJsonTypeInfoResolver.cs:14): -```csharp -private readonly ConcurrentDictionary> _subTypesCache = new(); -``` - -**Why**: Reflection is expensive. Cache discovered subtypes to avoid repeated `ITypeFinder.FindClassesOfType()` calls. +**Why**: Cache discovered subtypes (UmbracoJsonTypeInfoResolver.cs:14) to avoid expensive reflection calls ### Known Limitations @@ -318,23 +314,11 @@ private readonly ConcurrentDictionary> _subTypesCache = new(); ### Configuration -**HTTPS** (Configuration/ConfigureOpenIddict.cs:14): -```csharp -// Disable transport security requirement for local development -options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false; -``` - -**⚠️ Warning**: Never disable HTTPS in production. +**HTTPS**: `DisableTransportSecurityRequirement` for local dev only (ConfigureOpenIddict.cs:14). **Warning**: Never disable in production. -### Usage by Consuming APIs +### Usage Pattern -**Registration Pattern**: -```csharp -// In Umbraco.Cms.Api.Management or Umbraco.Cms.Api.Delivery -builder - .AddUmbracoApiOpenApiUI() // Swagger + custom handlers - .AddUmbracoOpenIddict(); // OAuth 2.0 authentication -``` +Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()` --- @@ -343,7 +327,7 @@ builder ### Essential Commands ```bash -# Build +# Build project dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj # Pack for NuGet @@ -351,6 +335,10 @@ dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release # Test via integration tests dotnet test tests/Umbraco.Tests.Integration/ + +# Check packages +dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated +dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable ``` ### Key Classes @@ -361,13 +349,14 @@ dotnet test tests/Umbraco.Tests.Integration/ | `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs | | `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs | | `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs | +| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs | | `PagedViewModel` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs | ### Important Files - `Umbraco.Cms.Api.Common.csproj` - Project dependencies -- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-30) -- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 19-144) +- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-31) +- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 20-183) - `Security/Paths.cs` - API endpoint path constants ### Getting Help diff --git a/src/Umbraco.Cms.Api.Delivery/CLAUDE.md b/src/Umbraco.Cms.Api.Delivery/CLAUDE.md new file mode 100644 index 000000000000..a29af215f401 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/CLAUDE.md @@ -0,0 +1,382 @@ +# Umbraco.Cms.Api.Delivery + +Headless content delivery REST API for Umbraco CMS. Enables frontend applications to fetch published content, media, and member-protected resources. + +--- + +## 1. Architecture + +**Type**: Class Library (NuGet Package) +**Target Framework**: .NET 10.0 +**Purpose**: Content Delivery API for headless CMS scenarios + +### Key Technologies + +- **ASP.NET Core** - Web framework +- **OpenIddict** - Member authentication (OAuth 2.0) +- **Asp.Versioning** - API versioning (V1, V2) +- **Output Caching** - Configurable response caching +- **Examine/Lucene** - Content querying + +### Dependencies + +- `Umbraco.Cms.Api.Common` - Shared API infrastructure (OpenAPI, auth) +- `Umbraco.Web.Common` - Web functionality + +### Project Structure (86 files) + +``` +Umbraco.Cms.Api.Delivery/ +├── Controllers/ +│ ├── Content/ # Content endpoints (by ID, route, query) +│ ├── Media/ # Media endpoints (by ID, path, query) +│ └── Security/ # Member auth (authorize, token, signout) +├── Querying/ +│ ├── Filters/ # ContentType, Name, CreateDate, UpdateDate +│ ├── Selectors/ # Ancestors, Children, Descendants +│ └── Sorts/ # Name, CreateDate, UpdateDate, Level, SortOrder +├── Indexing/ # Lucene index field handlers +├── Services/ # Business logic and query building +├── Caching/ # Output cache policies +├── Rendering/ # Output expansion strategies +├── Configuration/ # Swagger configuration +└── Filters/ # Action filters (access, validation) +``` + +### Design Patterns + +1. **Strategy Pattern** - Query handlers (`ISelectorHandler`, `IFilterHandler`, `ISortHandler`) +2. **Factory Pattern** - `ApiContentQueryFactory` builds Examine queries +3. **Template Method** - `ContentApiControllerBase` for shared controller logic +4. **Options Pattern** - `DeliveryApiSettings` for all configuration + +--- + +## 2. Commands + +See "Quick Reference" section at bottom for common commands. + +--- + +## 3. Key Patterns + +### API Versioning (V1 vs V2) + +**V1** (legacy) and **V2** (current) coexist. Key difference is output expansion: + +```csharp +// DependencyInjection/UmbracoBuilderExtensions.cs:49-52 +// V1 uses RequestContextOutputExpansionStrategy +// V2+ uses RequestContextOutputExpansionStrategyV2 +return apiVersion.MajorVersion == 1 + ? provider.GetRequiredService() + : provider.GetRequiredService(); +``` + +**Why V2**: Improved `expand` and `fields` query parameter parsing (tree-based). + +### Query System Architecture + +Content querying flows through handlers registered in DI: + +1. **Selectors** (`fetch` parameter): `ancestors:id`, `children:id`, `descendants:id` +2. **Filters** (`filter[]` parameter): `contentType:alias`, `name:value`, `createDate>2024-01-01` +3. **Sorts** (`sort[]` parameter): `name:asc`, `createDate:desc`, `level:asc` + +```csharp +// Services/ApiContentQueryService.cs:91-96 +ISelectorHandler? selectorHandler = _selectorHandlers.FirstOrDefault(h => h.CanHandle(fetch)); +return selectorHandler?.BuildSelectorOption(fetch); +``` + +### Path Decoding Workaround + +ASP.NET Core doesn't decode forward slashes in route parameters: + +```csharp +// Controllers/DeliveryApiControllerBase.cs:21-31 +// OpenAPI clients URL-encode paths, but ASP.NET Core doesn't decode "/" +// See https://github.com/dotnet/aspnetcore/issues/11544 +if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase)) +{ + path = WebUtility.UrlDecode(path); +} +``` + +--- + +## 4. Testing + +**Location**: No direct tests - tested via integration tests in test projects + +```bash +dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery" +``` + +**Internals exposed to** (csproj lines 28-36): +- `Umbraco.Tests.UnitTests` +- `Umbraco.Tests.Integration` +- `DynamicProxyGenAssembly2` (for mocking) + +**Focus areas**: +- Query parsing (selectors, filters, sorts) +- Member authentication flows +- Output caching behavior +- Protected content access + +--- + +## 5. Security & Access Control + +### Three Access Modes + +```csharp +// Services/ApiAccessService.cs:21-27 +public bool HasPublicAccess() => _deliveryApiSettings.PublicAccess || HasValidApiKey(); +public bool HasPreviewAccess() => HasValidApiKey(); +public bool HasMediaAccess() => _deliveryApiSettings is { PublicAccess: true, Media.PublicAccess: true } || HasValidApiKey(); +``` + +**Access levels**: +1. **Public** - No authentication required (if enabled) +2. **API Key** - Via `Api-Key` header +3. **Preview** - Always requires API key + +### Member Authentication + +OpenIddict-based OAuth 2.0 for member-protected content: + +**Flows supported** (Controllers/Security/MemberController.cs): +- Authorization Code + PKCE (line 53) +- Client Credentials (line 112) +- Refresh Token (line 98) + +**Endpoints**: +- `GET /umbraco/delivery/api/v1/security/member/authorize` +- `POST /umbraco/delivery/api/v1/security/member/token` +- `GET /umbraco/delivery/api/v1/security/member/signout` + +**Scopes**: Only `openid` and `offline_access` allowed for members (line 220-222) + +### Protected Content + +Member access checked via `ProtectedAccess` model: + +```csharp +// Controllers/Content/QueryContentApiController.cs:56-57 +ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync(); +Attempt, ApiContentQueryOperationStatus> queryAttempt = + _apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take); +``` + +--- + +## 6. Output Caching + +### Cache Policy Configuration + +```csharp +// DependencyInjection/UmbracoBuilderExtensions.cs:120-136 +// Content and Media have separate cache durations +options.AddPolicy( + Constants.DeliveryApi.OutputCache.ContentCachePolicy, + new DeliveryApiOutputCachePolicy( + outputCacheSettings.ContentDuration, + new StringValues([AcceptLanguage, AcceptSegment, StartItem]))); +``` + +**Cache invalidation conditions** (Caching/DeliveryApiOutputCachePolicy.cs:31): +```csharp +// Never cache preview or non-public access +context.EnableOutputCaching = requestPreviewService.IsPreview() is false + && apiAccessService.HasPublicAccess(); +``` + +**Vary by headers**: `Accept-Language`, `Accept-Segment`, `Start-Item` + +--- + +## 7. Edge Cases & Known Issues + +### Technical Debt (TODOs in codebase) + +1. **V1 Removal Pending** (4 locations): + - `DependencyInjection/UmbracoBuilderExtensions.cs:98` - FIXME: remove matcher policy + - `Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs:11` - FIXME: remove class + - `Filters/SwaggerDocumentationFilterBase.cs:79,83` - FIXME: remove V1 swagger docs + +2. **Obsolete Reference Warnings** (csproj:9-13): + - `ASP0019` - IHeaderDictionary.Append usage + - `CS0618/CS0612` - Obsolete member references + +### Empty Query Results + +Query service returns empty results (not errors) for invalid options: + +```csharp +// Services/ApiContentQueryService.cs:54-78 +// Invalid selector/filter/sort returns fail status with empty result +return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SelectorOptionNotFound, emptyResult); +``` + +### Start Item Fallback + +When no `fetch` parameter provided, uses start item or all content: + +```csharp +// Services/ApiContentQueryService.cs:99-112 +if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider)) +{ + IPublishedContent? startItem = requestStartItemProvider.GetStartItem(); + // Use descendants of start item +} +return _apiContentQueryProvider.AllContentSelectorOption(); // Fallback to all +``` + +--- + +## 8. Project-Specific Notes + +### V1 vs V2 Differences + +| Feature | V1 | V2 | +|---------|----|----| +| Output expansion | Basic | Tree-based parsing | +| `expand` parameter | Flat list | Nested syntax | +| `fields` parameter | Limited | Full property selection | +| Default expansion strategy | `RequestContextOutputExpansionStrategy` | `RequestContextOutputExpansionStrategyV2` | + +**Migration note**: V1 is deprecated; plan removal when V17+ drops V1 support. + +### JSON Configuration + +Delivery API has its own JSON options (distinct from Management API): + +```csharp +// DependencyInjection/UmbracoBuilderExtensions.cs:82-88 +.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options => +{ + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver(); + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); +``` + +### Member Token Revocation + +Tokens automatically revoked on member changes: + +```csharp +// DependencyInjection/UmbracoBuilderExtensions.cs:93-96 +builder.AddNotificationAsyncHandler(); +builder.AddNotificationAsyncHandler(); +builder.AddNotificationAsyncHandler(); +builder.AddNotificationAsyncHandler(); +``` + +### External Dependencies + +**Examine/Lucene** (via Core): +- Powers content querying +- Selector/Filter/Sort handlers build Lucene queries + +**OpenIddict** (via Api.Common): +- Member OAuth 2.0 authentication +- Reference tokens (not JWT) + +### Configuration (appsettings.json) + +```json +{ + "Umbraco": { + "CMS": { + "DeliveryApi": { + "Enabled": true, + "PublicAccess": true, + "ApiKey": "your-api-key", + "Media": { + "Enabled": true, + "PublicAccess": true + }, + "MemberAuthorization": { + "AuthorizationCodeFlow": { "Enabled": true }, + "ClientCredentialsFlow": { "Enabled": false } + }, + "OutputCache": { + "Enabled": true, + "ContentDuration": "00:01:00", + "MediaDuration": "00:01:00" + } + } + } + } +} +``` + +### API Endpoints Summary + +**Content** (`/umbraco/delivery/api/v2/content`): +- `GET /item/{id}` - Single content by GUID +- `GET /item/{path}` - Single content by route +- `GET /items` - Multiple by IDs +- `GET /` - Query with fetch/filter/sort + +**Media** (`/umbraco/delivery/api/v2/media`): +- `GET /item/{id}` - Single media by GUID +- `GET /item/{path}` - Single media by path +- `GET /items` - Multiple by IDs +- `GET /` - Query media + +**Security** (`/umbraco/delivery/api/v1/security/member`): +- `GET /authorize` - Start OAuth flow +- `POST /token` - Exchange code for token +- `GET /signout` - Revoke session + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build project +dotnet build src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj + +# Pack for NuGet +dotnet pack src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj -c Release + +# Run integration tests +dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery" + +# Check packages +dotnet list src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj package --outdated +``` + +### Key Classes + +| Class | Purpose | File | +|-------|---------|------| +| `DeliveryApiControllerBase` | Base controller with path decoding | Controllers/DeliveryApiControllerBase.cs | +| `ApiContentQueryService` | Query orchestration | Services/ApiContentQueryService.cs | +| `ApiAccessService` | Access control logic | Services/ApiAccessService.cs | +| `DeliveryApiOutputCachePolicy` | Cache policy implementation | Caching/DeliveryApiOutputCachePolicy.cs | +| `MemberController` | OAuth endpoints | Controllers/Security/MemberController.cs | +| `RequestContextOutputExpansionStrategyV2` | V2 output expansion | Rendering/RequestContextOutputExpansionStrategyV2.cs | + +### Important Files + +- `Umbraco.Cms.Api.Delivery.csproj` - Project dependencies +- `DependencyInjection/UmbracoBuilderExtensions.cs` - DI registration (lines 33-141) +- `Configuration/DeliveryApiConfiguration.cs` - API constants +- `Services/ApiContentQueryService.cs` - Query execution + +### Getting Help + +- **Root documentation**: `/CLAUDE.md` - Repository overview +- **API Common patterns**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` +- **Official docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api +- **Media docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api + +--- + +**This library exposes Umbraco content and media via REST for headless scenarios. Focus on query handlers, access control, and member authentication when working here.** diff --git a/src/Umbraco.Cms.DevelopmentMode.Backoffice/CLAUDE.md b/src/Umbraco.Cms.DevelopmentMode.Backoffice/CLAUDE.md new file mode 100644 index 000000000000..cb2da893b4ca --- /dev/null +++ b/src/Umbraco.Cms.DevelopmentMode.Backoffice/CLAUDE.md @@ -0,0 +1,258 @@ +# Umbraco.Cms.DevelopmentMode.Backoffice + +Development-time library enabling **InMemoryAuto** ModelsBuilder mode with runtime Razor view compilation. Allows content type changes to instantly regenerate strongly-typed models without application restart. + +--- + +## 1. Architecture + +**Type**: Class Library (NuGet Package) +**Target Framework**: .NET 10.0 +**Purpose**: Enable hot-reload of ModelsBuilder models during development + +### Key Technologies + +- **Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation** - Runtime Razor view compilation +- **Microsoft.CodeAnalysis.CSharp** (Roslyn) - Runtime C# compilation +- **AssemblyLoadContext** - Collectible assembly loading/unloading + +### Dependencies + +- `Umbraco.Web.Common` - Umbraco web infrastructure + +### Project Structure (18 source files) + +``` +Umbraco.Cms.DevelopmentMode.Backoffice/ +├── DependencyInjection/ +│ ├── BackofficeDevelopmentComposer.cs # Auto-registration via IComposer +│ └── UmbracoBuilderExtensions.cs # DI setup (CRITICAL: contains 80-line design overview) +└── InMemoryAuto/ + ├── InMemoryModelFactory.cs # Core factory - generates/loads models (876 lines) + ├── InMemoryAssemblyLoadContextManager.cs # Manages collectible AssemblyLoadContext + ├── RoslynCompiler.cs # Compiles generated C# to DLL + ├── CollectibleRuntimeViewCompiler.cs # Custom IViewCompiler for Razor (489 lines) + ├── UmbracoViewCompilerProvider.cs # Provides CollectibleRuntimeViewCompiler + ├── RuntimeCompilationCacheBuster.cs # Clears Razor caches on model rebuild + ├── UmbracoRazorReferenceManager.cs # Manages Roslyn MetadataReferences + ├── CompilationOptionsProvider.cs # Mirrors host app compilation settings + ├── UmbracoAssemblyLoadContext.cs # Collectible AssemblyLoadContext wrapper + ├── ChecksumValidator.cs # Validates precompiled view checksums + ├── CompilationExceptionFactory.cs # Creates detailed compilation errors + ├── UmbracoCompilationException.cs # Custom exception with CompilationFailures + ├── ModelsBuilderAssemblyAttribute.cs # Marks InMemory assemblies + ├── ModelsBuilderBindingErrorHandler.cs # Handles model binding version mismatches + ├── InMemoryModelsBuilderModeValidator.cs # Validates runtime mode configuration + └── ModelsModeConstants.cs # "InMemoryAuto" constant +``` + +### Design Patterns + +1. **Clone-and-Own Pattern** - ASP.NET Core's RuntimeViewCompiler and related classes are cloned because internal APIs can't be extended +2. **Collectible AssemblyLoadContext** - Enables assembly unloading for hot-reload +3. **FileSystemWatcher** - Monitors `~/umbraco/Data/TEMP/InMemoryAuto/` for changes +4. **Lazy Initialization** - References resolved on first use, not startup + +--- + +## 2. Key Patterns + +### Why Clone-and-Own (CRITICAL CONTEXT) + +The 80-line comment in `DependencyInjection/UmbracoBuilderExtensions.cs:13-80` explains why this project exists. Key points: + +1. **Problem**: ASP.NET Core's `RuntimeViewCompiler` loads assemblies into the default `AssemblyLoadContext`, which can't reference collectible contexts (breaking change in .NET 7) +2. **Failed Solutions**: + - Reflection to clear caches (unstable, internal APIs) + - Service wrapping via DI (still loads into wrong context) +3. **Solution**: Clone `RuntimeViewCompiler`, `RazorReferenceManager`, `ChecksumValidator`, and related classes, modifying them to: + - Load compiled views into the same collectible `AssemblyLoadContext` as models + - Explicitly add InMemoryAuto models assembly reference during compilation + +### InMemoryModelFactory Lifecycle + +``` +Content Type Changed → Reset() called → _pendingRebuild = true + ↓ +Next View Request → EnsureModels() → GetModelsAssembly(forceRebuild: true) + ↓ + GenerateModelsCode() → RoslynCompiler.CompileToFile() + ↓ + ReloadAssembly() → InMemoryAssemblyLoadContextManager.RenewAssemblyLoadContext() + ↓ + RuntimeCompilationCacheBuster.BustCache() → Views recompile with new models +``` + +### Assembly Caching Strategy + +Models are cached to disk for faster boot (`InMemoryModelFactory.cs:400-585`): + +1. **Hash Check**: `TypeModelHasher.Hash(typeModels)` creates hash of content type definitions +2. **Cache Files** (in `~/umbraco/Data/TEMP/InMemoryAuto/`): + - `models.hash` - Current content type hash + - `models.generated.cs` - Generated source + - `all.generated.cs` - Combined source with assembly attributes + - `all.dll.path` - Path to compiled DLL + - `Compiled/generated.cs{hash}.dll` - Compiled assembly + +3. **Boot Sequence**: + - Check if `models.hash` matches current hash + - If match, load existing DLL + - If mismatch or missing, recompile + +### Collectible AssemblyLoadContext + +```csharp +// InMemoryAssemblyLoadContextManager.cs:29-37 +internal void RenewAssemblyLoadContext() +{ + _currentAssemblyLoadContext?.Unload(); // Unload previous + _currentAssemblyLoadContext = new UmbracoAssemblyLoadContext(); // Create new collectible +} +``` + +**Key constraint**: No external references to the `AssemblyLoadContext` allowed - prevents unloading. + +--- + +## 3. Error Handling + +### Compilation Failures + +`CompilationExceptionFactory.cs` creates `UmbracoCompilationException` with: +- Source file content +- Generated code +- Diagnostic messages with line/column positions + +Errors logged in `CollectibleRuntimeViewCompiler.cs:383-396` and `InMemoryModelFactory.cs:341-354`. + +### Model Binding Errors + +`ModelsBuilderBindingErrorHandler.cs` handles version mismatches between: +- View's model type (from compiled view assembly) +- Content's model type (from InMemoryAuto assembly) + +Reports detailed error messages including assembly versions. + +--- + +## 4. Security + +**Runtime Mode Validation**: `InMemoryModelsBuilderModeValidator.cs` prevents `InMemoryAuto` mode outside `BackofficeDevelopment` runtime mode. + +**Temp File Location**: Models compiled to `~/umbraco/Data/TEMP/InMemoryAuto/` - ensure this directory isn't web-accessible. + +--- + +## 5. Edge Cases & Known Issues + +### Technical Debt (TODOs) + +1. **Circular Reference** - `InMemoryModelFactory.cs:46`: + ```csharp + private readonly Lazy _umbracoServices; // TODO: this is because of circular refs :( + ``` + +2. **DynamicMethod Performance** - `InMemoryModelFactory.cs:698-701`: + ```csharp + // TODO: use Core's ReflectionUtilities.EmitCtor !! + // Yes .. DynamicMethod is uber slow + ``` + +### Race Conditions + +`InMemoryModelFactory.cs:800-809` - FileSystemWatcher can cause race conditions on slow cloud filesystems. Own file changes are always ignored. + +### Unused Assembly Cleanup + +`InMemoryModelFactory.cs:587-612` - `TryDeleteUnusedAssemblies` may fail with `UnauthorizedAccessException` if files are locked. Cleanup retried on next rebuild. + +### Reflection for Cache Clearing + +`RuntimeCompilationCacheBuster.cs:50-51` uses reflection to call internal `RazorViewEngine.ClearCache()`: +```csharp +Action? clearCacheMethod = ReflectionUtilities.EmitMethod>("ClearCache"); +``` + +--- + +## 6. Configuration + +**Enable InMemoryAuto mode** (appsettings.json): +```json +{ + "Umbraco": { + "CMS": { + "Runtime": { + "Mode": "BackofficeDevelopment" + }, + "ModelsBuilder": { + "ModelsMode": "InMemoryAuto" + } + } + } +} +``` + +**Requirements**: +- `RuntimeMode` must be `BackofficeDevelopment` +- `ModelsMode` must be `InMemoryAuto` + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build +dotnet build src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj + +# Pack for NuGet +dotnet pack src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj -c Release + +# Run integration tests +dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~ModelsBuilder" +``` + +**Note**: This library has no direct tests - tested via integration tests in `Umbraco.Tests.Integration`. + +**Focus areas when modifying**: +- Assembly loading/unloading cycles +- Razor view compilation with models +- Cache invalidation timing +- Model binding error scenarios + +### Essential Classes + +| Class | Purpose | File | +|-------|---------|------| +| `InMemoryModelFactory` | Core model generation/loading | `InMemoryAuto/InMemoryModelFactory.cs` | +| `CollectibleRuntimeViewCompiler` | Custom Razor compiler | `InMemoryAuto/CollectibleRuntimeViewCompiler.cs` | +| `InMemoryAssemblyLoadContextManager` | Collectible assembly management | `InMemoryAuto/InMemoryAssemblyLoadContextManager.cs` | +| `RuntimeCompilationCacheBuster` | Cache invalidation | `InMemoryAuto/RuntimeCompilationCacheBuster.cs` | +| `UmbracoBuilderExtensions` | DI setup + design rationale | `DependencyInjection/UmbracoBuilderExtensions.cs` | + +### Important Files + +- **Design Overview**: `DependencyInjection/UmbracoBuilderExtensions.cs:13-80` - READ THIS FIRST +- **Project File**: `Umbraco.Cms.DevelopmentMode.Backoffice.csproj` +- **Model Factory**: `InMemoryAuto/InMemoryModelFactory.cs` - Most complex class + +### Cloned Microsoft Code + +These files are clones of ASP.NET Core internals - check upstream for updates: +- `ChecksumValidator.cs` - https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/ChecksumValidator.cs +- `UmbracoRazorReferenceManager.cs` - https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs +- `CompilationOptionsProvider.cs` - Partial clone of CSharpCompiler +- `CompilationExceptionFactory.cs` - Partial clone of CompilationFailedExceptionFactory + +### Getting Help + +- **Root Documentation**: `/CLAUDE.md` +- **Core Patterns**: `/src/Umbraco.Core/CLAUDE.md` +- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/configuration/modelsbuildersettings + +--- + +**This library enables hot-reload of content models during development. The core complexity is working around ASP.NET Core's internal Razor compilation APIs. Always read the design overview in `UmbracoBuilderExtensions.cs:13-80` before making changes.** diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md b/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md new file mode 100644 index 000000000000..5ab8580907f2 --- /dev/null +++ b/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md @@ -0,0 +1,234 @@ +# Umbraco.Cms.Imaging.ImageSharp + +Image processing library using **ImageSharp 3.x** and **ImageSharp.Web** for on-the-fly image manipulation, resizing, cropping, and caching. + +--- + +## 1. Architecture + +**Type**: Class Library (NuGet Package) +**Target Framework**: .NET 10.0 +**Purpose**: Provide image manipulation via query string parameters + +### Key Technologies + +- **SixLabors.ImageSharp** - Image processing library +- **SixLabors.ImageSharp.Web** - ASP.NET Core middleware for query string-based image manipulation + +### Dependencies + +- `Umbraco.Web.Common` - Web infrastructure + +### Project Structure (7 source files) + +``` +Umbraco.Cms.Imaging.ImageSharp/ +├── ImageSharpComposer.cs # Auto-registration via IComposer +├── UmbracoBuilderExtensions.cs # DI setup and middleware configuration +├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, HMAC, size limits) +├── ConfigurePhysicalFileSystemCacheOptions.cs # File cache location +├── ImageProcessors/ +│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness +└── Media/ + ├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware) + └── ImageSharpImageUrlGenerator.cs # Generate query string URLs for processing +``` + +### Relationship to ImageSharp2 + +Two imaging packages exist: +- **Umbraco.Cms.Imaging.ImageSharp** (this package) - Uses ImageSharp 3.x (default) +- **Umbraco.Cms.Imaging.ImageSharp2** - Uses ImageSharp 2.x for backwards compatibility + +**Key difference**: ImageSharp 3.x WebP encoder defaults to Lossless (10x larger files), so this package explicitly sets `WebpFileFormatType.Lossy` at `ConfigureImageSharpMiddlewareOptions.cs:108-115`. + +--- + +## 2. Key Patterns + +### Query String Image Processing + +Images are processed via URL query parameters handled by ImageSharp.Web middleware: + +| Parameter | Purpose | Example | +|-----------|---------|---------| +| `width` / `height` | Resize dimensions | `?width=800&height=600` | +| `mode` | Crop mode (pad, crop, stretch, etc.) | `?mode=crop` | +| `anchor` | Crop anchor position | `?anchor=center` | +| `cc` | Crop coordinates (custom) | `?cc=0.1,0.1,0.1,0.1` | +| `rxy` | Focal point | `?rxy=0.5,0.3` | +| `format` | Output format | `?format=webp` | +| `quality` | Compression quality | `?quality=80` | + +### Pipeline Integration + +ImageSharp middleware runs **before** static files in `UmbracoBuilderExtensions.cs:44-50`: +```csharp +options.AddFilter(new UmbracoPipelineFilter(nameof(ImageSharpComposer)) +{ + PrePipeline = prePipeline => prePipeline.UseImageSharp() +}); +``` + +This ensures query strings are processed before serving static files. + +### EXIF Orientation Handling + +Both `ImageSharpDimensionExtractor` and `CropWebProcessor` account for EXIF rotation: + +```csharp +// ImageSharpDimensionExtractor.cs:42-44 - Swap width/height for rotated images +size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); +``` + +```csharp +// CropWebProcessor.cs:64-65 - Transform crop coordinates for EXIF orientation +Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation); +``` + +### HMAC Request Authorization + +When `HMACSecretKey` is configured, URLs are signed to prevent abuse (`ImageSharpImageUrlGenerator.cs:121-131`): +```csharp +if (_options.HMACSecretKey.Length != 0 && _requestAuthorizationUtilities is not null) +{ + var token = _requestAuthorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize); + queryString.Add(RequestAuthorizationUtilities.TokenCommand, token); +} +``` + +--- + +## 3. Configuration + +### ImagingSettings (appsettings.json) + +```json +{ + "Umbraco": { + "CMS": { + "Imaging": { + "HMACSecretKey": "", + "Cache": { + "BrowserMaxAge": "7.00:00:00", + "CacheMaxAge": "365.00:00:00", + "CacheHashLength": 12, + "CacheFolder": "~/umbraco/Data/TEMP/MediaCache", + "CacheFolderDepth": 8 + }, + "Resize": { + "MaxWidth": 5000, + "MaxHeight": 5000 + } + } + } + } +} +``` + +### Security: Size Limits Without HMAC + +When HMAC is not configured, `ConfigureImageSharpMiddlewareOptions.cs:46-83` enforces max dimensions: +- Width/height requests exceeding `MaxWidth`/`MaxHeight` are **stripped** from the query +- This prevents DoS via excessive image generation + +When HMAC **is** configured, size validation is skipped (trusted requests). + +### Cache Busting + +Query parameters `rnd` or `v` trigger immutable cache headers (`ConfigureImageSharpMiddlewareOptions.cs:86-106`): +- Disables `MustRevalidate` +- Adds `immutable` directive + +--- + +## 4. Core Interfaces Implemented + +| Interface | Implementation | Purpose | +|-----------|----------------|---------| +| `IImageDimensionExtractor` | `ImageSharpDimensionExtractor` | Extract width/height from streams | +| `IImageUrlGenerator` | `ImageSharpImageUrlGenerator` | Generate manipulation URLs | +| `IImageWebProcessor` | `CropWebProcessor` | Custom crop with `cc` parameter | + +--- + +## 5. Edge Cases + +### WebP Encoding Change (ImageSharp 3.x) + +`ConfigureImageSharpMiddlewareOptions.cs:108-115` - ImageSharp 3.x defaults WebP to Lossless for PNGs, creating ~10x larger files. This is overridden: +```csharp +options.Configuration.ImageFormatsManager.SetEncoder( + WebpFormat.Instance, + new WebpEncoder { FileFormat = WebpFileFormatType.Lossy }); +``` + +### Crop Coordinates Format + +`CropWebProcessor.cs:50-56` - The `cc` parameter expects 4 values as distances from edges: +- Format: `left,top,right,bottom` (0-1 range, percentages) +- Right/bottom values are **distance from** those edges, not coordinates +- Zero values (`0,0,0,0`) are ignored (no crop) + +### Supported File Types + +Dynamically determined from ImageSharp configuration (`ImageSharpDimensionExtractor.cs:24`): +```csharp +SupportedImageFileTypes = configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(); +``` + +Default includes: jpg, jpeg, png, gif, bmp, webp, tiff, etc. + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build +dotnet build src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj + +# Run tests +dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp" +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `UmbracoBuilderExtensions.cs` | DI registration and pipeline setup | +| `ConfigureImageSharpMiddlewareOptions.cs` | Middleware config (caching, HMAC, size limits) | +| `CropWebProcessor.cs` | Custom `cc` crop parameter | +| `ImageSharpImageUrlGenerator.cs` | URL generation with HMAC signing | + +### URL Examples + +``` +# Basic resize +/media/image.jpg?width=800 + +# Crop to aspect ratio with focal point +/media/image.jpg?width=800&height=600&mode=crop&rxy=0.5,0.3 + +# Custom crop coordinates (10% from each edge) +/media/image.jpg?cc=0.1,0.1,0.1,0.1 + +# Format conversion with quality +/media/image.jpg?format=webp&quality=80 + +# Cache busted (immutable headers) +/media/image.jpg?width=800&v=abc123 +``` + +### Getting Help + +- **Root Documentation**: `/CLAUDE.md` +- **ImageSharp Docs**: https://docs.sixlabors.com/ +- **Umbraco Imaging**: https://docs.umbraco.com/umbraco-cms/reference/configuration/imagingsettings + +--- + +**This library provides query string-based image processing. Key concerns are EXIF orientation handling, WebP encoding defaults, and HMAC security for public-facing sites.** diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md b/src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md new file mode 100644 index 000000000000..b46b278e1610 --- /dev/null +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/CLAUDE.md @@ -0,0 +1,139 @@ +# Umbraco.Cms.Imaging.ImageSharp2 + +Image processing library using **ImageSharp 2.x** for backwards compatibility with existing deployments. Use this package only when migrating from older Umbraco versions that depend on ImageSharp 2.x behavior. + +**Namespace Note**: Uses `Umbraco.Cms.Imaging.ImageSharp` (same as v3 package) for drop-in replacement - no code changes needed when switching. + +--- + +## 1. Architecture + +**Type**: Class Library (NuGet Package) +**Target Framework**: .NET 10.0 +**Purpose**: ImageSharp 2.x compatibility layer + +### Package Versions (Pinned) + +```xml + + + +``` + +Version constraint `[2.1.11, 3)` means: minimum 2.1.11, below 3.0. + +### Project Structure (7 source files) + +``` +Umbraco.Cms.Imaging.ImageSharp2/ +├── ImageSharpComposer.cs # Auto-registration via IComposer +├── UmbracoBuilderExtensions.cs # DI setup and middleware configuration +├── ConfigureImageSharpMiddlewareOptions.cs # Middleware options (caching, size limits) +├── ConfigurePhysicalFileSystemCacheOptions.cs # File cache location +├── ImageProcessors/ +│ └── CropWebProcessor.cs # Custom crop processor with EXIF awareness +└── Media/ + ├── ImageSharpDimensionExtractor.cs # Extract image dimensions (EXIF-aware) + └── ImageSharpImageUrlGenerator.cs # Generate query string URLs +``` + +--- + +## 2. Key Differences from ImageSharp (3.x) + +| Feature | ImageSharp2 (this) | ImageSharp (3.x) | +|---------|-------------------|------------------| +| **Package version** | 2.1.11 - 2.x | 3.x+ | +| **HMAC signing** | Not supported | Supported | +| **WebP default** | Lossy (native) | Lossless (overridden to Lossy) | +| **Cache buster param** | `rnd` only | `rnd` or `v` | +| **API differences** | `Image.Identify(config, stream)` | `Image.Identify(options, stream)` | +| **Size property** | `image.Image.Size()` method | `image.Image.Size` property | + +### API Differences in Code + +**ImageSharpDimensionExtractor** (`Media/ImageSharpDimensionExtractor.cs:31`): +```csharp +// v2: Direct method call +IImageInfo imageInfo = Image.Identify(_configuration, stream); + +// v3: Uses DecoderOptions +ImageInfo imageInfo = Image.Identify(options, stream); +``` + +**CropWebProcessor** (`ImageProcessors/CropWebProcessor.cs:67`): +```csharp +// v2: Size is a method +Size size = image.Image.Size(); + +// v3: Size is a property +Size size = image.Image.Size; +``` + +### Missing Features (vs ImageSharp 3.x) + +1. **No HMAC request authorization** - `HMACSecretKey` setting is ignored +2. **No `v` cache buster** - Only `rnd` parameter triggers immutable headers +3. **No WebP encoder override** - Uses default Lossy encoding (no configuration needed) + +--- + +## 3. When to Use This Package + +**Use ImageSharp2 when:** +- Migrating from Umbraco versions that used ImageSharp 2.x +- Third-party packages have hard dependency on ImageSharp 2.x +- Need exact byte-for-byte output compatibility with existing cached images + +**Use ImageSharp (3.x) when:** +- New installations +- Need HMAC URL signing for security +- Want latest performance improvements + +--- + +## 4. Configuration + +Same as ImageSharp 3.x. See `/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md` → Section 3 for full configuration details. + +**Key difference**: `HMACSecretKey` setting exists but is **ignored** in this package (no HMAC support in v2). + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Build +dotnet build src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj + +# Run tests +dotnet test tests/Umbraco.Tests.UnitTests/ --filter "FullyQualifiedName~ImageSharp" +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `Umbraco.Cms.Imaging.ImageSharp2.csproj` | Version constraints (lines 7-8) | +| `ConfigureImageSharpMiddlewareOptions.cs` | Size limit enforcement | +| `Media/ImageSharpImageUrlGenerator.cs` | URL generation (no HMAC) | + +### Switching Between Packages + +To switch from ImageSharp2 to ImageSharp (3.x): +1. Remove `Umbraco.Cms.Imaging.ImageSharp2` package reference +2. Add `Umbraco.Cms.Imaging.ImageSharp` package reference +3. Clear media cache folder (`~/umbraco/Data/TEMP/MediaCache`) +4. No code changes needed (same namespace) + +### Getting Help + +- **ImageSharp 3.x Documentation**: `/src/Umbraco.Cms.Imaging.ImageSharp/CLAUDE.md` +- **Root Documentation**: `/CLAUDE.md` +- **SixLabors ImageSharp 2.x Docs**: https://docs.sixlabors.com/ + +--- + +**This is a backwards-compatibility package. For new projects, use `Umbraco.Cms.Imaging.ImageSharp` (3.x) instead.** diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/CLAUDE.md b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/CLAUDE.md new file mode 100644 index 000000000000..3708c3a4020e --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/CLAUDE.md @@ -0,0 +1,132 @@ +# Umbraco.Cms.Persistence.EFCore.SqlServer + +SQL Server-specific EF Core provider for Umbraco CMS. Contains SQL Server migrations and provider setup for the EF Core persistence layer. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Dependencies**: Umbraco.Cms.Persistence.EFCore + +--- + +## 1. Architecture + +### Project Purpose + +This is a thin provider project that implements SQL Server-specific functionality for the EF Core persistence layer: + +1. **Migration Provider** - Executes SQL Server-specific migrations +2. **Migration Provider Setup** - Configures DbContext to use SQL Server +3. **Migrations** - SQL Server-specific migration files for OpenIddict tables + +### Folder Structure + +``` +Umbraco.Cms.Persistence.EFCore.SqlServer/ +├── Migrations/ +│ ├── 20230622184303_InitialCreate.cs # No-op (NPoco creates tables) +│ ├── 20230807654321_AddOpenIddict.cs # OpenIddict tables +│ ├── 20240403140654_UpdateOpenIddictToV5.cs # OpenIddict v5 schema changes +│ ├── 20251006140751_UpdateOpenIddictToV7.cs # Token Type column expansion +│ └── UmbracoDbContextModelSnapshot.cs # Current model state +├── EFCoreSqlServerComposer.cs # DI registration +├── SqlServerMigrationProvider.cs # IMigrationProvider impl +└── SqlServerMigrationProviderSetup.cs # IMigrationProviderSetup impl +``` + +### Relationship with Parent Project + +This project extends `Umbraco.Cms.Persistence.EFCore`: + +- Implements `IMigrationProvider` interface defined in parent +- Implements `IMigrationProviderSetup` interface defined in parent +- Uses `UmbracoDbContext` from parent project +- Provider name: `Microsoft.Data.SqlClient` (from `Constants.ProviderNames.SQLServer`) + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). +**For EF Core migration commands**, see [parent project](../Umbraco.Cms.Persistence.EFCore/CLAUDE.md) (substitute `-p src/Umbraco.Cms.Persistence.EFCore.SqlServer`). + +--- + +## 3. Key Components + +### EFCoreSqlServerComposer (line 10-14) + +Registers SQL Server implementations of `IMigrationProvider` and `IMigrationProviderSetup`. + +### SqlServerMigrationProvider + +Executes migrations via `MigrateAsync(EFCoreMigration)` or `MigrateAllAsync()`. Unlike SQLite sibling, no transaction check needed (SQL Server handles concurrent migrations natively). + +### SqlServerMigrationProviderSetup (line 11-14) + +Configures `DbContextOptionsBuilder` with `UseSqlServer` and migrations assembly. + +--- + +## 4. Migrations + +### Migration History + +| Migration | Date | Purpose | +|-----------|------|---------| +| `InitialCreate` | 2023-06-22 | No-op - NPoco creates base tables | +| `AddOpenIddict` | 2023-08-07 | Creates OpenIddict tables | +| `UpdateOpenIddictToV5` | 2024-04-03 | Renames Type→ClientType, adds ApplicationType/JsonWebKeySet/Settings | +| `UpdateOpenIddictToV7` | 2025-10-06 | Expands Token.Type from nvarchar(50) to nvarchar(150) | + +### OpenIddict Tables Created + +Prefixed with `umbraco`: Applications, Authorizations, Scopes, Tokens (see SQLite sibling for details). + +### SQL Server-Specific Differences from SQLite + +1. **nvarchar types** - Uses `nvarchar(n)` and `nvarchar(max)` instead of TEXT +2. **v5 migration has actual changes** - Column rename and additions (SQLite handled differently) +3. **v7 migration has schema change** - Token.Type column expanded (SQLite didn't need this) + +### Adding New Migrations + +1. Configure SQL Server connection string in `src/Umbraco.Web.UI/appsettings.json` +2. Run migration command from repository root (see parent project docs) +3. **Critical**: Also add equivalent migration to `Umbraco.Cms.Persistence.EFCore.Sqlite` +4. Update `SqlServerMigrationProvider.GetMigrationType()` switch (line 27-35) if adding named migrations + +--- + +## 5. Project-Specific Notes + +### Named Migration Mapping + +`SqlServerMigrationProvider.GetMigrationType()` (line 27-35) maps `EFCoreMigration` enum to migration types. Update both parent enum and this switch when adding named migrations. + +### Transaction Handling + +SQL Server handles concurrent migrations natively - no transaction check needed (unlike SQLite). + +### Auto-Generated Files + +`UmbracoDbContextModelSnapshot.cs` is regenerated by EF Core - do not edit manually. + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `SqlServerMigrationProvider.cs` | Migration execution | +| `SqlServerMigrationProviderSetup.cs` | DbContext configuration | +| `EFCoreSqlServerComposer.cs` | DI registration | +| `Migrations/*.cs` | Migration files | + +### Provider Name +`Constants.ProviderNames.SQLServer` = `"Microsoft.Data.SqlClient"` + +### Related Projects +- **Parent**: `Umbraco.Cms.Persistence.EFCore` - Interfaces and base DbContext +- **Sibling**: `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite equivalent diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/CLAUDE.md b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/CLAUDE.md new file mode 100644 index 000000000000..7d8936eba9e7 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/CLAUDE.md @@ -0,0 +1,134 @@ +# Umbraco.Cms.Persistence.EFCore.Sqlite + +SQLite-specific EF Core provider for Umbraco CMS. Contains SQLite migrations and provider setup for the EF Core persistence layer. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Dependencies**: Umbraco.Cms.Persistence.EFCore + +--- + +## 1. Architecture + +### Project Purpose + +This is a thin provider project that implements SQLite-specific functionality for the EF Core persistence layer: + +1. **Migration Provider** - Executes SQLite-specific migrations +2. **Migration Provider Setup** - Configures DbContext to use SQLite +3. **Migrations** - SQLite-specific migration files for OpenIddict tables + +### Folder Structure + +``` +Umbraco.Cms.Persistence.EFCore.Sqlite/ +├── Migrations/ +│ ├── 20230622183638_InitialCreate.cs # No-op (NPoco creates tables) +│ ├── 20230807123456_AddOpenIddict.cs # OpenIddict tables +│ ├── 20240403141051_UpdateOpenIddictToV5.cs # OpenIddict v5 schema +│ ├── 20251006140958_UpdateOpenIddictToV7.cs # OpenIddict v7 (no-op for SQLite) +│ └── UmbracoDbContextModelSnapshot.cs # Current model state +├── EFCoreSqliteComposer.cs # DI registration +├── SqliteMigrationProvider.cs # IMigrationProvider impl +└── SqliteMigrationProviderSetup.cs # IMigrationProviderSetup impl +``` + +### Relationship with Parent Project + +This project extends `Umbraco.Cms.Persistence.EFCore`: + +- Implements `IMigrationProvider` interface defined in parent +- Implements `IMigrationProviderSetup` interface defined in parent +- Uses `UmbracoDbContext` from parent project +- Provider name: `Microsoft.Data.Sqlite` (from `Constants.ProviderNames.SQLLite`) + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). +**For EF Core migration commands**, see [parent project](../Umbraco.Cms.Persistence.EFCore/CLAUDE.md) (substitute `-p src/Umbraco.Cms.Persistence.EFCore.Sqlite`). + +--- + +## 3. Key Components + +### EFCoreSqliteComposer (line 10-14) + +Registers `IMigrationProvider` and `IMigrationProviderSetup` for SQLite. + +### SqliteMigrationProvider + +- `MigrateAsync(EFCoreMigration)` - Runs specific named migration +- `MigrateAllAsync()` - Runs all pending migrations +- **Critical**: Cannot run `MigrateAllAsync` when transaction is active (line 26-29) + +### SqliteMigrationProviderSetup (line 11-14) + +Configures `DbContextOptionsBuilder` with `UseSqlite` and migrations assembly. + +--- + +## 4. Migrations + +### Migration History + +| Migration | Date | Purpose | +|-----------|------|---------| +| `InitialCreate` | 2023-06-22 | No-op - NPoco creates base tables | +| `AddOpenIddict` | 2023-08-07 | Creates OpenIddict tables (Applications, Tokens, Authorizations, Scopes) | +| `UpdateOpenIddictToV5` | 2024-04-03 | Schema updates for OpenIddict v5 | +| `UpdateOpenIddictToV7` | 2025-10-06 | No-op for SQLite (no schema changes needed) | + +### OpenIddict Tables Created + +All tables prefixed with `umbraco`: +- `umbracoOpenIddictApplications` - OAuth client applications +- `umbracoOpenIddictAuthorizations` - User authorizations +- `umbracoOpenIddictScopes` - OAuth scopes +- `umbracoOpenIddictTokens` - Access/refresh tokens + +### SQLite-Specific Notes + +- **TEXT column type** - SQLite uses TEXT for all strings (no varchar) +- **InitialCreate is no-op** - NPoco creates base tables, EF Core manages OpenIddict only +- **v7 migration is no-op** - SQLite didn't require schema changes that SQL Server needed + +### Adding New Migrations + +1. Configure SQLite connection string in `src/Umbraco.Web.UI/appsettings.json` +2. Run migration command from repository root (see parent project docs) +3. **Critical**: Also add equivalent migration to `Umbraco.Cms.Persistence.EFCore.SqlServer` +4. Update `SqliteMigrationProvider.GetMigrationType()` switch (line 34-42) if adding named migrations + +--- + +## 5. Project-Specific Notes + +### Named Migration Mapping + +`SqliteMigrationProvider.GetMigrationType()` (line 34-42) maps `EFCoreMigration` enum to migration types. When adding named migrations, update both the parent project's enum and this switch. + +### Auto-Generated Files + +`UmbracoDbContextModelSnapshot.cs` is regenerated by EF Core - do not edit manually. + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `SqliteMigrationProvider.cs` | Migration execution | +| `SqliteMigrationProviderSetup.cs` | DbContext configuration | +| `EFCoreSqliteComposer.cs` | DI registration | +| `Migrations/*.cs` | Migration files | + +### Provider Name +`Constants.ProviderNames.SQLLite` = `"Microsoft.Data.Sqlite"` + +### Related Projects +- **Parent**: `Umbraco.Cms.Persistence.EFCore` - Interfaces and base DbContext +- **Sibling**: `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server equivalent diff --git a/src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md b/src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md new file mode 100644 index 000000000000..844a46b3c071 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore/CLAUDE.md @@ -0,0 +1,255 @@ +# Umbraco.Cms.Persistence.EFCore + +Entity Framework Core persistence layer for Umbraco CMS. This project provides EF Core integration including scoping, distributed locking, and migration infrastructure. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Dependencies**: Umbraco.Core, Umbraco.Infrastructure + +--- + +## 1. Architecture + +### Project Purpose + +This project bridges EF Core with Umbraco's existing persistence infrastructure. It provides: + +1. **UmbracoDbContext** - Base DbContext with automatic table prefix (`umbraco`) and provider-based configuration +2. **EF Core Scoping** - Integration with Umbraco's Unit of Work pattern (scopes) +3. **Distributed Locking** - SQL Server and SQLite locking mechanisms for EF Core contexts +4. **Migration Infrastructure** - Provider-agnostic migration execution + +### Folder Structure + +``` +Umbraco.Cms.Persistence.EFCore/ +├── Composition/ +│ └── UmbracoEFCoreComposer.cs # DI registration, OpenIddict setup +├── Extensions/ +│ ├── DbContextExtensions.cs # ExecuteScalarAsync, MigrateDatabaseAsync +│ └── UmbracoEFCoreServiceCollectionExtensions.cs # AddUmbracoDbContext +├── Locking/ +│ ├── SqlServerEFCoreDistributedLockingMechanism.cs +│ └── SqliteEFCoreDistributedLockingMechanism.cs +├── Migrations/ +│ ├── IMigrationProvider.cs # Provider-specific migration execution +│ └── IMigrationProviderSetup.cs # DbContext options setup per provider +├── Scoping/ +│ ├── AmbientEFCoreScopeStack.cs # AsyncLocal scope stack +│ ├── EFCoreScope.cs # Main scope implementation +│ ├── EFCoreDetachableScope.cs # Detachable scope for Deploy +│ ├── EFCoreScopeProvider.cs # Scope factory +│ ├── EFCoreScopeAccessor.cs # Ambient scope accessor +│ └── I*.cs # Interfaces +├── Constants-ProviderNames.cs # SQLite/SQLServer provider constants +├── EfCoreMigrationExecutor.cs # Migration orchestrator +├── StringExtensions.cs # Provider name comparison +└── UmbracoDbContext.cs # Base DbContext +``` + +### Key Design Decisions + +1. **Generic DbContext Support** - All scoping/locking is generic (``) allowing custom contexts +2. **Parallel NPoco Coexistence** - Designed to work alongside existing NPoco persistence layer +3. **Provider Abstraction** - `IMigrationProvider` and `IMigrationProviderSetup` enable database-agnostic migrations +4. **OpenIddict Integration** - UmbracoDbContext automatically registers OpenIddict entity sets + +### Database Providers + +Provider-specific implementations live in separate projects: +- `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server provider and migrations +- `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite provider and migrations + +Provider names (from `Constants.ProviderNames`): +- `Microsoft.Data.SqlClient` - SQL Server +- `Microsoft.Data.Sqlite` - SQLite + +--- + +## 2. Commands + +**For Git workflow and standard build commands**, see [repository root](../../CLAUDE.md). + +### EF Core Migrations + +**Important**: Run from repository root with valid connection string in `src/Umbraco.Web.UI/appsettings.json`. + +```bash +# Add migration (SQL Server) +dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext + +# Add migration (SQLite) +dotnet ef migrations add %MigrationName% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext + +# Remove last migration (SQL Server) +dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer + +# Remove last migration (SQLite) +dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite + +# Generate migration script +dotnet ef migrations script -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer +``` + +--- + +## 3. Key Patterns + +### Adding a Custom DbContext + +Use `AddUmbracoDbContext` to register with full scope integration: + +```csharp +services.AddUmbracoDbContext((provider, options, connectionString, providerName) => +{ + options.UseOpenIddict(); // Optional +}); +``` + +Registers: `IDbContextFactory`, scope providers, accessors, and distributed locking + +### Using EF Core Scopes + +```csharp +using IEfCoreScope scope = _scopeProvider.CreateScope(); + +await scope.ExecuteWithContextAsync(async dbContext => +{ + return await dbContext.MyEntities.FirstOrDefaultAsync(x => x.Id == id); +}); + +scope.Complete(); // Required to commit +``` + +### UmbracoDbContext Table Naming + +All tables auto-prefixed with `umbraco` (see `UmbracoDbContext.cs:85-88`). + +--- + +## 4. Scoping System + +### Scope Hierarchy + +``` +EFCoreScopeProvider + ├── Creates EFCoreScope (normal scope) + └── Creates EFCoreDetachableScope (for Deploy scenarios) + +EFCoreScope + ├── Manages DbContext lifecycle + ├── Handles transactions (BeginTransaction/Commit/Rollback) + ├── Integrates with parent NPoco IScope when nested + └── Manages distributed locks via Locks property +``` + +### Ambient Scope Stack + +Uses `AsyncLocal>` for thread-safe tracking. Scopes must be disposed in LIFO order (child before parent). + +### Transaction Integration + +When an EF Core scope is created inside an NPoco scope: +1. The EF Core scope reuses the parent's `DbTransaction` +2. Transaction commit/rollback is delegated to the parent scope +3. DbContext connects to the same connection as the parent + +See `EFCoreScope.cs:158-180` for transaction initialization logic. + +--- + +## 5. Distributed Locking + +### SQL Server Locking + +Table-level locks with `REPEATABLEREAD` hint. Timeout via `SET LOCK_TIMEOUT`. Requires `ReadCommitted` or higher. + +### SQLite Locking + +Database-level locking (SQLite limitation). Read locks use snapshot isolation (WAL mode). Write locks are exclusive. Handles `SQLITE_BUSY/LOCKED` errors. + +### Lock Requirements + +Requires active scope, transaction, and minimum `ReadCommitted` isolation. + +--- + +## 6. Migration System + +### How Migrations Execute + +1. `UmbracoEFCoreComposer` registers `EfCoreMigrationExecutor` +2. Notification handler triggers on database creation +3. `EfCoreMigrationExecutor` finds correct `IMigrationProvider` by provider name +4. Provider executes via `dbContext.Database.Migrate()` + +### Adding New Migrations + +Create equivalent migrations in both SqlServer and Sqlite provider projects using commands from section 2. + +--- + +## 7. Project-Specific Notes + +### Known Technical Debt + +**Warning Suppressions** (`.csproj:9-14`): `IDE0270` (null checks), `CS0108` (hiding members), `CS1998` (async/await). + +**Detachable Scope** (`EFCoreDetachableScope.cs:93-94`): TODO for Deploy integration, limited test coverage. + +### Circular Dependency Handling + +`SqlServerEFCoreDistributedLockingMechanism` uses `Lazy>` to break circular dependency. + +### StaticServiceProvider Usage + +Fallback for design-time EF tooling (migrations) and startup edge cases when DI unavailable. + +### OpenIddict Integration + +`UmbracoEFCoreComposer` configures OpenIddict via `options.UseOpenIddict()` (see `Composition/UmbracoEFCoreComposer.cs:22-36`). + +### Vulnerable Dependency Override + +Top-level dependency on `Microsoft.Extensions.Caching.Memory` overrides vulnerable EF Core transitive dependency (`.csproj:18-19`). + +### InternalsVisibleTo + +`Umbraco.Tests.Integration` has access to internal types. + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `UmbracoDbContext.cs` | Base DbContext with table prefixing | +| `EFCoreScope.cs` | Main scope implementation | +| `EFCoreScopeProvider.cs` | Scope factory | +| `UmbracoEFCoreServiceCollectionExtensions.cs` | `AddUmbracoDbContext` extension | +| `UmbracoEFCoreComposer.cs` | DI registration | + +### Key Interfaces + +| Interface | Purpose | +|-----------|---------| +| `IEfCoreScope` | Scope contract with `ExecuteWithContextAsync` | +| `IEFCoreScopeProvider` | Creates/manages scopes | +| `IEFCoreScopeAccessor` | Access ambient scope | +| `IMigrationProvider` | Provider-specific migration execution | +| `IMigrationProviderSetup` | DbContext options setup per provider | + +### Provider Constants + +```csharp +Constants.ProviderNames.SQLServer = "Microsoft.Data.SqlClient" +Constants.ProviderNames.SQLLite = "Microsoft.Data.Sqlite" +``` + +### Related Projects + +- `Umbraco.Cms.Persistence.EFCore.SqlServer` - SQL Server implementation +- `Umbraco.Cms.Persistence.EFCore.Sqlite` - SQLite implementation +- `Umbraco.Infrastructure` - Contains NPoco scoping that this integrates with diff --git a/src/Umbraco.Cms.Persistence.SqlServer/CLAUDE.md b/src/Umbraco.Cms.Persistence.SqlServer/CLAUDE.md new file mode 100644 index 000000000000..4514899379ee --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/CLAUDE.md @@ -0,0 +1,235 @@ +# Umbraco.Cms.Persistence.SqlServer + +SQL Server database provider for Umbraco CMS using NPoco ORM. Provides SQL Server-specific SQL syntax, bulk copy operations, distributed locking, and Azure SQL transient fault handling. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Dependencies**: Umbraco.Infrastructure, NPoco.SqlServer, Microsoft.Data.SqlClient + +**Note**: This is the **legacy NPoco-based** SQL Server provider. For EF Core SQL Server support, see `Umbraco.Cms.Persistence.EFCore.SqlServer`. + +--- + +## 1. Architecture + +### Project Purpose + +This project provides complete SQL Server support for Umbraco's NPoco-based persistence layer: + +1. **SQL Syntax Provider** - SQL Server-specific SQL generation and schema operations +2. **Bulk Insert** - True `SqlBulkCopy` for high-performance batch inserts +3. **Distributed Locking** - Row-level locking with `REPEATABLEREAD` hints +4. **Fault Handling** - Azure SQL and network transient error retry policies +5. **LocalDB Support** - SQL Server LocalDB for development + +### Folder Structure + +``` +Umbraco.Cms.Persistence.SqlServer/ +├── Dtos/ +│ ├── ColumnInSchemaDto.cs # Schema query results +│ ├── ConstraintPerColumnDto.cs +│ ├── ConstraintPerTableDto.cs +│ ├── DefaultConstraintPerColumnDto.cs +│ └── DefinedIndexDto.cs +├── FaultHandling/ +│ ├── RetryPolicyFactory.cs # Creates retry policies +│ └── Strategies/ +│ ├── NetworkConnectivityErrorDetectionStrategy.cs +│ ├── SqlAzureTransientErrorDetectionStrategy.cs # Azure SQL error codes +│ └── ThrottlingCondition.cs # Azure throttling decode +├── Interceptors/ +│ ├── SqlServerAddMiniProfilerInterceptor.cs +│ ├── SqlServerAddRetryPolicyInterceptor.cs +│ └── SqlServerConnectionInterceptor.cs # Base interceptor +├── Services/ +│ ├── BulkDataReader.cs # Base bulk reader +│ ├── MicrosoftSqlSyntaxProviderBase.cs # Shared SQL syntax +│ ├── PocoDataDataReader.cs # NPoco bulk reader +│ ├── SqlServerBulkSqlInsertProvider.cs # SqlBulkCopy wrapper +│ ├── SqlServerDatabaseCreator.cs +│ ├── SqlServerDistributedLockingMechanism.cs +│ ├── SqlServerSpecificMapperFactory.cs +│ ├── SqlServerSyntaxProvider.cs # Main syntax provider +│ ├── SqlAzureDatabaseProviderMetadata.cs +│ ├── SqlLocalDbDatabaseProviderMetadata.cs +│ └── SqlServerDatabaseProviderMetadata.cs +├── Constants.cs # Provider name +├── LocalDb.cs # LocalDB management (~1,100 lines) +├── NPocoSqlServerDatabaseExtensions.cs # NPoco bulk extensions config +├── SqlServerComposer.cs # DI auto-registration +└── UmbracoBuilderExtensions.cs # AddUmbracoSqlServerSupport() +``` + +### Auto-Registration + +When this assembly is referenced, `SqlServerComposer` automatically registers all SQL Server services via `AddUmbracoSqlServerSupport()`. + +### Provider Name Migration + +Auto-migrates legacy `System.Data.SqlClient` to `Microsoft.Data.SqlClient`: + +**Line 53-59**: `UmbracoBuilderExtensions.cs` updates connection string provider name +```csharp +if (options.ProviderName == "System.Data.SqlClient") +{ + options.ProviderName = Constants.ProviderName; // Microsoft.Data.SqlClient +} +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### SqlServerDistributedLockingMechanism (Services/SqlServerDistributedLockingMechanism.cs) + +Row-level locking using `REPEATABLEREAD` table hints: + +- **Read locks** (line 147): `SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id` +- **Write locks** (line 182-183): `UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = ... WHERE id=@id` +- **Timeout**: Uses `SET LOCK_TIMEOUT {milliseconds}` (lines 149, 185) +- **Error handling**: Catches SQL error 1222 (lock timeout) → throws `DistributedReadLockTimeoutException` or `DistributedWriteLockTimeoutException` +- **Minimum isolation**: Requires `ReadCommitted` or higher (lines 141-145, 176-180) + +### SqlServerBulkSqlInsertProvider (Services/SqlServerBulkSqlInsertProvider.cs) + +True bulk insert using `SqlBulkCopy`: + +```csharp +// Line 61-68: SqlBulkCopy configuration +new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) +{ + BulkCopyTimeout = 0, // No timeout (uses connection timeout) + DestinationTableName = tableName, + BatchSize = 4096, // Consistent with NPoco +} +``` + +**Key features**: +- Streams data via `PocoDataDataReader` (line 69) - avoids in-memory DataTable +- Explicit column mappings by name (lines 74-77) - prevents column order mismatches +- Returns actual count inserted (line 80) - NPoco's `InsertBulk` doesn't provide this + +### SqlAzureTransientErrorDetectionStrategy (FaultHandling/Strategies/) + +Detects transient Azure SQL errors for retry: + +| Error Code | Description | +|------------|-------------| +| 40501 | Service busy (throttling) | +| 10928/10929 | Resource limits reached | +| 10053/10054 | Transport-level errors | +| 10060 | Connection timeout | +| 40197/40540/40143 | Service processing errors | +| 40613 | Database not available | +| 233 | Connection initialization error | +| 64 | Login process error | + +### SqlServerSyntaxProvider (Services/SqlServerSyntaxProvider.cs) + +SQL Server version detection and syntax generation: + +- **Engine editions**: Desktop, Standard, Enterprise, Express, Azure (lines 23-31) +- **SQL Server versions**: V7 through V2019, plus Azure (lines 33-47) +- **Default isolation**: `ReadCommitted` (line 69) +- **Azure detection**: `ServerVersion?.IsAzure` determines `DbProvider` (line 67) +- **Version detection**: Populated via `GetDbProviderManifest()` by querying SQL Server metadata + +--- + +## 4. LocalDB Support + +`LocalDb.cs` (~1,100 lines) provides comprehensive SQL Server LocalDB management: + +- Database creation/deletion +- Instance management +- File path handling +- Connection string generation + +**Known issues** (from TODO comments): +- Line 358: Stale database handling not implemented +- Line 359: File name assumptions may not always be correct + +--- + +## 5. Differences from SQLite Provider + +| Feature | SQL Server | SQLite | +|---------|-----------|--------| +| **Bulk Insert** | `SqlBulkCopy` (true bulk) | Transaction + individual inserts | +| **Locking** | Row-level with `REPEATABLEREAD` hints | Database-level (WAL mode) | +| **Types** | Native (NVARCHAR, DECIMAL, UNIQUEIDENTIFIER) | TEXT for most types | +| **Transient Retry** | Azure SQL error codes + network errors | BUSY/LOCKED retry only | +| **LocalDB** | Full support (~1,100 lines) | N/A | + +--- + +## 6. Project-Specific Notes + +### InternalsVisibleTo + +Test projects have access to internal types: +```xml +Umbraco.Tests.Integration +Umbraco.Tests.UnitTests +``` + +### Known Technical Debt + +1. **Warning Suppressions** (`.csproj:8-22`): Multiple analyzer warnings suppressed + - StyleCop: SA1405, SA1121, SA1117 + - IDE: 1006 (naming), 0270/0057/0054/0048 (simplification) + - CS0618 (obsolete usage), CS1574 (XML comments) + +2. **BulkInsert TODO** (`SqlServerBulkSqlInsertProvider.cs:44-46`): Custom `SqlBulkCopy` implementation used because NPoco's `InsertBulk` doesn't return record count. Performance comparison vs NPoco's DataTable approach pending. + +3. **LocalDB TODOs** (`LocalDb.cs:358-359`): Stale database cleanup and file name assumption handling incomplete. + +### NPoco Bulk Extensions + +`NPocoSqlServerDatabaseExtensions.ConfigureNPocoBulkExtensions()` is called during registration (line 50) to configure NPoco's SQL Server bulk operations. + +### Three Provider Metadata Types + +Different metadata for different SQL Server variants: +- `SqlServerDatabaseProviderMetadata` - Standard SQL Server +- `SqlLocalDbDatabaseProviderMetadata` - LocalDB +- `SqlAzureDatabaseProviderMetadata` - Azure SQL + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `SqlServerSyntaxProvider.cs` | Core SQL syntax provider | +| `SqlServerDistributedLockingMechanism.cs` | Row-level locking | +| `SqlServerBulkSqlInsertProvider.cs` | SqlBulkCopy wrapper | +| `SqlAzureTransientErrorDetectionStrategy.cs` | Azure retry logic | +| `LocalDb.cs` | LocalDB management | + +### Provider Name +`Constants.ProviderName` = `"Microsoft.Data.SqlClient"` + +### Registered Services + +All registered via `TryAddEnumerable`: +- `ISqlSyntaxProvider` → `SqlServerSyntaxProvider` +- `IBulkSqlInsertProvider` → `SqlServerBulkSqlInsertProvider` +- `IDatabaseCreator` → `SqlServerDatabaseCreator` +- `IProviderSpecificMapperFactory` → `SqlServerSpecificMapperFactory` +- `IDatabaseProviderMetadata` → Three variants (SqlServer, LocalDb, Azure) +- `IDistributedLockingMechanism` → `SqlServerDistributedLockingMechanism` +- `IProviderSpecificInterceptor` → MiniProfiler and RetryPolicy interceptors + +### Related Projects +- **Sibling**: `Umbraco.Cms.Persistence.Sqlite` - SQLite NPoco provider +- **EF Core**: `Umbraco.Cms.Persistence.EFCore.SqlServer` - EF Core SQL Server provider diff --git a/src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md b/src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md new file mode 100644 index 000000000000..5e15f919f3e9 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/CLAUDE.md @@ -0,0 +1,204 @@ +# Umbraco.Cms.Persistence.Sqlite + +SQLite database provider for Umbraco CMS using NPoco ORM. Provides SQLite-specific SQL syntax, type mappers, distributed locking, and connection interceptors. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Dependencies**: Umbraco.Infrastructure, Microsoft.Data.Sqlite + +**Note**: This is the **legacy NPoco-based** SQLite provider. For EF Core SQLite support, see `Umbraco.Cms.Persistence.EFCore.Sqlite`. + +--- + +## 1. Architecture + +### Project Purpose + +This project provides complete SQLite support for Umbraco's NPoco-based persistence layer: + +1. **SQL Syntax Provider** - SQLite-specific SQL generation and schema operations +2. **Type Mappers** - GUID, decimal, date/time mapping for SQLite's type system +3. **Distributed Locking** - SQLite-specific locking using WAL mode +4. **Connection Interceptors** - Deferred transactions, MiniProfiler, retry policies +5. **Bulk Insert** - SQLite-optimized record insertion + +### Folder Structure + +``` +Umbraco.Cms.Persistence.Sqlite/ +├── Interceptors/ +│ ├── SqliteAddMiniProfilerInterceptor.cs # MiniProfiler integration +│ ├── SqliteAddPreferDeferredInterceptor.cs # Deferred transaction wrapper +│ ├── SqliteAddRetryPolicyInterceptor.cs # Transient error retry +│ └── SqliteConnectionInterceptor.cs # Base interceptor +├── Mappers/ +│ ├── SqliteGuidScalarMapper.cs # GUID as TEXT mapping +│ ├── SqlitePocoDateAndTimeOnlyMapper.cs # Date/Time as TEXT +│ ├── SqlitePocoDecimalMapper.cs # Decimal as TEXT (lossless) +│ └── SqlitePocoGuidMapper.cs # GUID for NPoco +├── Services/ +│ ├── SqliteBulkSqlInsertProvider.cs # Bulk insert via transactions +│ ├── SqliteDatabaseCreator.cs # Database initialization +│ ├── SqliteDatabaseProviderMetadata.cs # Provider info +│ ├── SqliteDistributedLockingMechanism.cs # WAL-based locking +│ ├── SqliteExceptionExtensions.cs # Error code helpers +│ ├── SqlitePreferDeferredTransactionsConnection.cs # Deferred tx wrapper +│ ├── SqliteSpecificMapperFactory.cs # Type mapper factory +│ ├── SqliteSyntaxProvider.cs # SQL syntax (481 lines) +│ └── SqliteTransientErrorDetectionStrategy.cs # BUSY/LOCKED detection +├── Constants.cs # Provider name constant +├── SqliteComposer.cs # DI auto-registration +└── UmbracoBuilderExtensions.cs # AddUmbracoSqliteSupport() +``` + +### Auto-Registration + +When this assembly is referenced, `SqliteComposer` automatically registers all SQLite services via the `AddUmbracoSqliteSupport()` extension method. + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### SqliteSyntaxProvider (Services/SqliteSyntaxProvider.cs) + +The core SQL syntax provider implementing `ISqlSyntaxProvider`. Key characteristics: + +- **All strings stored as TEXT** with `COLLATE NOCASE` (case-insensitive) +- **GUIDs stored as TEXT** (no native GUID support) +- **Decimals stored as TEXT** to avoid REAL precision loss (line 51) +- **Default isolation level**: `IsolationLevel.Serializable` (line 66-67) +- **No identity insert support** (line 73) +- **No clustered index support** (line 76) +- **LIMIT instead of TOP** for pagination (line 273-278) +- **AUTOINCREMENT required** to prevent magic ID issues with negative IDs (line 224-228) + +### SqliteDistributedLockingMechanism (Services/SqliteDistributedLockingMechanism.cs) + +Database-level locking using SQLite WAL mode: + +- **Read locks**: Snapshot isolation via WAL (mostly no-op, just validates transaction exists) +- **Write locks**: Only one writer at a time (entire database locked) +- **Timeout handling**: Uses `CommandTimeout` for busy-wait (line 163) +- **Error handling**: Catches `SQLITE_BUSY` and `SQLITE_LOCKED` errors + +### SqlitePreferDeferredTransactionsConnection (Services/SqlitePreferDeferredTransactionsConnection.cs) + +Wraps `SqliteConnection` to force deferred transactions: + +```csharp +// Line 33-34: The critical behavior +protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => _inner.BeginTransaction(isolationLevel, true); // <-- The important bit +``` + +This prevents immediate write locks when beginning transactions, allowing multiple readers. + +### Connection Interceptors + +- **SqliteAddPreferDeferredInterceptor** - Wraps connections with deferred transaction support +- **SqliteAddMiniProfilerInterceptor** - Adds profiling for development +- **SqliteAddRetryPolicyInterceptor** - Retries on transient SQLite errors + +--- + +## 4. SQLite Type Mapping + +SQLite has limited types. Umbraco maps .NET types as follows: + +| .NET Type | SQLite Type | Notes | +|-----------|-------------|-------| +| `int`, `long`, `bool` | INTEGER | Native support | +| `string` | TEXT COLLATE NOCASE | Case-insensitive | +| `Guid` | TEXT | Stored as string representation | +| `DateTime`, `DateTimeOffset` | TEXT | ISO format string | +| `decimal` | TEXT | Prevents REAL precision loss | +| `double`, `float` | REAL | Native support | +| `byte[]` | BLOB | Native support | + +--- + +## 5. Project-Specific Notes + +### Database Creation Prevention + +`UmbracoBuilderExtensions.cs` (line 49-64) prevents accidental database file creation: + +```csharp +// Changes ReadWriteCreate mode to ReadWrite only +if (connectionStringBuilder.Mode == SqliteOpenMode.ReadWriteCreate) +{ + connectionStringBuilder.Mode = SqliteOpenMode.ReadWrite; +} +``` + +This ensures the database must exist before Umbraco connects. + +### WAL Mode Locking + +SQLite with WAL (Write-Ahead Logging) journal mode: +- Multiple readers can access snapshots concurrently +- Only one writer at a time (database-level lock) +- Write lock uses `umbracoLock` table with UPDATE to acquire + +### Bulk Insert Implementation + +`SqliteBulkSqlInsertProvider` (line 38-60) doesn't use true bulk copy (SQLite doesn't support it). Instead: +1. Wraps inserts in a transaction if not already in one +2. Inserts records one-by-one with `database.Insert()` +3. Completes transaction + +This is slower than SQL Server's `SqlBulkCopy` but safe for SQLite. + +### Known Technical Debt + +1. **Warning Suppression** (`.csproj:8-12`): `CS0114` - hiding inherited members needs fixing +2. **TODO in SqliteSyntaxProvider** (line 178): `TryGetDefaultConstraint` not implemented for SQLite + +### Differences from SQL Server Provider + +| Feature | SQLite | SQL Server | +|---------|--------|------------| +| Bulk Insert | Transaction + individual inserts | SqlBulkCopy | +| Locking | Database-level (WAL) | Row-level | +| Identity Insert | Not supported | Supported | +| Clustered Indexes | Not supported | Supported | +| TOP clause | LIMIT | TOP | +| Decimal | TEXT (lossless) | DECIMAL | +| GUID | TEXT | UNIQUEIDENTIFIER | + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `SqliteSyntaxProvider.cs` | Core SQL syntax provider | +| `SqliteDistributedLockingMechanism.cs` | Database locking | +| `UmbracoBuilderExtensions.cs` | DI registration | +| `SqlitePreferDeferredTransactionsConnection.cs` | Deferred tx support | + +### Provider Name +`Constants.ProviderName` = `"Microsoft.Data.Sqlite"` + +### Registered Services + +All registered via `TryAddEnumerable` (allowing multiple providers): +- `ISqlSyntaxProvider` → `SqliteSyntaxProvider` +- `IBulkSqlInsertProvider` → `SqliteBulkSqlInsertProvider` +- `IDatabaseCreator` → `SqliteDatabaseCreator` +- `IProviderSpecificMapperFactory` → `SqliteSpecificMapperFactory` +- `IDatabaseProviderMetadata` → `SqliteDatabaseProviderMetadata` +- `IDistributedLockingMechanism` → `SqliteDistributedLockingMechanism` +- `IProviderSpecificInterceptor` → Three interceptors + +### Related Projects +- **Sibling**: `Umbraco.Cms.Persistence.SqlServer` - SQL Server NPoco provider +- **EF Core**: `Umbraco.Cms.Persistence.EFCore.Sqlite` - EF Core SQLite provider diff --git a/src/Umbraco.Cms.StaticAssets/CLAUDE.md b/src/Umbraco.Cms.StaticAssets/CLAUDE.md new file mode 100644 index 000000000000..7ad044a42bf3 --- /dev/null +++ b/src/Umbraco.Cms.StaticAssets/CLAUDE.md @@ -0,0 +1,260 @@ +# Umbraco.Cms.StaticAssets + +Static assets and Razor views for Umbraco CMS. This is a Razor Class Library (RCL) that packages all backoffice, login, and website assets for deployment. + +**Project Type**: Razor Class Library (NuGet package) +**Target Framework**: net10.0 +**SDK**: Microsoft.NET.Sdk.Razor + +--- + +## 1. Architecture + +### Project Purpose + +This project packages all static assets required for Umbraco CMS at runtime: + +1. **Razor Views** - Server-rendered pages for backoffice, login, and error pages +2. **Static Web Assets** - JavaScript, CSS, fonts, images served at runtime +3. **Build Integration** - MSBuild targets that compile TypeScript/frontend projects on demand + +### Key Characteristic + +**No C# Source Code** - This is a pure asset packaging project. All functionality comes from referenced projects (`Umbraco.Cms.Api.Management`, `Umbraco.Web.Website`). + +### Folder Structure + +``` +Umbraco.Cms.StaticAssets/ +├── umbraco/ # Razor Views (server-rendered) +│ ├── UmbracoBackOffice/ +│ │ └── Index.cshtml # Backoffice SPA shell (69 lines) +│ ├── UmbracoLogin/ +│ │ └── Index.cshtml # Login page (99 lines) +│ └── UmbracoWebsite/ +│ ├── NoNodes.cshtml # "No published content" page +│ ├── NotFound.cshtml # 404 error page +│ └── Maintenance.cshtml # Maintenance mode page +│ +├── wwwroot/ # Static Web Assets +│ ├── App_Plugins/ +│ │ └── Umbraco.BlockGridEditor.DefaultCustomViews/ # Block grid demo templates +│ └── umbraco/ +│ ├── assets/ # Logos and branding (see README.md) +│ ├── backoffice/ # Built backoffice SPA (from Umbraco.Web.UI.Client) +│ │ ├── apps/ # Application modules +│ │ ├── assets/ # Fonts, language files +│ │ ├── css/ # Themes and stylesheets +│ │ └── monaco-editor/ # Code editor assets +│ ├── login/ # Built login SPA (from Umbraco.Web.UI.Login) +│ └── website/ # Frontend website assets (fonts, CSS) +│ +└── Umbraco.Cms.StaticAssets.csproj # Build configuration (149 lines) +``` + +### Project Dependencies + +```xml + + +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### Razor Views + +| View | Purpose | Line Count | +|------|---------|------------| +| `UmbracoBackOffice/Index.cshtml` | Backoffice SPA entry point with `` web component | 69 | +| `UmbracoLogin/Index.cshtml` | Login page with `` web component | 99 | +| `UmbracoWebsite/NoNodes.cshtml` | Welcome page when no content published | 59 | +| `UmbracoWebsite/NotFound.cshtml` | 404 error page (debug info in debug mode) | 79 | +| `UmbracoWebsite/Maintenance.cshtml` | Maintenance mode during upgrades | 65 | + +### Backoffice Index View (umbraco/UmbracoBackOffice/Index.cshtml) + +Key injected services: +- `IBackOfficePathGenerator` - Generates backoffice URL paths +- `IPackageManifestService` - Package manifest discovery +- `IJsonSerializer` - JSON serialization for import maps +- `IProfilerHtml` - MiniProfiler integration + +Key features: +- **Import Maps** (line 35): `Html.BackOfficeImportMapScriptAsync()` generates JavaScript module import maps +- **Debug Mode** (line 16, 63-66): `?umbDebug=true` query param enables profiler +- **NoScript Fallback** (lines 40-60): Displays message if JavaScript disabled + +### Login View (umbraco/UmbracoLogin/Index.cshtml) + +Configures `` web component with attributes: +- `return-url` - Redirect after login +- `logo-image` / `background-image` - Branding URLs from `BackOfficeGraphicsController` +- `username-is-email` - From `SecuritySettings` +- `allow-user-invite` / `allow-password-reset` - Email capability check +- `disable-local-login` - External login provider configuration + +--- + +## 4. Build System + +### Frontend Build Integration (csproj lines 36-90) + +The `.csproj` contains MSBuild targets that automatically build frontend projects when assets are missing. + +**Backoffice Build** (lines 36-90): +``` +BackofficeProjectDirectory = ../Umbraco.Web.UI.Client/ +BackofficeAssetsPath = wwwroot/umbraco/backoffice +``` + +**Login Build** (lines 94-148): +``` +LoginProjectDirectory = ../Umbraco.Web.UI.Login/ +LoginAssetsPath = wwwroot/umbraco/login +``` + +### Build Targets + +| Target | Purpose | +|--------|---------| +| `BuildStaticAssetsPreconditions` | Checks if build needed (Visual Studio only) | +| `RestoreBackoffice` | Runs `npm i` if package-lock changed | +| `BuildBackoffice` | Runs `npm run build:for:cms` | +| `DefineBackofficeAssets` | Registers assets with StaticWebAssets system | +| `CleanBackoffice` | Removes built assets on `dotnet clean` | + +### Build Conditions + +- **UmbracoBuild Variable**: When set (CI/CD), skips frontend builds (pre-built assets expected) +- **Visual Studio Detection**: `'$(UmbracoBuild)' == ''` indicates VS build +- **preserve.backoffice Marker**: Skip clean if `preserve.backoffice` file exists in solution root + +--- + +## 5. Static Web Assets + +### Asset Categories + +| Directory | Contents | Served At | +|-----------|----------|-----------| +| `wwwroot/umbraco/assets/` | Branding logos | Via `BackOfficeGraphicsController` API | +| `wwwroot/umbraco/backoffice/` | Built backoffice SPA | `/umbraco/backoffice/*` | +| `wwwroot/umbraco/login/` | Built login SPA | `/umbraco/login/*` | +| `wwwroot/umbraco/website/` | Website assets (fonts, CSS) | `/umbraco/website/*` | +| `wwwroot/App_Plugins/` | Block editor demo views | `/App_Plugins/*` | + +### Logo Assets (wwwroot/umbraco/assets/) + +Documented in `wwwroot/umbraco/assets/README.md` (16 lines): + +| File | Usage | API Endpoint | +|------|-------|--------------| +| `logo.svg` | Backoffice and public sites | `/umbraco/management/api/v1/security/back-office/graphics/logo` | +| `logo_dark.svg` | Login screen (dark mode) | `.../graphics/login-logo-alternative` | +| `logo_light.svg` | Login screen (light mode) | `.../graphics/login-logo` | +| `logo_blue.svg` | Alternative branding | N/A | + +### Block Grid Demo Views (wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/) + +Pre-built AngularJS templates for block grid editor demos: +- `umbBlockGridDemoHeadlineBlock.html` +- `umbBlockGridDemoImageBlock.html` +- `umbBlockGridDemoRichTextBlock.html` +- `umbBlockGridDemoTwoColumnLayoutBlock.html` + +--- + +## 6. Project-Specific Notes + +### Static Web Asset Base Path + +```xml +/ +``` + +Assets are served from root path, not under assembly name. + +### Compression Disabled + +```xml +false +``` + +Comment notes `MapStaticAssets()` is not used (yet). + +### Known Technical Debt + +1. **Warning Suppression** (`.csproj:14-16`): `NU5123` - File paths too long for NuGet package. TODO indicates files should be renamed. + +2. **Excluded Content** (`.csproj:31`): `wwwroot/umbraco/assets/README.md` explicitly excluded from package. + +### Backoffice Localization + +The backoffice includes language files for 25+ languages in `wwwroot/umbraco/backoffice/assets/lang/`: +- ar, bs, cs, cy, da, de, en, en-us, es, fr, he, hr, it, ja, ko, nb, nl, pl, pt, pt-br, ro, ru, sv, tr, uk, zh, zh-tw + +### Monaco Editor + +Full Monaco code editor included at `wwwroot/umbraco/backoffice/monaco-editor/` for rich code editing in backoffice. + +### Theming + +CSS themes in `wwwroot/umbraco/backoffice/css/`: +- `umb-css.css` - Main styles +- `uui-css.css` - UI library styles +- `dark.theme.css` - Dark theme +- `high-contrast.theme.css` - Accessibility theme +- `umbraco-blockgridlayout.css` - Block grid styles +- `rte-content.css` - Rich text editor content styles + +--- + +## Quick Reference + +### Essential Files + +| File | Purpose | +|------|---------| +| `umbraco/UmbracoBackOffice/Index.cshtml` | Backoffice entry point | +| `umbraco/UmbracoLogin/Index.cshtml` | Login page | +| `wwwroot/umbraco/assets/README.md` | Asset documentation | +| `Umbraco.Cms.StaticAssets.csproj` | Build targets for frontend | + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Web.UI.Client` | Source for backoffice assets (npm build) | +| `Umbraco.Web.UI.Login` | Source for login assets (npm build) | +| `Umbraco.Cms.Api.Management` | Razor view dependencies | +| `Umbraco.Web.Website` | Razor view dependencies | + +### Build Commands (Manual) + +```bash +# Build backoffice assets (from Umbraco.Web.UI.Client) +cd src/Umbraco.Web.UI.Client +npm install +npm run build:for:cms + +# Build login assets (from Umbraco.Web.UI.Login) +cd src/Umbraco.Web.UI.Login +npm install +npm run build +``` + +### Preserve Assets During Clean + +Create marker file to prevent asset deletion: +```bash +touch preserve.backoffice # In solution root +touch preserve.login # In solution root +``` diff --git a/src/Umbraco.Examine.Lucene/CLAUDE.md b/src/Umbraco.Examine.Lucene/CLAUDE.md new file mode 100644 index 000000000000..a19053a59e03 --- /dev/null +++ b/src/Umbraco.Examine.Lucene/CLAUDE.md @@ -0,0 +1,293 @@ +# Umbraco.Examine.Lucene + +Full-text search provider for Umbraco CMS using Examine and Lucene.NET. Provides content, media, member, and Delivery API indexing with configurable directory factories and index diagnostics. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Package ID**: Umbraco.Cms.Examine.Lucene +**Namespace**: Umbraco.Cms.Infrastructure.Examine +**Dependencies**: Umbraco.Infrastructure, Examine (Lucene.NET wrapper) + +--- + +## 1. Architecture + +### Project Purpose + +This project provides Lucene-based full-text search capabilities for Umbraco: + +1. **Index Types** - Content, Media, Member, and Delivery API indexes +2. **Directory Factories** - Configurable index storage (filesystem, temp, synced) +3. **Index Diagnostics** - Health checks and metadata for backoffice +4. **Backoffice Search** - Unified search across content tree with permissions + +### Folder Structure + +``` +Umbraco.Examine.Lucene/ +├── DependencyInjection/ +│ ├── ConfigureIndexOptions.cs # Per-index configuration (77 lines) +│ └── UmbracoBuilderExtensions.cs # AddExamineIndexes() registration (64 lines) +├── Extensions/ +│ └── ExamineExtensions.cs # Lucene query parsing, health check +├── AddExamineComposer.cs # Auto-registration via IComposer +├── BackOfficeExamineSearcher.cs # Unified backoffice search (532 lines) +├── ConfigurationEnabledDirectoryFactory.cs # Directory factory selector +├── DeliveryApiContentIndex.cs # Headless API index with culture support +├── LuceneIndexDiagnostics.cs # Base diagnostics implementation +├── LuceneIndexDiagnosticsFactory.cs # Diagnostics factory +├── LuceneRAMDirectoryFactory.cs # In-memory directory for testing +├── NoPrefixSimpleFsLockFactory.cs # Lock factory without prefix +├── UmbracoApplicationRoot.cs # Application root path provider +├── UmbracoContentIndex.cs # Content/media index (158 lines) +├── UmbracoExamineIndex.cs # Base index class (153 lines) +├── UmbracoExamineIndexDiagnostics.cs # Extended diagnostics +├── UmbracoLockFactory.cs # Lock factory wrapper +├── UmbracoMemberIndex.cs # Member index +└── UmbracoTempEnvFileSystemDirectoryFactory.cs # Temp directory factory +``` + +### Index Hierarchy + +``` +LuceneIndex (Examine) + └── UmbracoExamineIndex (base for all Umbraco indexes) + ├── UmbracoContentIndex (content + media) + ├── UmbracoMemberIndex (members) + └── DeliveryApiContentIndex (headless API) +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### Built-in Indexes (Constants.UmbracoIndexes) + +| Index Name | Type | Purpose | +|------------|------|---------| +| `InternalIndex` | UmbracoContentIndex | Backoffice search (all content) | +| `ExternalIndex` | UmbracoContentIndex | Frontend published content | +| `MembersIndex` | UmbracoMemberIndex | Member search | +| `DeliveryApiContentIndex` | DeliveryApiContentIndex | Headless API content | + +**Note:** See [ConfigureIndexOptions](#configureindexoptions-dependencyinjectionconfigureindexoptionscs) for per-index analyzer and validator configuration. + +### UmbracoExamineIndex (UmbracoExamineIndex.cs) + +Base class for all Umbraco indexes. Key features: + +**Runtime State Check** (lines 84-95): +```csharp +protected bool CanInitialize() +{ + var canInit = _runtimeState.Level == RuntimeLevel.Run; + // Logs warning once if runtime not ready +} +``` +Prevents indexing during install/upgrade. + +**Special Field Transformations** (lines 130-152): +- `__Path` field - Enables descendant queries via path matching +- `__Icon` field - Preserves icon for display + +**Raw Field Storage** (lines 101-118): +Fields prefixed with `__Raw_` are stored as `StoredField` (not analyzed), used for returning exact values. + +### UmbracoContentIndex (UmbracoContentIndex.cs) + +Handles content and media indexing with validation and cascade deletes. + +**Cascade Delete** (lines 128-157): +When content deleted, automatically finds and removes all descendants: +```csharp +var descendantPath = $@"\-1\,*{nodeId}\,*"; +var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; +``` + +**Validation Groups** (lines 66-116): +- `Valid` - Index normally +- `Failed` - Skip (invalid data) +- `Filtered` - Delete from index (moved to recycle bin) + +### DeliveryApiContentIndex (DeliveryApiContentIndex.cs) + +Specialized index for Delivery API with culture support. + +**Key Differences** (lines 20-34): +- `ApplySpecialValueTransformations = false` - No path/icon transformations +- `PublishedValuesOnly = false` - Handles via populator +- `EnableDefaultEventHandler = false` - Custom event handling + +**Composite IDs** (lines 118-128): +Index IDs can be composite: `"1234|da-DK"` (content ID + culture) or simple: `"1234"`. + +### BackOfficeExamineSearcher (BackOfficeExamineSearcher.cs) + +Unified search for backoffice tree with user permissions. + +**Search Features**: +- Node name boosting (10x for exact match, line 316-327) +- Wildcard support for partial matches (line 350-376) +- User start node filtering (lines 378-456) +- Recycle bin filtering (lines 185-191) +- Multi-language variant search (all `nodeName_{lang}` fields) + +**Entity Type Routing** (lines 89-155): Member→MembersIndex, Media→InternalIndex, Document→InternalIndex + +--- + +## 4. Directory Factories + +### Configuration Options (IndexCreatorSettings.LuceneDirectoryFactory) + +- `Default` - FileSystemDirectoryFactory (standard filesystem storage) +- `TempFileSystemDirectoryFactory` - UmbracoTempEnvFileSystemDirectoryFactory (store in `%TEMP%/ExamineIndexes`) +- `SyncedTempFileSystemDirectoryFactory` - SyncedFileSystemDirectoryFactory (temp with sync to persistent) + +### UmbracoTempEnvFileSystemDirectoryFactory (lines 20-34) + +Creates unique temp path using site name + application identifier hash: +```csharp +var hashString = hostingEnvironment.SiteName + "::" + applicationIdentifier.GetApplicationUniqueIdentifier(); +var cachePath = Path.Combine(Path.GetTempPath(), "ExamineIndexes", appDomainHash); +``` + +**Purpose**: Prevents index collisions when same app moves between workers in load-balanced scenarios. + +### ConfigurationEnabledDirectoryFactory (lines 34-62) + +Selector that creates appropriate factory based on `IndexCreatorSettings.LuceneDirectoryFactory` config value. + +--- + +## 5. Index Configuration + +### ConfigureIndexOptions (DependencyInjection/ConfigureIndexOptions.cs) + +Configures per-index options via `IConfigureNamedOptions`. + +**Per-Index Settings** (lines 39-61): + +| Index | Analyzer | Validator | +|-------|----------|-----------| +| InternalIndex | CultureInvariantWhitespaceAnalyzer | ContentValueSetValidator | +| ExternalIndex | StandardAnalyzer | PublishedContentValueSetValidator | +| MembersIndex | CultureInvariantWhitespaceAnalyzer | MemberValueSetValidator | +| DeliveryApiContentIndex | StandardAnalyzer | None (populator handles) | + +**Global Settings** (lines 63-70): +- `UnlockIndex = true` - Always unlock on startup +- Snapshot deletion policy when using SyncedTempFileSystemDirectoryFactory + +--- + +## 6. Index Diagnostics + +### LuceneIndexDiagnostics (LuceneIndexDiagnostics.cs) + +Provides health checks and metadata for backoffice examine dashboard. + +**Health Check** (lines 41-45): +```csharp +public Attempt IsHealthy() +{ + var isHealthy = Index.IsHealthy(out Exception? indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError?.Message); +} +``` + +**Metadata** (lines 51-92): +- CommitCount, DefaultAnalyzer, LuceneDirectory type +- LuceneIndexFolder (relative path) +- DirectoryFactory and IndexDeletionPolicy types + +### UmbracoExamineIndexDiagnostics (UmbracoExamineIndexDiagnostics.cs) + +Extends base with Umbraco-specific metadata (lines 23-48): +- EnableDefaultEventHandler +- PublishedValuesOnly +- Validator settings (IncludeItemTypes, ExcludeItemTypes, etc.) + +--- + +## 7. Project-Specific Notes + +### Auto-Registration + +`AddExamineComposer` (lines 10-14) automatically registers all Examine services: +```csharp +builder + .AddExamine() + .AddExamineIndexes(); +``` + +### Service Registrations (UmbracoBuilderExtensions.cs) + +Key services registered (lines 18-62): `IBackOfficeExamineSearcher`, `IIndexDiagnosticsFactory`, `IApplicationRoot`, `ILockFactory`, plus all 4 indexes with `ConfigurationEnabledDirectoryFactory`. + +### Known Technical Debt + +1. **Warning Suppression** (`.csproj:10-14`): `CS0618` - Uses obsolete members in Examine/Lucene.NET + +2. **TODO: Raw Query Support** (`BackOfficeExamineSearcher.cs:71-76`): + ```csharp + // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string + // manipulation for things like start paths, member types, etc... + ``` + +3. **TODO: Query Parsing** (`ExamineExtensions.cs:21-22`): + ```csharp + // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet + ``` + +### Index Field Names + +Key fields from `UmbracoExamineFieldNames` (defined in Umbraco.Core): +- `__Path` - Content path for descendant queries +- `__Icon` - Content icon +- `__Raw_*` - Raw stored values (not analyzed) +- `__Key` - Content GUID +- `__NodeTypeAlias` - Content type alias +- `__IndexType` - `content`, `media`, or `member` + +### Lucene Query Escaping + +`BackOfficeExamineSearcher.BuildQuery()` (lines 193-311) handles: +- Special characters `*`, `-`, `_` removal/replacement +- Quoted phrase search (`"exact match"`) +- Query escaping via `QueryParserBase.Escape()` +- Path escaping (replace `-` with `\-`, `,` with `\,`) + +### User Start Node Filtering + +`BackOfficeExamineSearcher.AppendPath()` (lines 378-456) ensures users only see content they have access to: +1. Gets user's start nodes from `IBackOfficeSecurityAccessor` +2. Filters search results to paths under start nodes +3. Returns empty results if searchFrom outside user's access + +--- + +## Quick Reference + +**Index Names** (Constants.UmbracoIndexes): +- `InternalIndexName` = "InternalIndex" +- `ExternalIndexName` = "ExternalIndex" +- `MembersIndexName` = "MembersIndex" +- `DeliveryApiContentIndexName` = "DeliveryApiContentIndex" + +**Key Interfaces**: +- `IUmbracoIndex` - Marker for Umbraco indexes +- `IUmbracoContentIndex` - Content index marker +- `IBackOfficeExamineSearcher` - Backoffice search service +- `IIndexDiagnostics` - Health/metadata for dashboard + +**Dependencies**: +- `Umbraco.Infrastructure` - Core services, index populators +- `Umbraco.Core` - Field names, constants, interfaces +- `Examine` (NuGet) - Lucene.NET abstraction diff --git a/src/Umbraco.PublishedCache.HybridCache/CLAUDE.md b/src/Umbraco.PublishedCache.HybridCache/CLAUDE.md new file mode 100644 index 000000000000..b881d185ac8a --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/CLAUDE.md @@ -0,0 +1,362 @@ +# Umbraco.PublishedCache.HybridCache + +Published content caching layer for Umbraco CMS using Microsoft's HybridCache (in-memory + optional distributed cache). Provides high-performance content delivery with cache seeding, serialization, and notification-based invalidation. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Package ID**: Umbraco.Cms.PublishedCache.HybridCache +**Namespace**: Umbraco.Cms.Infrastructure.HybridCache +**Dependencies**: Umbraco.Core, Umbraco.Infrastructure, Microsoft.Extensions.Caching.Hybrid, MessagePack, K4os.Compression.LZ4 + +--- + +## 1. Architecture + +### Project Purpose + +This project implements the published content cache using Microsoft's HybridCache pattern: + +1. **Multi-Level Caching** - L1 (in-memory) + L2 (optional distributed cache like Redis) +2. **Cache Seeding** - Pre-populates cache on startup with frequently accessed content +3. **MessagePack Serialization** - Fast binary serialization with LZ4 compression +4. **Notification-Based Invalidation** - Automatic cache updates on content changes +5. **Draft/Published Separation** - Separate cache entries for draft and published content + +### Folder Structure + +``` +Umbraco.PublishedCache.HybridCache/ +├── DependencyInjection/ +│ └── UmbracoBuilderExtensions.cs # AddUmbracoHybridCache() registration (90 lines) +├── Extensions/ +│ └── HybridCacheExtensions.cs # ExistsAsync extension method +├── Factories/ +│ ├── CacheNodeFactory.cs # Creates ContentCacheNode from IContent +│ ├── ICacheNodeFactory.cs +│ ├── IPublishedContentFactory.cs +│ └── PublishedContentFactory.cs # Creates IPublishedContent from cache +├── NotificationHandlers/ +│ ├── CacheRefreshingNotificationHandler.cs # Content/media change handler (120 lines) +│ ├── HybridCacheStartupNotificationHandler.cs +│ └── SeedingNotificationHandler.cs # Startup seeding (39 lines) +├── Persistence/ +│ ├── ContentSourceDto.cs # DTO for database queries +│ ├── DatabaseCacheRepository.cs # NPoco database access (891 lines) +│ └── IDatabaseCacheRepository.cs +├── SeedKeyProviders/ +│ ├── BreadthFirstKeyProvider.cs # Base breadth-first traversal +│ ├── Document/ +│ │ ├── ContentTypeSeedKeyProvider.cs # Seeds by content type +│ │ └── DocumentBreadthFirstKeyProvider.cs # Seeds top-level content (83 lines) +│ └── Media/ +│ └── MediaBreadthFirstKeyProvider.cs +├── Serialization/ +│ ├── ContentCacheDataModel.cs # Serializable cache model +│ ├── HybridCacheSerializer.cs # MessagePack serializer (40 lines) +│ ├── IContentCacheDataSerializer.cs # Nested data serializer interface +│ ├── JsonContentNestedDataSerializer.cs # JSON serializer option +│ ├── MsgPackContentNestedDataSerializer.cs # MessagePack serializer option +│ └── LazyCompressedString.cs # Lazy LZ4 compression +├── Services/ +│ ├── DocumentCacheService.cs # Content caching service (372 lines) +│ ├── MediaCacheService.cs # Media caching service +│ ├── MemberCacheService.cs # Member caching service +│ └── DomainCacheService.cs # Domain caching service +├── CacheManager.cs # ICacheManager facade (27 lines) +├── ContentCacheNode.cs # Cache entry model (24 lines) +├── ContentData.cs # Content data container +├── ContentNode.cs # Content node representation +├── DatabaseCacheRebuilder.cs # Full cache rebuild +├── DocumentCache.cs # IPublishedContentCache impl (58 lines) +├── MediaCache.cs # IPublishedMediaCache impl +├── MemberCache.cs # IPublishedMemberCache impl +├── DomainCache.cs # IDomainCache impl +└── PublishedContent.cs # IPublishedContent impl +``` + +### Cache Architecture + +``` +Request → DocumentCache (IPublishedContentCache) + ↓ + DocumentCacheService + ↓ + ┌─────────────────────────┐ + │ HybridCache │ + │ ┌───────┐ ┌────────┐ │ + │ │ L1 │ │ L2 │ │ + │ │Memory │→ │ Redis │ │ + │ └───────┘ └────────┘ │ + └─────────────────────────┘ + ↓ (cache miss) + DatabaseCacheRepository + ↓ + cmsContentNu table +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### HybridCache Configuration (UmbracoBuilderExtensions.cs) + +**Registration** (lines 30-36): +```csharp +builder.Services.AddHybridCache(options => +{ + // Default 100MB max payload (configurable) + options.MaximumPayloadBytes = 1024 * 1024 * 100; +}).AddSerializer(); +``` + +**Key Services Registered** (lines 38-52): +- `IPublishedContentCache` → `DocumentCache` +- `IPublishedMediaCache` → `MediaCache` +- `IPublishedMemberCache` → `MemberCache` +- `IDomainCache` → `DomainCache` +- `IDocumentCacheService` → `DocumentCacheService` +- `IMediaCacheService` → `MediaCacheService` +- `ICacheManager` → `CacheManager` + +### DocumentCacheService (Services/DocumentCacheService.cs) + +Core caching logic for content. + +**GetNodeAsync** (lines 110-155): +1. Check in-memory `_publishedContentCache` dictionary +2. Call `HybridCache.GetOrCreateAsync()` with database fallback +3. Verify published ancestor path exists +4. Convert `ContentCacheNode` to `IPublishedContent` + +**Cache Key Format** (line 326): +```csharp +private static string GetCacheKey(Guid key, bool preview) + => preview ? $"{key}+draft" : $"{key}"; +``` + +**Cache Entry Options** (lines 275-287): +- Seed entries use `SeedCacheDuration` from `CacheSettings` +- Regular entries use `LocalCacheDuration` and `RemoteCacheDuration` +- Preview/draft entries always use regular (non-seed) durations + +### ContentCacheNode (ContentCacheNode.cs) + +Immutable cache entry model (marked `[ImmutableObject(true)]` for HybridCache performance): +- `Id`, `Key`, `SortOrder`, `CreateDate`, `CreatorId` +- `ContentTypeId`, `IsDraft` +- `ContentData? Data` - Property values, culture data, URL segments + +### HybridCacheSerializer (Serialization/HybridCacheSerializer.cs) + +MessagePack serialization with LZ4 compression: +```csharp +_options = defaultOptions + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData); +``` + +--- + +## 4. Cache Seeding + +### Seed Key Providers + +| Provider | Purpose | Config Setting | +|----------|---------|----------------| +| `ContentTypeSeedKeyProvider` | Seeds specific content types | N/A | +| `DocumentBreadthFirstKeyProvider` | Seeds top-level content first | `DocumentBreadthFirstSeedCount` | +| `MediaBreadthFirstKeyProvider` | Seeds top-level media first | `MediaBreadthFirstSeedCount` | + +### DocumentBreadthFirstKeyProvider (lines 28-82) + +Breadth-first traversal that: +1. Gets root content keys +2. Filters to only published content (any culture) +3. Traverses children breadth-first until seed count reached +4. Only includes content with published ancestor path + +### SeedingNotificationHandler (lines 27-38) + +Runs on `UmbracoApplicationStartingNotification`: +- Skips during Install or Upgrade (when maintenance page shown) +- Calls `DocumentCacheService.SeedAsync()` then `MediaCacheService.SeedAsync()` + +### Seed Process (DocumentCacheService.SeedAsync lines 205-264) + +1. Collect seed keys from all `IDocumentSeedKeyProvider` instances +2. Process in batches of `DocumentSeedBatchSize` +3. Check if key already in cache via `ExistsAsync()` +4. Batch fetch uncached keys from database +5. Set in cache with seed entry options + +--- + +## 5. Cache Invalidation + +### CacheRefreshingNotificationHandler (120 lines) + +Handles 8 notification types: + +| Notification | Action | +|--------------|--------| +| `ContentRefreshNotification` | `RefreshContentAsync()` | +| `ContentDeletedNotification` | `DeleteItemAsync()` for each | +| `MediaRefreshNotification` | `RefreshMediaAsync()` | +| `MediaDeletedNotification` | `DeleteItemAsync()` for each | +| `ContentTypeRefreshedNotification` | Clear type cache, `Rebuild()` | +| `ContentTypeDeletedNotification` | Clear type cache | +| `MediaTypeRefreshedNotification` | Clear type cache, `Rebuild()` | +| `MediaTypeDeletedNotification` | Clear type cache | + +### RefreshContentAsync (DocumentCacheService.cs lines 300-324) + +1. Creates draft cache node from `IContent` +2. Persists to `cmsContentNu` table +3. If publishing: creates published node and persists +4. If unpublishing: clears published cache entry + +### Cache Tags (line 331) + +All content uses tag `Constants.Cache.Tags.Content` for bulk invalidation: +```csharp +await _hybridCache.RemoveByTagAsync(Constants.Cache.Tags.Content, cancellationToken); +``` + +--- + +## 6. Database Persistence + +### DatabaseCacheRepository (891 lines) + +NPoco-based repository for `cmsContentNu` table. + +**Key Methods**: +- `GetContentSourceAsync(Guid key, bool preview)` - Single content fetch +- `GetContentSourcesAsync(IEnumerable keys)` - Batch fetch +- `RefreshContentAsync(ContentCacheNode, PublishedState)` - Insert/update cache +- `Rebuild()` - Full cache rebuild by content type + +**SQL Templates** (lines 575-748): +Uses `SqlContext.Templates` for cached SQL generation with optimized joins across: +- `umbracoNode`, `umbracoContent`, `cmsDocument`, `umbracoContentVersion` +- `cmsDocumentVersion`, `cmsContentNu` + +### Serialization Options (NuCacheSettings.NuCacheSerializerType) + +| Type | Serializer | +|------|------------| +| `JSON` | JsonContentNestedDataSerializer | +| `MessagePack` | MsgPackContentNestedDataSerializer | + +--- + +## 7. Project-Specific Notes + +### Experimental API Warning + +HybridCache API is experimental (suppressed with `#pragma warning disable EXTEXP0018`). + +### Draft vs Published Caching + +- **Draft cache key**: `"{key}+draft"` +- **Published cache key**: `"{key}"` +- Each stored separately to avoid cross-contamination +- Draft entries never use seed durations + +### Published Ancestor Path Check (DocumentCacheService.cs lines 131-135) + +Before returning cached content, verifies ancestor path is published via `_publishStatusQueryService.HasPublishedAncestorPath()`. Returns null if parent unpublished. + +### In-Memory Content Cache (DocumentCacheService.cs line 39) + +Secondary `ConcurrentDictionary` caches converted objects, since `ContentCacheNode` to `IPublishedContent` conversion is expensive. + +### Known Technical Debt + +1. **Warnings Disabled** (`.csproj:10-12`): `TreatWarningsAsErrors=false` +2. **Property Value Null Handling** (`PropertyData.cs:25, 37`): Cannot be null, needs fallback decision +3. **Routing Cache TODO** (`ContentCacheDataModel.cs:26`): Remove when routing cache implemented +4. **SQL Template Limitations** (`DatabaseCacheRepository.cs:612, 688, 737`): Can't use templates with certain joins +5. **Serializer Refactor** (`DatabaseCacheRepository.cs:481`): Standardize to ContentTypeId only +6. **String Interning** (`MsgPackContentNestedDataSerializer.cs:29`): Intern alias strings during deserialization +7. **V18 Interface Cleanup** (`IDatabaseCacheRepository.cs:24, 48`): Remove default implementations + +### Cache Settings (CacheSettings) + +Key configuration options: +- `DocumentBreadthFirstSeedCount` - Number of documents to seed +- `MediaBreadthFirstSeedCount` - Number of media items to seed +- `DocumentSeedBatchSize` - Batch size for seeding +- `Entry.Document.SeedCacheDuration` - Duration for seeded entries +- `Entry.Document.LocalCacheDuration` - L1 cache duration +- `Entry.Document.RemoteCacheDuration` - L2 cache duration + +### InternalsVisibleTo + +Test projects with access to internal types: +- `Umbraco.Tests` +- `Umbraco.Tests.Common` +- `Umbraco.Tests.UnitTests` +- `Umbraco.Tests.Integration` +- `DynamicProxyGenAssembly2` (for mocking) + +--- + +## Quick Reference + +### Cache Flow + +``` +GetByIdAsync(id) → GetByKeyAsync(key) → GetNodeAsync(key, preview) + → HybridCache.GetOrCreateAsync() + → DatabaseCacheRepository.GetContentSourceAsync() [on miss] + → PublishedContentFactory.ToIPublishedContent() + → CreateModel(IPublishedModelFactory) +``` + +### Key Interfaces + +| Interface | Implementation | Purpose | +|-----------|----------------|---------| +| `IPublishedContentCache` | DocumentCache | Content retrieval | +| `IPublishedMediaCache` | MediaCache | Media retrieval | +| `IDocumentCacheService` | DocumentCacheService | Cache operations | +| `IDatabaseCacheRepository` | DatabaseCacheRepository | Database access | +| `ICacheNodeFactory` | CacheNodeFactory | Cache node creation | + +### Configuration Keys + +```json +{ + "Umbraco": { + "CMS": { + "Cache": { + "DocumentBreadthFirstSeedCount": 100, + "MediaBreadthFirstSeedCount": 50, + "DocumentSeedBatchSize": 100, + "Entry": { + "Document": { + "SeedCacheDuration": "04:00:00", + "LocalCacheDuration": "00:05:00", + "RemoteCacheDuration": "01:00:00" + } + } + } + } + } +} +``` + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Core` | Interfaces (IPublishedContentCache, etc.) | +| `Umbraco.Infrastructure` | Services, repositories, NPoco | +| Microsoft.Extensions.Caching.Hybrid | HybridCache implementation | diff --git a/src/Umbraco.Web.Common/CLAUDE.md b/src/Umbraco.Web.Common/CLAUDE.md new file mode 100644 index 000000000000..3731cfd7e01e --- /dev/null +++ b/src/Umbraco.Web.Common/CLAUDE.md @@ -0,0 +1,380 @@ +# Umbraco.Web.Common + +Shared ASP.NET Core web functionality for Umbraco CMS. Provides controllers, middleware, application builder extensions, security/identity, localization, and the UmbracoContext request pipeline. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Package ID**: Umbraco.Cms.Web.Common +**Namespace**: Umbraco.Cms.Web.Common +**Dependencies**: Umbraco.Examine.Lucene, Umbraco.PublishedCache.HybridCache, MiniProfiler, Serilog, Asp.Versioning + +--- + +## 1. Architecture + +### Project Purpose + +This project provides the web layer foundation for Umbraco CMS: + +1. **DI Registration** - `AddUmbraco()` entry point for service registration +2. **Application Pipeline** - `UmbracoApplicationBuilder` for middleware ordering +3. **Controllers** - Base classes for frontend and backoffice controllers +4. **UmbracoContext** - Request-scoped context with published content access +5. **Security** - Member sign-in, identity management, authentication middleware +6. **Middleware** - Boot failure handling, preview authentication, request routing + +### Folder Structure + +``` +Umbraco.Web.Common/ +├── ApplicationBuilder/ +│ ├── IUmbracoApplicationBuilder.cs # Builder interface +│ ├── IUmbracoPipelineFilter.cs # Pipeline customization hooks +│ └── UmbracoApplicationBuilder.cs # Middleware orchestration (170 lines) +├── AspNetCore/ +│ ├── AspNetCoreIpResolver.cs # IP address resolution +│ ├── AspNetCorePasswordHasher.cs # Identity password hashing +│ ├── AspNetCoreRequestAccessor.cs # Request access abstraction +│ └── OptionsMonitorAdapter.cs # Config monitoring wrapper +├── Controllers/ +│ ├── IRenderController.cs # Frontend controller marker +│ ├── IVirtualPageController.cs # Virtual page support +│ ├── PluginController.cs # Plugin controller base (104 lines) +│ ├── UmbracoApiController.cs # Legacy API controller (obsolete) +│ ├── UmbracoAuthorizedController.cs # Backoffice authorized base +│ └── UmbracoController.cs # Base MVC controller (13 lines) +├── DependencyInjection/ +│ └── UmbracoBuilderExtensions.cs # AddUmbraco(), AddUmbracoCore() (338 lines) +├── Extensions/ +│ ├── BlockGridTemplateExtensions.cs # Block grid Razor helpers +│ ├── BlockListTemplateExtensions.cs # Block list Razor helpers +│ ├── HttpRequestExtensions.cs # Request utility methods +│ ├── ImageCropperTemplateExtensions.cs # Image crop Razor helpers +│ ├── LinkGeneratorExtensions.cs # URL generation helpers +│ ├── WebApplicationExtensions.cs # BootUmbracoAsync() (32 lines) +│ └── [30+ extension classes] +├── Filters/ +│ ├── BackOfficeCultureFilter.cs # Culture detection +│ ├── DisableBrowserCacheAttribute.cs # Cache control headers +│ ├── UmbracoMemberAuthorizeAttribute.cs # Member authorization +│ └── ValidateUmbracoFormRouteStringAttribute.cs +├── Localization/ +│ ├── UmbracoBackOfficeIdentityCultureProvider.cs +│ └── UmbracoPublishedContentCultureProvider.cs +├── Middleware/ +│ ├── BootFailedMiddleware.cs # Startup failure handling (81 lines) +│ └── PreviewAuthenticationMiddleware.cs # Preview mode auth (84 lines) +├── Routing/ +│ ├── IAreaRoutes.cs # Area routing interface +│ ├── IRoutableDocumentFilter.cs # Content routing filter +│ ├── UmbracoRouteValues.cs # Route data container (60 lines) +│ └── UmbracoVirtualPageRoute.cs # Virtual page routing +├── Security/ +│ ├── MemberSignInManager.cs # Member sign-in (350 lines) +│ ├── UmbracoSignInManager.cs # Base sign-in manager +│ ├── IMemberSignInManager.cs # Sign-in interface +│ └── EncryptionHelper.cs # Surface controller encryption +├── Templates/ +│ └── TemplateRenderer.cs # Razor template rendering +├── UmbracoContext/ +│ ├── UmbracoContext.cs # Request context (173 lines) +│ └── UmbracoContextFactory.cs # Context creation (75 lines) +├── UmbracoHelper.cs # Template helper (427 lines) +└── Umbraco.Web.Common.csproj # Project configuration (48 lines) +``` + +### Request Pipeline Flow + +``` +WebApplication.BootUmbracoAsync() + ↓ +UseUmbraco().WithMiddleware() + ↓ +┌──────────────────────────────┐ +│ UmbracoApplicationBuilder │ +│ ├─ RunPrePipeline() │ +│ ├─ BootFailedMiddleware │ +│ ├─ UseUmbracoCore() │ +│ ├─ UseStaticFiles() │ +│ ├─ UseRouting() │ +│ ├─ UseAuthentication() │ +│ ├─ UseAuthorization() │ +│ ├─ UseRequestLocalization() │ +│ └─ RunPostPipeline() │ +└──────────────────────────────┘ + ↓ +WithEndpoints() → Content routing +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### DI Entry Point (DependencyInjection/UmbracoBuilderExtensions.cs) + +**AddUmbraco()** (lines 77-123) - Creates `IUmbracoBuilder` and registers core services: +```csharp +public static IUmbracoBuilder AddUmbraco( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IConfiguration config) +``` + +Key registrations: +- Sets `DataDirectory` for database paths (line 97-99) +- Creates `HttpContextAccessor` singleton (line 104-105) +- Initializes `AppCaches` with request-scoped cache +- Creates `TypeLoader` for assembly scanning +- Returns `UmbracoBuilder` for fluent configuration + +**AddUmbracoCore()** (lines 131-173) - Registers ASP.NET Core-specific services: +- `IHostingEnvironment` → `AspNetCoreHostingEnvironment` +- `IBackOfficeInfo` → `AspNetCoreBackOfficeInfo` +- `IDbProviderFactoryCreator` with SQL syntax providers +- Telemetry providers and application lifetime + +**AddWebComponents()** (lines 222-288) - Registers web-specific services: +- Session with `UMB_SESSION` cookie (line 229) +- API versioning configuration +- Password hasher, cookie manager, IP resolver +- `UmbracoHelper`, `UmbracoContextFactory` +- All middleware as singletons + +### Application Builder (ApplicationBuilder/UmbracoApplicationBuilder.cs) + +Orchestrates middleware registration in correct order. + +**WithMiddleware()** (lines 49-65): +1. `RunPrePipeline()` - Custom filters before Umbraco middleware +2. `RegisterDefaultRequiredMiddleware()` - Core middleware stack +3. `RunPostPipeline()` - Custom filters after +4. User-provided middleware callback + +**RegisterDefaultRequiredMiddleware()** (lines 70-104): +```csharp +UseUmbracoCoreMiddleware(); +AppBuilder.UseUmbracoMediaFileProvider(); +AppBuilder.UseUmbracoBackOfficeRewrites(); +AppBuilder.UseStaticFiles(); +AppBuilder.UseUmbracoPluginsStaticFiles(); +AppBuilder.UseRouting(); +AppBuilder.UseAuthentication(); +AppBuilder.UseAuthorization(); +AppBuilder.UseAntiforgery(); +AppBuilder.UseRequestLocalization(); +AppBuilder.UseSession(); +``` + +**Pipeline Filter Hooks** (IUmbracoPipelineFilter): +- `OnPrePipeline()` - Before any Umbraco middleware +- `OnPreRouting()` - Before UseRouting() +- `OnPostRouting()` - After UseRouting() +- `OnPostPipeline()` - After all Umbraco middleware +- `OnEndpoints()` - Before endpoint mapping + +### UmbracoContext (UmbracoContext/UmbracoContext.cs) + +Request-scoped container for Umbraco request state. + +**Key Properties**: +- `Content` → `IPublishedContentCache` (line 102) +- `Media` → `IPublishedMediaCache` (line 105) +- `Domains` → `IDomainCache` (line 108) +- `PublishedRequest` → Routed content for request (line 111) +- `InPreviewMode` → Preview cookie detected (lines 140-152) +- `OriginalRequestUrl` / `CleanedUmbracoUrl` - Request URLs + +**Preview Detection** (lines 154-166): +```csharp +private void DetectPreviewMode() +{ + // Check preview cookie, verify not backoffice request + var previewToken = _cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); + _previewing = _previewToken.IsNullOrWhiteSpace() == false; +} +``` + +### UmbracoHelper (UmbracoHelper.cs) + +Template helper for Razor views (scoped lifetime). + +**Content Retrieval** (lines 192-317): +- `Content(id)` - Get by int, Guid, string, or Udi +- `Content(ids)` - Batch retrieval +- `ContentAtRoot()` - Root content items + +**Media Retrieval** (lines 320-424): +- Same pattern as content + +**Dictionary** (lines 102-189): +- `GetDictionaryValue(key)` - Localized string lookup +- `GetDictionaryValueOrDefault(key, defaultValue)` +- `CultureDictionary` - Current culture dictionary + +### Controller Base Classes + +| Controller | Purpose | Features | +|------------|---------|----------| +| `UmbracoController` | Base MVC controller | Simple base, debug InstanceId | +| `UmbracoAuthorizedController` | Backoffice controllers | `[Authorize(BackOfficeAccess)]`, `[DisableBrowserCache]` | +| `PluginController` | Plugin/package controllers | UmbracoContext, Services, AppCaches, ProfilingLogger | +| `UmbracoApiController` | Legacy API controller | **Obsolete** - Use ASP.NET Core ApiController | +| `IRenderController` | Frontend rendering marker | Route hijacking support | + +**PluginController** (lines 18-104): +- Provides `UmbracoContext`, `DatabaseFactory`, `Services`, `AppCaches` +- Static metadata caching with `ConcurrentDictionary` +- Auto-discovers `[PluginController]` and `[IsBackOffice]` attributes + +### Member Sign-In (Security/MemberSignInManager.cs) + +ASP.NET Core Identity sign-in manager for members. + +**Key Features**: +- External login with auto-linking (lines 112-142) +- Two-factor authentication support (lines 162-172) +- Auto-link and create member accounts (lines 180-267) + +**Auto-Link Flow** (lines 180-267): +1. Check if auto-link enabled and email available +2. Find or create member by email +3. Call `OnAutoLinking` callback for customization +4. Add external login link +5. Sign in or request 2FA + +**Result Types** (lines 320-348): +- `ExternalLoginSignInResult.NotAllowed` - Login refused by callback +- `AutoLinkSignInResult.FailedNoEmail` - No email from provider +- `AutoLinkSignInResult.FailedCreatingUser` - User creation failed +- `AutoLinkSignInResult.FailedLinkingUser` - Link creation failed + +### Middleware + +**BootFailedMiddleware** (lines 17-81): +- Intercepts requests when `RuntimeLevel == BootFailed` +- Debug mode: Rethrows exception for stack trace +- Production: Shows `BootFailed.html` error page + +**PreviewAuthenticationMiddleware** (lines 22-84): +- Adds backoffice identity to principal for preview requests +- Skips client-side requests and backoffice paths +- Uses `IPreviewService.TryGetPreviewClaimsIdentityAsync()` + +--- + +## 4. Routing + +### UmbracoRouteValues (Routing/UmbracoRouteValues.cs) + +Container for routed request data: +- `PublishedRequest` - The resolved content request +- `ControllerActionDescriptor` - MVC routing info +- `TemplateName` - Resolved template name +- `DefaultActionName` = "Index" + +### Route Hijacking + +Implement `IRenderController` to handle specific content types: +```csharp +public class ProductController : Controller, IRenderController +{ + public IActionResult Index() => View(); +} +``` + +### Virtual Pages + +Implement `IVirtualPageController` for URL-to-content mapping without physical content nodes. + +--- + +## 5. Project-Specific Notes + +### Warning Suppressions (csproj lines 10-22) + +Multiple analyzer warnings suppressed: +- SA1117, SA1401, SA1134 - StyleCop formatting +- ASP0019 - Header dictionary usage +- CS0618/SYSLIB0051 - Obsolete references +- IDE0040/SA1400 - Access modifiers +- SA1649 - File name matching + +### InternalsVisibleTo + +```xml +Umbraco.Tests.UnitTests +``` + +### Known Technical Debt + +1. **MVC Global State** (UmbracoBuilderExtensions.cs:210-211): `AddControllersWithViews` modifies global app, order matters +2. **OptionsMonitor Hack** (AspNetCore/OptionsMonitorAdapter.cs:6): Temporary workaround for TypeLoader during ConfigureServices +3. **DisposeResources TODO** (UmbracoContext.cs:168-171): Empty dispose method marked for removal +4. **Pipeline Default Implementations** (IUmbracoPipelineFilter.cs:36,45): Default methods to remove in Umbraco 13 +5. **SignIn Manager Sharing** (MemberSignInManager.cs:319,325): Could share code with backoffice sign-in + +### Session Configuration + +Default session cookie (lines 227-231): +```csharp +options.Cookie.Name = "UMB_SESSION"; +options.Cookie.HttpOnly = true; +``` + +Can be overridden by calling `AddSession` after `AddWebComponents`. + +--- + +## Quick Reference + +### Startup Flow + +```csharp +// Program.cs +builder.Services.AddUmbraco(webHostEnvironment, config) + .AddUmbracoCore() + .AddMvcAndRazor() + .AddWebComponents() + .AddUmbracoProfiler() + .Build(); + +await app.BootUmbracoAsync(); +app.UseUmbraco() + .WithMiddleware(u => u.UseBackOffice()) + .WithEndpoints(u => u.UseBackOfficeEndpoints()); +``` + +### Key Interfaces + +| Interface | Implementation | Purpose | +|-----------|----------------|---------| +| `IUmbracoContext` | UmbracoContext | Request state | +| `IUmbracoContextFactory` | UmbracoContextFactory | Context creation | +| `IUmbracoContextAccessor` | (in Core) | Context access | +| `IMemberSignInManager` | MemberSignInManager | Member auth | +| `IUmbracoApplicationBuilder` | UmbracoApplicationBuilder | Pipeline config | + +### Extension Method Namespaces + +Most extensions are in `Umbraco.Extensions` namespace: +- `HttpRequestExtensions` - Request helpers +- `LinkGeneratorExtensions` - URL generation +- `BlockGridTemplateExtensions` - Block grid rendering +- `ImageCropperTemplateExtensions` - Image cropping + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Core` | Interface contracts | +| `Umbraco.Infrastructure` | Service implementations | +| `Umbraco.Examine.Lucene` | Search dependency | +| `Umbraco.PublishedCache.HybridCache` | Caching dependency | +| `Umbraco.Web.UI` | Main web application (references this) | +| `Umbraco.Cms.Api.Common` | API layer (references this) | diff --git a/src/Umbraco.Web.UI.Login/CLAUDE.md b/src/Umbraco.Web.UI.Login/CLAUDE.md new file mode 100644 index 000000000000..de5f9422d2e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Login/CLAUDE.md @@ -0,0 +1,266 @@ +# Umbraco.Web.UI.Login + +TypeScript/Lit login SPA for Umbraco CMS backoffice authentication. Provides the `` web component used in the login page, supporting local login, MFA, password reset, and user invitation flows. + +**Project Type**: TypeScript Library (Vite) +**Runtime**: Node.js >= 22, npm >= 10.9 +**Output**: ES Module library → `../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/` +**Dependencies**: @umbraco-cms/backoffice, Lit, Vite + +--- + +## 1. Architecture + +### Project Purpose + +This project builds the login single-page application: + +1. **Web Component** - `` custom element for authentication +2. **Authentication Flows** - Login, MFA, password reset, user invitation +3. **Localization** - Multi-language support (en, de, da, nb, nl, sv) +4. **API Client** - Generated from OpenAPI specification + +### Folder Structure + +``` +Umbraco.Web.UI.Login/ +├── src/ +│ ├── api/ # Generated API client +│ │ ├── client/ # HTTP client utilities +│ │ ├── core/ # Serializers, types +│ │ ├── sdk.gen.ts # Generated SDK +│ │ └── types.gen.ts # Generated types +│ ├── components/ +│ │ ├── layouts/ # Layout components +│ │ │ ├── auth-layout.element.ts +│ │ │ ├── confirmation-layout.element.ts +│ │ │ ├── error-layout.element.ts +│ │ │ └── new-password-layout.element.ts +│ │ ├── pages/ # Page components +│ │ │ ├── login.page.element.ts +│ │ │ ├── mfa.page.element.ts +│ │ │ ├── new-password.page.element.ts +│ │ │ ├── reset-password.page.element.ts +│ │ │ └── invite.page.element.ts +│ │ └── back-to-login-button.element.ts +│ ├── contexts/ +│ │ ├── auth.context.ts # Authentication state (86 lines) +│ │ └── auth.repository.ts # API calls (231 lines) +│ ├── controllers/ +│ │ └── slim-backoffice-initializer.ts # Minimal backoffice bootstrap +│ ├── localization/ +│ │ ├── lang/ # Language files (da, de, en, en-us, nb, nl, sv) +│ │ └── manifests.ts # Localization registration +│ ├── mocks/ # MSW mock handlers for development +│ │ ├── handlers/ +│ │ │ ├── login.handlers.ts +│ │ │ └── backoffice.handlers.ts +│ │ └── data/login.data.ts +│ ├── utils/ +│ │ ├── is-problem-details.function.ts +│ │ └── load-custom-view.function.ts +│ ├── auth.element.ts # Main component (404 lines) +│ ├── types.ts # Type definitions +│ ├── manifests.ts # Extension manifests +│ └── umbraco-package.ts # Package definition +├── public/ +│ └── favicon.svg +├── index.html # Development entry point +├── package.json # npm configuration (29 lines) +├── tsconfig.json # TypeScript config (25 lines) +├── vite.config.ts # Vite build config (20 lines) +├── .nvmrc # Node version (22) +└── .prettierrc.json # Prettier config +``` + +### Build Output + +Built assets are output to `Umbraco.Cms.StaticAssets`: +``` +../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/ +├── login.js # Main ES module +└── login.js.map # Source map +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +### Development + +```bash +cd src/Umbraco.Web.UI.Login + +# Install dependencies +npm install + +# Run development server with hot reload +npm run dev + +# Build for production (outputs to StaticAssets) +npm run build + +# Watch mode build +npm run watch + +# Preview production build +npm run preview +``` + +### API Generation + +```bash +# Regenerate API client from OpenAPI spec +npm run generate:server-api +``` + +Uses `@hey-api/openapi-ts` to generate TypeScript client from the Management API OpenAPI specification. + +--- + +## 3. Key Components + +### UmbAuthElement (src/auth.element.ts) + +Main `` web component (404 lines). + +**Attributes** (lines 172-204): +| Attribute | Type | Description | +|-----------|------|-------------| +| `disable-local-login` | boolean | Disables local login, external only | +| `background-image` | string | Login page background URL | +| `logo-image` | string | Logo URL | +| `logo-image-alternative` | string | Alternative logo (dark mode) | +| `username-is-email` | boolean | Use email as username | +| `allow-password-reset` | boolean | Show password reset link | +| `allow-user-invite` | boolean | Enable user invitation flow | +| `return-url` | string | Redirect URL after login | + +**Authentication Flows** (lines 379-396): +- Default: Login page with username/password +- `flow=mfa`: Multi-factor authentication +- `flow=reset`: Request password reset +- `flow=reset-password`: Set new password +- `flow=invite-user`: User invitation acceptance + +**Form Workaround** (lines 274-334): +Creates login form in light DOM (not shadow DOM) to support Chrome autofill/autocomplete, which doesn't work properly with shadow DOM inputs. + +### UmbAuthContext (src/contexts/auth.context.ts) + +Authentication state and API methods (86 lines): +- `login(data)` - Authenticate user +- `resetPassword(username)` - Request password reset +- `validatePasswordResetCode(userId, code)` - Validate reset code +- `newPassword(password, code, userId)` - Set new password +- `validateInviteCode(token, userId)` - Validate invitation +- `newInvitedUserPassword(...)` - Set invited user password +- `validateMfaCode(code, provider)` - MFA validation + +**Return Path Security** (lines 37-54): Validates return URL to prevent open redirect attacks - only allows same-origin URLs. + +### Localization + +Supported languages: English (en, en-us), Danish (da), German (de), Norwegian Bokmål (nb), Dutch (nl), Swedish (sv). + +--- + +## 4. Development with Mocks + +### MSW (Mock Service Worker) + +Development uses MSW for API mocking. Run `npm run dev` to start with mocks enabled. + +**Mock Files**: +- `handlers/login.handlers.ts` - Login, MFA, password reset +- `handlers/backoffice.handlers.ts` - Backoffice API +- `data/login.data.ts` - Test user data +- `customViews/` - Example custom login views + +--- + +## 5. Project-Specific Notes + +### Shadow DOM Limitation + +Chrome doesn't support autofill in shadow DOM inputs. The login form is created in light DOM via `#initializeForm()` (lines 274-334) and inserted with `insertAdjacentElement()`. See Chromium intent-to-ship discussion linked in code. + +### Slim Backoffice Controller + +`UmbSlimBackofficeController` provides minimal backoffice utilities (extension registry, localization, context API) without loading the full app. + +### Localization Wait Pattern (lines 242-265) + +Form waits for localization availability before rendering. Retries 40 times with 50ms interval (2 second max wait). + +### External Dependencies + +- `@umbraco-cms/backoffice` ^16.2.0 - Umbraco UI components, Lit element base +- `vite` ^7.2.0 - Build tooling +- `typescript` ^5.9.3 - Type checking +- `msw` ^2.11.3 - API mocking +- `@hey-api/openapi-ts` ^0.85.0 - API client generation + +### Known Technical Debt + +1. **UUI Color Variable** - Multiple files use `--uui-color-text-alt` with TODO to change when UUI adds muted text variable: + - `back-to-login-button.element.ts:35` + - `confirmation-layout.element.ts:41` + - `error-layout.element.ts:42` + - `login.page.element.ts:203,226` + - `new-password-layout.element.ts:221` + - `reset-password.page.element.ts:110` + +2. **API Client Error Types** (`api/client/client.gen.ts:207`): Error handling and types need improvement + +### TypeScript Configuration + +Key settings in `tsconfig.json`: +- `target`: ES2022 +- `experimentalDecorators`: true (for Lit decorators) +- `useDefineForClassFields`: false (Lit compatibility) +- `moduleResolution`: bundler (Vite) +- `strict`: true + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Development +npm run dev # Start dev server +npm run build # Build to StaticAssets +npm run watch # Watch mode + +# API +npm run generate:server-api # Regenerate API client +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `src/auth.element.ts` | Main `` component | +| `src/contexts/auth.context.ts` | Auth state management | +| `src/contexts/auth.repository.ts` | API calls | +| `vite.config.ts` | Build configuration | +| `package.json` | Dependencies and scripts | + +### Build Output + +Built files go to: +``` +../Umbraco.Cms.StaticAssets/wwwroot/umbraco/login/ +``` + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Cms.StaticAssets` | Receives built output | +| `Umbraco.Web.UI.Client` | Backoffice SPA (similar architecture) | +| `@umbraco-cms/backoffice` | NPM dependency for UI components | diff --git a/src/Umbraco.Web.UI/CLAUDE.md b/src/Umbraco.Web.UI/CLAUDE.md new file mode 100644 index 000000000000..cbad8c2a4b5b --- /dev/null +++ b/src/Umbraco.Web.UI/CLAUDE.md @@ -0,0 +1,320 @@ +# Umbraco.Web.UI + +Main ASP.NET Core web application for running Umbraco CMS. This is the development test site that references all Umbraco packages and provides a minimal startup configuration for testing and development. + +**Project Type**: ASP.NET Core Web Application +**SDK**: Microsoft.NET.Sdk.Web +**Target Framework**: net10.0 +**IsPackable**: false (not published as NuGet package) +**Namespace**: Umbraco.Cms.Web.UI + +--- + +## 1. Architecture + +### Project Purpose + +This is the **runnable Umbraco instance** used for development and testing. It: + +1. **References All Umbraco Packages** - Via the `Umbraco.Cms` meta-package +2. **Provides Minimal Startup** - Simple `Program.cs` with builder pattern +3. **Includes Development Tools** - Backoffice development mode support +4. **Demonstrates Patterns** - Example composers, views, and block templates + +### Folder Structure + +``` +Umbraco.Web.UI/ +├── Composers/ +│ ├── ControllersAsServicesComposer.cs # DI validation composer (66 lines) +│ └── UmbracoAppAuthenticatorComposer.cs # 2FA example (commented out) +├── Properties/ +│ └── launchSettings.json # Debug profiles (29 lines) +├── Views/ +│ ├── _ViewImports.cshtml # Global view imports +│ ├── page.cshtml # Generic page template +│ ├── BlockPage.cshtml # Block page template +│ ├── BlockTester.cshtml # Block testing template +│ └── Partials/ +│ ├── blockgrid/ # Block grid partials +│ │ ├── area.cshtml +│ │ ├── areas.cshtml +│ │ ├── items.cshtml +│ │ └── default.cshtml +│ ├── blocklist/ # Block list partials +│ │ ├── Components/textBlock.cshtml +│ │ └── default.cshtml +│ └── singleblock/ # Single block partials +│ └── default.cshtml +├── wwwroot/ +│ └── favicon.ico # Site favicon +├── umbraco/ # Runtime data directory +│ ├── Data/ # SQLite database, temp files +│ │ ├── Umbraco.sqlite.db # Development database +│ │ └── TEMP/ # ModelsBuilder, DistCache +│ ├── Logs/ # Serilog JSON logs +│ └── models/ # Generated content models +├── appsettings.json # Production config +├── appsettings.Development.json # Development overrides +├── appsettings.template.json # Template for new installs +├── appsettings.Development.template.json # Dev template +├── appsettings-schema.json # JSON schema reference +├── appsettings-schema.Umbraco.Cms.json # Full Umbraco schema (71KB) +├── umbraco-package-schema.json # Package manifest schema (495KB) +├── Program.cs # Application entry point (33 lines) +└── Umbraco.Web.UI.csproj # Project file (75 lines) +``` + +### Project Dependencies + +```xml + + +``` + +- **Umbraco.Cms** - Meta-package referencing all Umbraco packages +- **Umbraco.Cms.DevelopmentMode.Backoffice** - Hot reload for backoffice development + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +### Running the Application + +```bash +# Run with Kestrel (recommended) +dotnet run --project src/Umbraco.Web.UI + +# Run with watch for auto-reload +dotnet watch --project src/Umbraco.Web.UI + +# Run from Visual Studio +# Profile: "Umbraco.Web.UI" (https://localhost:44339) +# Profile: "IIS Express" (http://localhost:11000) +``` + +### Database + +SQLite is configured by default for development: +- Location: `src/Umbraco.Web.UI/umbraco/Data/Umbraco.sqlite.db` +- Connection string configured in `appsettings.json` + +--- + +## 3. Key Components + +### Program.cs (33 lines) + +Minimal Umbraco startup using builder pattern: + +```csharp +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.CreateUmbracoBuilder() + .AddBackOffice() + .AddWebsite() +#if UseDeliveryApi + .AddDeliveryApi() +#endif + .AddComposers() + .Build(); + +WebApplication app = builder.Build(); + +await app.BootUmbracoAsync(); + +app.UseUmbraco() + .WithMiddleware(u => + { + u.UseBackOffice(); + u.UseWebsite(); + }) + .WithEndpoints(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); + +await app.RunAsync(); +``` + +**Preprocessor Directives**: +- `#if UseDeliveryApi` - Enable/disable Delivery API +- `#if UseHttpsRedirect` - Enable/disable HTTPS redirection + +### ControllersAsServicesComposer (lines 36-38) + +Registers all controllers in the DI container for validation purposes: + +```csharp +public void Compose(IUmbracoBuilder builder) => builder.Services + .AddMvc() + .AddControllersAsServicesWithoutChangingActivator(); +``` + +**Purpose**: Detects ambiguous constructors in CMS controllers that would cause issues with `ServiceBasedControllerActivator`. Not shipped in `Umbraco.Templates`. + +### Launch Profiles (Properties/launchSettings.json) + +| Profile | URL | Command | +|---------|-----|---------| +| Umbraco.Web.UI | https://localhost:44339, http://localhost:11000 | Project | +| IIS Express | http://localhost:11000, SSL port 44339 | IISExpress | + +--- + +## 4. Configuration + +### appsettings.template.json + +Default configuration for new installations: + +| Setting | Value | Description | +|---------|-------|-------------| +| `ModelsMode` | `InMemoryAuto` | Auto-generate models in memory | +| `DefaultUILanguage` | `en-us` | Backoffice language | +| `HideTopLevelNodeFromPath` | `true` | Clean URLs | +| `TimeOut` | `00:20:00` | Session timeout | +| `UseHttps` | `false` | HTTPS enforcement | +| `UsernameIsEmail` | `true` | Email as username | +| `UserPassword.RequiredLength` | `10` | Minimum password length | + +### appsettings.Development.template.json + +Development-specific overrides: + +| Setting | Value | Description | +|---------|-------|-------------| +| `Hosting.Debug` | `true` | Debug mode enabled | +| `LuceneDirectoryFactory` | `TempFileSystemDirectoryFactory` | Examine indexes in temp | +| Console logging | `Async` sink | Serilog console output | +| Examine log levels | `Debug` | Detailed Examine logging | + +### Auto-Copy Build Target (csproj lines 65-73) + +MSBuild targets automatically copy template files if missing (appsettings.json and appsettings.Development.json). + +--- + +## 5. ModelsBuilder + +### InMemoryAuto Mode + +The project uses `InMemoryAuto` mode: +- Models auto-generated at runtime +- Source stored in `umbraco/Data/TEMP/InMemoryAuto/` +- Compiled assembly in `Compiled/` subdirectory +- `models.hash` tracks content type changes + +### Generated Models Location + +``` +umbraco/models/*.generated.cs # Source files (for reference) +umbraco/Data/TEMP/InMemoryAuto/ # Runtime compilation +``` + +### Razor Compilation Settings (csproj lines 50-51) + +Razor compilation is disabled for InMemoryAuto mode (`RazorCompileOnBuild=false`, `RazorCompileOnPublish=false`). + +--- + +## 6. ICU Globalization + +### App-Local ICU (csproj lines 39-40) + +Uses app-local ICU (`Microsoft.ICU.ICU4C.Runtime` v72.1.0.3) for consistent globalization across platforms. + +**Note**: Ensure ICU version matches between package reference and runtime option. Changes must also be made to `Umbraco.Templates`. + +--- + +## 7. Project-Specific Notes + +### Development vs Production + +This project is for **development only**: +- `IsPackable=false` - Not published as NuGet +- `EnablePackageValidation=false` - No package validation +- References `DevelopmentMode.Backoffice` for hot reload + +### Package Version Management + +```xml +false +``` + +Does NOT use central package management. Versions specified directly: +- `Microsoft.EntityFrameworkCore.Design` - For EF Core migrations tooling +- `Microsoft.Build.Tasks.Core` - Security fix for EFCore.Design dependency +- `Microsoft.ICU.ICU4C.Runtime` - Globalization + +### Umbraco Targets Import (csproj lines 19-20) + +Imports shared build configuration from `Umbraco.Cms.Targets` project (props and targets). + +### Excluded Views (csproj lines 55-57) + +Three Umbraco views excluded from content (UmbracoInstall, UmbracoLogin, UmbracoBackOffice) as they come from `Umbraco.Cms.StaticAssets` RCL. + +### Known Technical Debt + +1. **Warning Suppression** (csproj lines 12-16): `SA1119` - Unnecessary parenthesis to fix + +### Runtime Data (umbraco/ directory) + +| Directory | Contents | Git Status | +|-----------|----------|------------| +| `umbraco/Data/` | SQLite database, MainDom locks | Ignored | +| `umbraco/Logs/` | Serilog JSON trace logs | Ignored | +| `umbraco/models/` | Generated content models | Ignored | +| `umbraco/Data/TEMP/` | ModelsBuilder, DistCache | Ignored | + +--- + +## Quick Reference + +### Essential Commands + +```bash +# Run development site +dotnet run --project src/Umbraco.Web.UI + +# Clean runtime data (reset database) +rm -rf src/Umbraco.Web.UI/umbraco/Data + +# Reset to fresh install +rm src/Umbraco.Web.UI/appsettings.json +rm src/Umbraco.Web.UI/appsettings.Development.json +``` + +### Important Files + +| File | Purpose | +|------|---------| +| `Program.cs` | Application entry point | +| `appsettings.template.json` | Default configuration | +| `appsettings.Development.template.json` | Development overrides | +| `launchSettings.json` | Debug profiles | +| `Composers/ControllersAsServicesComposer.cs` | DI validation | + +### URLs + +| Environment | URL | +|-------------|-----| +| Development (Kestrel) | https://localhost:44339 | +| Development (HTTP) | http://localhost:11000 | +| Backoffice | /umbraco | +| Installer | /install (on first run) | + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Cms` | Meta-package (all dependencies) | +| `Umbraco.Cms.DevelopmentMode.Backoffice` | Hot reload support | +| `Umbraco.Cms.StaticAssets` | Backoffice/login views | +| `Umbraco.Cms.Targets` | Build configuration | +| `Umbraco.Web.Common` | Web functionality | diff --git a/src/Umbraco.Web.Website/CLAUDE.md b/src/Umbraco.Web.Website/CLAUDE.md new file mode 100644 index 000000000000..572ccfd76141 --- /dev/null +++ b/src/Umbraco.Web.Website/CLAUDE.md @@ -0,0 +1,245 @@ +# Umbraco.Web.Website + +Front-end website functionality for Umbraco CMS. Provides Surface controllers, member authentication/registration, content routing, and Razor view engine support for rendering published content. + +**Project Type**: Class Library (NuGet package) +**Target Framework**: net10.0 +**Package ID**: Umbraco.Cms.Web.Website +**Namespace**: Umbraco.Cms.Web.Website +**Dependencies**: Umbraco.Web.Common + +--- + +## 1. Architecture + +### Project Purpose + +This project provides the front-end website layer: + +1. **Surface Controllers** - POST/GET controllers for forms and user interactions +2. **Member Authentication** - Login, registration, profile, 2FA, external logins +3. **Content Routing** - Dynamic route value transformation for Umbraco content +4. **View Engines** - Custom Razor view locations and profiling wrappers +5. **Public Access** - Protected content with member authentication + +### Folder Structure + +``` +Umbraco.Web.Website/ +├── ActionResults/ +│ ├── RedirectToUmbracoPageResult.cs # Redirect to content by key (151 lines) +│ ├── RedirectToUmbracoUrlResult.cs # Redirect to current URL +│ └── UmbracoPageResult.cs # Return current page with ViewData +├── Cache/ +│ └── PartialViewCacheInvalidators/ +│ └── MemberPartialViewCacheInvalidator.cs +├── Collections/ +│ ├── SurfaceControllerTypeCollection.cs +│ └── SurfaceControllerTypeCollectionBuilder.cs +├── Controllers/ +│ ├── SurfaceController.cs # Base front-end controller (113 lines) +│ ├── UmbLoginController.cs # Member login (128 lines) +│ ├── UmbRegisterController.cs # Member registration +│ ├── UmbProfileController.cs # Member profile management +│ ├── UmbLoginStatusController.cs # Login status checking +│ ├── UmbExternalLoginController.cs # OAuth/external login +│ ├── UmbTwoFactorLoginController.cs # 2FA authentication +│ ├── RenderNoContentController.cs # No content page +│ └── UmbracoRenderingDefaultsOptions.cs # Default rendering options +├── DependencyInjection/ +│ ├── UmbracoBuilderExtensions.cs # AddWebsite() (91 lines) +│ └── UmbracoBuilder.MemberIdentity.cs # AddMemberExternalLogins() (22 lines) +├── Extensions/ +│ ├── HtmlHelperRenderExtensions.cs # Razor render helpers +│ ├── LinkGeneratorExtensions.cs # URL generation +│ ├── TypeLoaderExtensions.cs # Surface controller discovery +│ ├── UmbracoApplicationBuilder.Website.cs # UseWebsite() middleware +│ └── WebsiteUmbracoBuilderExtensions.cs +├── Middleware/ +│ └── BasicAuthenticationMiddleware.cs # Basic auth support +├── Models/ +│ ├── LoginModel.cs # (in Web.Common) +│ ├── RegisterModel.cs # Registration form +│ ├── ProfileModel.cs # Profile form +│ ├── RegisterModelBuilder.cs # Model builder +│ ├── ProfileModelBuilder.cs # Profile builder +│ ├── MemberModelBuilderBase.cs # Builder base class +│ ├── MemberModelBuilderFactory.cs # Factory for builders +│ └── NoNodesViewModel.cs # Empty site view model +├── Routing/ +│ ├── UmbracoRouteValueTransformer.cs # Dynamic route transformer (330 lines) +│ ├── UmbracoRouteValuesFactory.cs # Creates UmbracoRouteValues +│ ├── ControllerActionSearcher.cs # Finds controller actions +│ ├── PublicAccessRequestHandler.cs # Protected content handling +│ ├── FrontEndRoutes.cs # Route definitions +│ ├── EagerMatcherPolicy.cs # Early route matching +│ ├── NotFoundSelectorPolicy.cs # 404 handling +│ ├── SurfaceControllerMatcherPolicy.cs # Surface controller matching +│ ├── IControllerActionSearcher.cs +│ ├── IPublicAccessRequestHandler.cs +│ └── IUmbracoRouteValuesFactory.cs +├── Security/ +│ ├── MemberAuthenticationBuilder.cs # Auth configuration +│ └── MemberExternalLoginsBuilder.cs # External login providers +├── ViewEngines/ +│ ├── ProfilingViewEngine.cs # MiniProfiler wrapper +│ ├── ProfilingViewEngineWrapperMvcViewOptionsSetup.cs +│ ├── PluginRazorViewEngineOptionsSetup.cs # Plugin view locations +│ └── RenderRazorViewEngineOptionsSetup.cs # Render view locations +└── Umbraco.Web.Website.csproj # Project file (34 lines) +``` + +### Request Flow + +``` +HTTP Request → UmbracoRouteValueTransformer.TransformAsync() + ↓ + Check UmbracoContext exists + ↓ + Check routable document filter + ↓ + RouteRequestAsync() → IPublishedRouter + ↓ + UmbracoRouteValuesFactory.CreateAsync() + ↓ + PublicAccessRequestHandler (protected content) + ↓ + Check for Surface controller POST (ufprt) + ↓ (if POST) + HandlePostedValues() → Surface Controller + ↓ (else) + Return route values → Render Controller +``` + +--- + +## 2. Commands + +**For Git workflow and build commands**, see [repository root](../../CLAUDE.md). + +--- + +## 3. Key Components + +### SurfaceController (Controllers/SurfaceController.cs) + +Base class for front-end form controllers (113 lines). + +**Key Features**: +- `[AutoValidateAntiforgeryToken]` applied by default (line 19) +- Extends `PluginController` with `IPublishedUrlProvider` +- `CurrentPage` property for accessing routed content (lines 40-53) +- Redirect helpers for content by key/entity (lines 58-102) +- Supports query strings on redirects + +### UmbracoRouteValueTransformer (Routing/UmbracoRouteValueTransformer.cs) + +Dynamic route value transformer for front-end content routing (330 lines). + +**TransformAsync Flow** (lines 120-194): +1. Check `UmbracoContext` exists (skip client-side requests) +2. Check `IRoutableDocumentFilter.IsDocumentRequest()` (skip static files) +3. Check no existing dynamic routing active +4. Check `IDocumentUrlService.HasAny()` → `RenderNoContentController` if empty +5. `RouteRequestAsync()` → create published request via `IPublishedRouter` +6. `UmbracoRouteValuesFactory.CreateAsync()` → resolve controller/action +7. `PublicAccessRequestHandler.RewriteForPublishedContentAccessAsync()` → handle protected content +8. Store `UmbracoRouteValues` in `HttpContext.Features` +9. Check for Surface controller POST via `ufprt` parameter +10. Return route values for controller/action + +**Surface Controller POST Handling** (lines 244-309): +- Detects `ufprt` (Umbraco Form Post Route Token) in request +- Decrypts via `EncryptionHelper.DecryptAndValidateEncryptedRouteString()` +- Extracts controller, action, area from encrypted data +- Routes to Surface controller action + +**Reserved Keys for ufprt** (lines 321-327): +- `c` = Controller +- `a` = Action +- `ar` = Area + +### AddWebsite() (DependencyInjection/UmbracoBuilderExtensions.cs) + +Main DI registration entry point (91 lines). + +**Key Registrations** (lines 34-90): Surface controller discovery, view engine setup, routing services, matcher policies, member models, distributed cache, ModelsBuilder, and member identity. + +### Member Controllers + +Built-in Surface controllers for member functionality: + +| Controller | Purpose | Key Action | +|------------|---------|------------| +| `UmbLoginController` | Member login | `HandleLogin(LoginModel)` | +| `UmbRegisterController` | Member registration | `HandleRegister(RegisterModel)` | +| `UmbProfileController` | Profile management | `HandleUpdate(ProfileModel)` | +| `UmbLoginStatusController` | Login status | `HandleLogout()` | +| `UmbExternalLoginController` | OAuth login | `ExternalLogin(provider)` | +| `UmbTwoFactorLoginController` | 2FA verification | `HandleTwoFactorLogin()` | + +### UmbLoginController (Controllers/UmbLoginController.cs) + +Member login Surface controller (128 lines). + +**HandleLogin** (lines 53-114): Validates credentials, handles 2FA/lockout, redirects on success. Uses encrypted redirect URLs (lines 120-126). + +### RedirectToUmbracoPageResult (ActionResults/RedirectToUmbracoPageResult.cs) + +Action result for redirecting to Umbraco content (151 lines). + +Implements `IKeepTempDataResult` to preserve TempData. Resolves content by `Guid` key or `IPublishedContent`, supports query strings. + +--- + +## 4. View Engine Configuration + +**Custom View Locations**: +- `RenderRazorViewEngineOptionsSetup` - /Views/{controller}/{action}.cshtml +- `PluginRazorViewEngineOptionsSetup` - /App_Plugins/{area}/Views/... +- `ProfilingViewEngine` - MiniProfiler wrapper for view timing + +--- + +## 5. Project-Specific Notes + +**Warning Suppressions** (csproj lines 10-18): ASP0019, CS0618, SA1401, SA1649, IDE1006 + +**InternalsVisibleTo**: Umbraco.Tests.UnitTests, Umbraco.Tests.Integration + +**Known Technical Debt**: +1. Load balanced setup needs review (UmbracoBuilderExtensions.cs:47) +2. UmbracoContext re-assignment pattern (UmbracoRouteValueTransformer.cs:229-232) +3. Obsolete constructor without IDocumentUrlService (removal in Umbraco 18) + +**Surface Controller Form Token (ufprt)**: Encrypted route token prevents tampering. Hidden field includes controller/action/area encrypted via Data Protection, decrypted in `UmbracoRouteValueTransformer.GetFormInfo()`. + +**Public Access**: `PublicAccessRequestHandler` checks member authentication, redirects to login if needed. + +--- + +## Quick Reference + +**Startup**: +```csharp +builder.CreateUmbracoBuilder().AddWebsite().AddMembersIdentity().Build(); +app.UseUmbraco().WithMiddleware(u => u.UseWebsite()).WithEndpoints(u => u.UseWebsiteEndpoints()); +``` + +### Key Interfaces + +| Interface | Implementation | Purpose | +|-----------|----------------|---------| +| `IControllerActionSearcher` | ControllerActionSearcher | Find controller actions | +| `IUmbracoRouteValuesFactory` | UmbracoRouteValuesFactory | Create route values | +| `IPublicAccessRequestHandler` | PublicAccessRequestHandler | Protected content | +| `IRoutableDocumentFilter` | RoutableDocumentFilter | Filter routable requests | + +### Related Projects + +| Project | Relationship | +|---------|--------------| +| `Umbraco.Web.Common` | Base controllers, UmbracoContext | +| `Umbraco.Core` | Interfaces, routing contracts | +| `Umbraco.Infrastructure` | Service implementations | +| `Umbraco.Web.UI` | Main web application (references this) | From aeff2d1a4d849af89a9235ee0126e34e78395836 Mon Sep 17 00:00:00 2001 From: NillasKA Date: Thu, 27 Nov 2025 10:51:45 +0100 Subject: [PATCH 04/45] Moving files to correct folder and namespace --- .../{ValueConverters => }/IBlockGridConfiguration.cs | 2 +- .../{ValueConverters => }/IRichTextBlockConfiguration.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/Umbraco.Core/PropertyEditors/{ValueConverters => }/IBlockGridConfiguration.cs (84%) rename src/Umbraco.Core/PropertyEditors/{ValueConverters => }/IRichTextBlockConfiguration.cs (65%) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs b/src/Umbraco.Core/PropertyEditors/IBlockGridConfiguration.cs similarity index 84% rename from src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs rename to src/Umbraco.Core/PropertyEditors/IBlockGridConfiguration.cs index 6d9a76bb24d2..208536841c6f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IBlockGridConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/IBlockGridConfiguration.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; +namespace Umbraco.Cms.Core.PropertyEditors; public interface IBlockGridConfiguration : IBlockConfiguration { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs b/src/Umbraco.Core/PropertyEditors/IRichTextBlockConfiguration.cs similarity index 65% rename from src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs rename to src/Umbraco.Core/PropertyEditors/IRichTextBlockConfiguration.cs index f0b6e05c7012..be2785e49e5e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IRichTextBlockConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/IRichTextBlockConfiguration.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; +namespace Umbraco.Cms.Core.PropertyEditors; public interface IRichTextBlockConfiguration : IBlockConfiguration { From d90758b26dda214bd415c761c8f037f5c87f44f7 Mon Sep 17 00:00:00 2001 From: NillasKA Date: Thu, 27 Nov 2025 12:30:31 +0100 Subject: [PATCH 05/45] Removing unused using statements --- src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs | 2 -- src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs index 029920846bde..859515822fc8 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; - namespace Umbraco.Cms.Core.PropertyEditors; /// diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index 4dbbad33cca3..4f90d2a12577 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -1,5 +1,3 @@ -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; - namespace Umbraco.Cms.Core.PropertyEditors; /// From 615cb76288df5ca6ff4425a97d4cccfd16b99782 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 27 Nov 2025 15:39:27 +0100 Subject: [PATCH 06/45] Block editors: adds prefix to workspace title (closes #20588) (#20884) * Adds `$settings` to the block workspace label renderer * Adds a prefix to the Block workspace title * Imports tidy-up --- .../block-workspace-editor.element.ts | 14 ++++------ .../workspace/block-workspace.context.ts | 27 ++++++++++++++----- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace-editor.element.ts index a76ed3ea2f37..9ebd8991c52f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace-editor.element.ts @@ -1,8 +1,6 @@ import { UMB_BLOCK_WORKSPACE_CONTEXT } from './index.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { customElement, css, html, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-block-workspace-editor') export class UmbBlockWorkspaceEditorElement extends UmbLitElement { @@ -11,10 +9,9 @@ export class UmbBlockWorkspaceEditorElement extends UmbLitElement { this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => { if (context) { this.observe( - observeMultiple([context.isNew, context.name]), - ([isNew, name]) => { - this._headline = - this.localize.term(isNew ? 'general_add' : 'general_edit') + ' ' + this.localize.string(name); + context.name, + (name) => { + this._headline = this.localize.string(name); }, 'observeOwnerContentElementTypeName', ); @@ -28,11 +25,10 @@ export class UmbBlockWorkspaceEditorElement extends UmbLitElement { private _headline: string = ''; override render() { - return html` `; + return html``; } static override readonly styles = [ - UmbTextStyles, css` :host { display: block; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 4a8fcbb12fb1..08a6090eb4be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -113,8 +113,8 @@ export class UmbBlockWorkspaceContext { - this.#renderLabel(contentValues); + async ([contentValues, settingsValues]) => { + this.#renderLabel(contentValues, settingsValues); }, 'observeContentForLabelRender', ); @@ -243,11 +243,14 @@ export class UmbBlockWorkspaceContext | undefined) { + async #renderLabel( + contentValues: Array | undefined, + settingsValues: Array | undefined, + ) { const valueObject = {} as Record; if (contentValues) { for (const property of contentValues) { @@ -255,12 +258,22 @@ export class UmbBlockWorkspaceContext requestAnimationFrame(() => resolve(true))); - const result = this.#labelRender.toString(); - this.#name.setValue(result); - this.view.setTitle(result); + const prefix = this.getIsNew() === true ? '#general_add' : '#general_edit'; + const label = this.#labelRender.toString(); + const title = `${prefix} ${label}`; + this.#name.setValue(title); + this.view.setTitle(title); } #allowNavigateAway = false; From f44e9328d7fdfe2bf3ad408ce045e43d6eeb606e Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 27 Nov 2025 15:22:50 +0000 Subject: [PATCH 07/45] Extensions: Adds all yet unused Lit directives to @umbraco-cms/backoffice/external/lit (closes #20961) (#20963) * Adds choose directive to @umbraco-cms/backoffice/external/lit This can then allow choose to be imported like so import { html, customElement, LitElement, property, css, choose } from '@umbraco-cms/backoffice/external/lit'; * Exports all of Lits directives for @umbraco-cms/backoffice/external/lit Also puts them in alphabetical order to help add any new ones Lit may add in the future --- src/Umbraco.Web.UI.Client/src/external/lit/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/external/lit/index.ts b/src/Umbraco.Web.UI.Client/src/external/lit/index.ts index a1f349289879..0d81284e616f 100644 --- a/src/Umbraco.Web.UI.Client/src/external/lit/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/lit/index.ts @@ -1,12 +1,24 @@ export * from 'lit'; export * from 'lit/decorators.js'; export { directive, AsyncDirective, type PartInfo } from 'lit/async-directive.js'; +export * from 'lit/directives/async-append.js'; +export * from 'lit/directives/async-replace.js'; +export * from 'lit/directives/cache.js'; +export * from 'lit/directives/choose.js'; export * from 'lit/directives/class-map.js'; +export * from 'lit/directives/guard.js'; export * from 'lit/directives/if-defined.js'; +export * from 'lit/directives/join.js'; +export * from 'lit/directives/keyed.js'; +export * from 'lit/directives/live.js'; export * from 'lit/directives/map.js'; +export * from 'lit/directives/range.js'; export * from 'lit/directives/ref.js'; export * from 'lit/directives/repeat.js'; export * from 'lit/directives/style-map.js'; +export * from 'lit/directives/template-content.js'; export * from 'lit/directives/unsafe-html.js'; +export * from 'lit/directives/unsafe-mathml.js'; +export * from 'lit/directives/unsafe-svg.js'; export * from 'lit/directives/until.js'; export * from 'lit/directives/when.js'; From 5ab93454d9f06360c5fe405c9e2e022ed95320de Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Fri, 28 Nov 2025 10:58:04 +0900 Subject: [PATCH 08/45] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 17debcd84643..a30263281270 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "17.0.0", + "version": "17.0.1", "assemblyVersion": { "precision": "build" }, From e6b99938db307f76bb158425d8e2c60d6fa181e3 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:25:06 +0900 Subject: [PATCH 09/45] Delivery API: Only add default strategy if delivery API is not registered. (#20982) * Only add if not already present * Update src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Kenn Jacobsen --- .../DependencyInjection/WebhooksBuilderExtensions.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs index 81d764f69793..fa3bcdb71aae 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Api.Common.Accessors; using Umbraco.Cms.Api.Common.Rendering; using Umbraco.Cms.Api.Management.Factories; @@ -16,9 +16,10 @@ internal static IUmbracoBuilder AddWebhooks(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.AddMapDefinition(); - // deliveryApi will overwrite these more basic ones. - builder.Services.AddScoped(); - builder.Services.AddSingleton(); + // We have to use TryAdd here, as if they are registered by the delivery API, we don't want to register them + // Delivery API will also overwrite these IF it is enabled. + builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); return builder; } From 9c038bc68b6bc1cbfff2a4eb1904194d0de7020d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Nov 2025 09:48:06 +0100 Subject: [PATCH 10/45] Re-enable package validation (#20964) * Re-enable package validation. * Remove unnecessary supressions file. * Removed unnecessary suppressions. * Restored and obsoleted all overload. --- Directory.Build.props | 4 +-- .../MemberType/CopyMemberTypeController.cs | 6 +++++ .../CompatibilitySuppressions.xml | 10 +------ .../CompatibilitySuppressions.xml | 11 ++++++++ .../CompatibilitySuppressions.xml | 27 ++++++++++++++++--- 5 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml diff --git a/Directory.Build.props b/Directory.Build.props index 09dd04749dd2..28035831ba21 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -40,8 +40,8 @@ false - false - 16.0.0 + true + 17.0.0 true true diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs index e88e50a1946c..02e1507f7627 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/CopyMemberTypeController.cs @@ -20,6 +20,12 @@ public class CopyMemberTypeController : MemberTypeControllerBase public CopyMemberTypeController(IMemberTypeService memberTypeService) => _memberTypeService = memberTypeService; + [Obsolete("Please use the overload that includes all parameters. Scheduled for removal in Umbraco 19.")] + [NonAction] + public async Task Copy( + CancellationToken cancellationToken, + Guid id) => await Copy(cancellationToken, id, null); + [HttpPost("{id:guid}/copy")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status201Created)] diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index 3cd59acc387e..0497d618a920 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -1,11 +1,3 @@  - - - CP0002 - M:Umbraco.Cms.Core.Configuration.Models.ContentSettings.get_Error404Collection - lib/net9.0/Umbraco.Core.dll - lib/net9.0/Umbraco.Core.dll - true - - \ No newline at end of file + \ No newline at end of file diff --git a/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml new file mode 100644 index 000000000000..50897d00145b --- /dev/null +++ b/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml @@ -0,0 +1,11 @@ + + + + + CP0002 + M:Umbraco.Cms.Tests.Common.Builders.TemplateBuilder.CreateTextPageTemplate(System.String) + lib/net10.0/Umbraco.Tests.Common.dll + lib/net10.0/Umbraco.Tests.Common.dll + true + + \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml index f5c79dd905fb..53b07fb764db 100644 --- a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml +++ b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml @@ -1,11 +1,32 @@  + + CP0001 + T:Umbraco.Cms.Tests.Integration.ManagementApi.DocumentType.Root.ChildrenDocumentTypeTreeControllerTests + lib/net10.0/Umbraco.Tests.Integration.dll + lib/net10.0/Umbraco.Tests.Integration.dll + true + + + CP0001 + T:Umbraco.Cms.Tests.Integration.ManagementApi.DocumentType.Root.RootDocumentTypeTreeControllerTests + lib/net10.0/Umbraco.Tests.Integration.dll + lib/net10.0/Umbraco.Tests.Integration.dll + true + + + CP0002 + M:Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine.IndexInitializer.#ctor(Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.PropertyEditors.PropertyEditorCollection,Umbraco.Cms.Core.PropertyEditors.MediaUrlGeneratorCollection,Umbraco.Cms.Core.Scoping.IScopeProvider,Microsoft.Extensions.Logging.ILoggerFactory,Microsoft.Extensions.Options.IOptions{Umbraco.Cms.Core.Configuration.Models.ContentSettings},Umbraco.Cms.Core.Services.ILocalizationService,Umbraco.Cms.Core.Services.IContentTypeService,Umbraco.Cms.Core.Services.IDocumentUrlService,Umbraco.Cms.Core.Services.ILanguageService) + lib/net10.0/Umbraco.Tests.Integration.dll + lib/net10.0/Umbraco.Tests.Integration.dll + true + CP0002 - M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.ContentEditingServiceTests.Updating_Single_Variant_Name_Does_Not_Change_Update_Dates_Of_Other_Vaiants - lib/net9.0/Umbraco.Tests.Integration.dll - lib/net9.0/Umbraco.Tests.Integration.dll + M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.ContentEditingServiceTests.Relate(Umbraco.Cms.Core.Models.IContent,Umbraco.Cms.Core.Models.IContent) + lib/net10.0/Umbraco.Tests.Integration.dll + lib/net10.0/Umbraco.Tests.Integration.dll true \ No newline at end of file From f99e9394f84139a9688cf6247b4975367a5d9519 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 10:06:20 +0100 Subject: [PATCH 11/45] Culture and Hostnames: Add ability to sort hostnames (closes #20691) (#20826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding the sorter controller, and fixing some ui elements so you are able to drag the hostname elements around to sort them * Fixed sorting * Changed the html structure and tweaked around with the css to make it look better. Added a description for the Culture section. Alligned the rendered text to allign better with the name "Culture and Hostnames" * Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts Forgot to remove this after I was done testing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts Changing grid-gap to just gap Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Removed the disabled and readonly props I added since they are not needed. Removed the conditional rendering that was attached to the readonly and disabled properties * Removed the item id from the element and changed css and sorter logic to target the hostname-item class instead * Updated test * Bumped helpers --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Niels Lyngsø Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Andreas Zerbst --- .../src/assets/lang/en.ts | 38 +-- .../culture-and-hostnames-modal.element.ts | 222 +++++++++++++----- .../package-lock.json | 8 +- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../Content/CultureAndHostnames.spec.ts | 1 + 5 files changed, 182 insertions(+), 89 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index fa65c6995991..d2e056d070e4 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -113,26 +113,26 @@ export default { }, assignDomain: { permissionDenied: 'Permission denied.', - addNew: 'Add new domain', - addCurrent: 'Add current domain', + addNew: 'Add new hostname', + addCurrent: 'Add current hostname', remove: 'remove', invalidNode: 'Invalid node.', - invalidDomain: 'One or more domains have an invalid format.', - duplicateDomain: 'Domain has already been assigned.', - language: 'Language', - domain: 'Domain', - domainCreated: "New domain '%0%' has been created", - domainDeleted: "Domain '%0%' is deleted", - domainExists: "Domain '%0%' has already been assigned", - domainUpdated: "Domain '%0%' has been updated", - orEdit: 'Edit Current Domains', + invalidDomain: 'One or more hostnames have an invalid format.', + duplicateDomain: 'Hostname has already been assigned.', + language: 'Culture', + domain: 'Hostname', + domainCreated: "New hostname '%0%' has been created", + domainDeleted: "Hostname '%0%' is deleted", + domainExists: "Hostname '%0%' has already been assigned", + domainUpdated: "Hostname '%0%' has been updated", + orEdit: 'Edit Current Hostnames', domainHelpWithVariants: - 'Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". Furthermore also one-level paths in domains are supported, e.g. "example.com/en" or "/en".', + 'Valid hostnames are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". Furthermore also one-level paths in hostnames are supported, e.g. "example.com/en" or "/en".', inherit: 'Inherit', setLanguage: 'Culture', setLanguageHelp: - 'Set the culture for nodes below the current node,
or inherit culture from parent nodes. Will also apply
to the current node, unless a domain below applies too.', - setDomains: 'Domains', + 'Set the culture for nodes below the current node, or inherit culture from parent nodes. Will also apply to the current node, unless a hostname below applies too.', + setDomains: 'Hostnames', }, buttons: { clearSelection: 'Clear selection', @@ -191,7 +191,7 @@ export default { save: 'Media saved', }, auditTrails: { - assigndomain: 'Domain assigned: %0%', + assigndomain: 'Hostname assigned: %0%', atViewingFor: 'Viewing for', delete: 'Content deleted', unpublish: 'Content unpublished', @@ -209,7 +209,7 @@ export default { custom: '%0%', contentversionpreventcleanup: 'Clean up disabled for version: %0%', contentversionenablecleanup: 'Clean up enabled for version: %0%', - smallAssignDomain: 'Assign Domain', + smallAssignDomain: 'Assign Hostname', smallCopy: 'Copy', smallPublish: 'Publish', smallPublishVariant: 'Publish', @@ -1562,9 +1562,9 @@ export default { dictionaryItemExportedError: 'An error occurred while exporting the dictionary item(s)', dictionaryItemImported: 'The following dictionary item(s) has been imported!', publishWithNoDomains: - 'Domains are not configured for multilingual site, please contact an administrator, see log for more information', + 'Hostnames are not configured for multilingual site, please contact an administrator, see log for more information', publishWithMissingDomain: - 'There is no domain configured for %0%, please contact an administrator, see log for more information', + 'There is no hostname configured for %0%, please contact an administrator, see log for more information', copySuccessMessage: 'Your system information has successfully been copied to the clipboard', cannotCopyInformation: 'Could not copy your system information to the clipboard', webhookSaved: 'Webhook saved', @@ -2786,7 +2786,7 @@ export default { minimalLevelDescription: 'We will only send an anonymised site ID to let us know that the site exists.', basicLevelDescription: 'We will send an anonymised site ID, Umbraco version, and packages installed', detailedLevelDescription: - 'We will send:
  • Anonymised site ID, Umbraco version, and packages installed.
  • Number of: Root nodes, Content nodes, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • Configuration settings: ModelsBuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymised information being collected.
', + 'We will send:
  • Anonymised site ID, Umbraco version, and packages installed.
  • Number of: Root nodes, Content nodes, Media, Document Types, Templates, Languages, Hostnames, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • Configuration settings: ModelsBuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymised information being collected.
', }, routing: { routeNotFoundTitle: 'Not found', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts index b03235b7ccb2..104e74e046f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts @@ -3,19 +3,49 @@ import type { UmbCultureAndHostnamesModalData, UmbCultureAndHostnamesModalValue, } from './culture-and-hostnames-modal.token.js'; -import { css, customElement, html, query, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { + css, + customElement, + html, + query, + repeat, + state, + type PropertyValues, +} from '@umbraco-cms/backoffice/external/lit'; import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { DomainPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import type { UUIInputEvent, UUIPopoverContainerElement, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +interface UmbDomainPresentationModel { + unique: string; + domainName: string; + isoCode: string; +} @customElement('umb-culture-and-hostnames-modal') export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< UmbCultureAndHostnamesModalData, UmbCultureAndHostnamesModalValue > { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => { + return element.getAttribute('data-sort-entry-id'); + }, + getUniqueOfModel: (modelEntry: UmbDomainPresentationModel) => { + return modelEntry.unique; + }, + itemSelector: '.hostname-item', + containerSelector: '#sorter-wrapper', + onChange: ({ model }) => { + const oldValue = this._domains; + this._domains = model; + this.requestUpdate('_domains', oldValue); + }, + }); + #documentRepository = new UmbDocumentCultureAndHostnamesRepository(this); #languageCollectionRepository = new UmbLanguageCollectionRepository(this); @@ -28,13 +58,20 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< private _defaultIsoCode?: string | null; @state() - private _domains: Array = []; + private _domains: Array = []; @query('#more-options') popoverContainerElement?: UUIPopoverContainerElement; // Init + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('_domains')) { + // Update sorter whenever _domains changes + this.#sorter.setModel(this._domains); + } + } + override firstUpdated() { this.#unique = this.data?.unique; this.#requestLanguages(); @@ -47,7 +84,7 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< if (!data) return; this._defaultIsoCode = data.defaultIsoCode; - this._domains = data.domains; + this._domains = data.domains.map((domain) => ({ ...domain, unique: UmbId.new() })); } async #requestLanguages() { @@ -57,7 +94,8 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< } async #handleSave() { - this.value = { defaultIsoCode: this._defaultIsoCode, domains: this._domains }; + const cleanDomains = this._domains.map((domain) => ({ domainName: domain.domainName, isoCode: domain.isoCode })); + this.value = { defaultIsoCode: this._defaultIsoCode, domains: cleanDomains }; const { error } = await this.#documentRepository.updateCultureAndHostnames(this.#unique!, this.value); if (!error) { @@ -101,18 +139,61 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.popoverContainerElement?.hidePopover(); - this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: window.location.host }]; + this._domains = [ + ...this._domains, + { isoCode: defaultModel?.unique ?? '', domainName: window.location.host, unique: UmbId.new() }, + ]; + + this.#focusNewItem(); } else { - this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '' }]; + this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '', unique: UmbId.new() }]; + + this.#focusNewItem(); } } + async #focusNewItem() { + await this.updateComplete; + const items = this.shadowRoot?.querySelectorAll('div.hostname-item') as NodeListOf; + const newItem = items[items.length - 1]; + const firstInput = newItem?.querySelector('uui-input') as HTMLElement; + firstInput?.focus(); + } + // Renders override render() { return html` - ${this.#renderCultureSection()} ${this.#renderDomainSection()} + +
+ + + + ${this.localize.term('assignDomain_inherit')} + + ${this.#renderLanguageModelOptions()} + + +
+
+
+ +
${this.#renderDomains()} ${this.#renderAddNewDomainButton()}
+
- ${this.localize.term('assignDomain_language')} - - - - ${this.localize.term('assignDomain_inherit')} - - ${this.#renderLanguageModelOptions()} - - - - `; - } - - #renderDomainSection() { - return html` - - - Valid domain names are: "example.com", "www.example.com", "example.com:8080", or - "https://www.example.com/".
Furthermore also one-level paths in domains are supported, eg. - "example.com/en" or "/en". -
- ${this.#renderDomains()} ${this.#renderAddNewDomainButton()} -
- `; - } - #renderDomains() { - if (!this._domains?.length) return; return html` -
+
${repeat( this._domains, - (domain) => domain.isoCode, + (domain) => domain.unique, (domain, index) => html` - this.#onChangeDomainHostname(e, index)}> - this.#onChangeDomainLanguage(e, index)}> - ${this.#renderLanguageModelOptions()} - - this.#onRemoveDomain(index)}> - - +
+ +
+ this.#onChangeDomainHostname(e, index)}> + this.#onChangeDomainLanguage(e, index)}> + ${this.#renderLanguageModelOptions()} + + this.#onRemoveDomain(index)}> + + +
+
`, )}
@@ -229,6 +281,9 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< static override styles = [ UmbTextStyles, css` + umb-property-layout[orientation='vertical'] { + padding: 0; + } uui-button-group { width: 100%; } @@ -241,12 +296,49 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement< flex-grow: 0; } - #domains { - margin-top: var(--uui-size-layout-1); - margin-bottom: var(--uui-size-2); + .hostname-item { + position: relative; + display: flex; + gap: var(--uui-size-1); + align-items: center; + } + + .hostname-wrapper { + position: relative; + flex: 1; display: grid; grid-template-columns: 1fr 1fr auto; - grid-gap: var(--uui-size-1); + gap: var(--uui-size-1); + } + + #sorter-wrapper { + margin-bottom: var(--uui-size-2); + display: flex; + flex-direction: column; + gap: var(--uui-size-1); + } + + .handle { + cursor: grab; + } + + .handle:active { + cursor: grabbing; + } + #action { + display: block; + } + + .--umb-sorter-placeholder { + position: relative; + visibility: hidden; + } + .--umb-sorter-placeholder::after { + content: ''; + position: absolute; + inset: 0px; + border-radius: var(--uui-border-radius); + border: 1px dashed var(--uui-color-divider-emphasis); } `, ]; diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index bdda714dd7cb..5fa63bbeff09 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.42", - "@umbraco/playwright-testhelpers": "^17.0.11", + "@umbraco/playwright-testhelpers": "^17.0.12", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.11.tgz", - "integrity": "sha512-+2zijm64oppD17NQg0om7ip1iFJsTQy0ugGgQamZvpf2mUPoGV2CpIz7enPY5YmrQerPacS/1riBMWx/eafqHA==", + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.12.tgz", + "integrity": "sha512-GhOj5ytXEY1sG8Nt6CAkJcqjxfRWUFKLl63SCk2quew/1rLCeaUV5I2+YJ3LkfQetMdDlqtMVZP7FdMk+iWJNQ==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.42", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index e42d11593e42..1e142aba1ddf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.42", - "@umbraco/playwright-testhelpers": "^17.0.11", + "@umbraco/playwright-testhelpers": "^17.0.12", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts index 766a628b3979..7db43f1611c7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts @@ -36,6 +36,7 @@ test('can add a culture', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); + await umbracoUi.content.clickAddNewHostnameButton(); await umbracoUi.content.selectCultureLanguageOption(languageName); await umbracoUi.content.clickSaveModalButton(); From 050b37ed1ad4a28e741ee2a5547e4f8778270be0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:21:04 +0100 Subject: [PATCH 12/45] Installer: Removes unused telemetry functionality (#20995) * fix: removes the non-functioning installer telemetry and obsoletes all InstallHelper functionality * fix: deprecates related cookie * fix: adds ActivatorUtilitiesConstructor for DI * fix: obsoletes and removes more telemetry functionality * fix: removes uneeded modifier * docs: removes docs * 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> --- .../Install/SettingsInstallController.cs | 15 ++-- src/Umbraco.Core/Constants-Web.cs | 1 + .../Repositories/IInstallationRepository.cs | 2 + .../Repositories/InstallationRepository.cs | 26 ++---- .../Install/InstallHelper.cs | 81 +------------------ .../Steps/RegisterInstallCompleteStep.cs | 16 ++-- 6 files changed, 31 insertions(+), 110 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs index 518edb9afe8f..466541f9e39e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Install/SettingsInstallController.cs @@ -1,6 +1,7 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Api.Management.ViewModels.Installer; @@ -12,16 +13,23 @@ namespace Umbraco.Cms.Api.Management.Controllers.Install; [ApiVersion("1.0")] public class SettingsInstallController : InstallControllerBase { - private readonly InstallHelper _installHelper; private readonly IInstallSettingsFactory _installSettingsFactory; private readonly IUmbracoMapper _mapper; + [Obsolete("Please use the constructor without the InstallHelper parameter. Scheduled for removal in Umbraco 19.")] public SettingsInstallController( InstallHelper installHelper, IInstallSettingsFactory installSettingsFactory, IUmbracoMapper mapper) + : this(installSettingsFactory, mapper) + { + } + + [ActivatorUtilitiesConstructor] + public SettingsInstallController( + IInstallSettingsFactory installSettingsFactory, + IUmbracoMapper mapper) { - _installHelper = installHelper; _installSettingsFactory = installSettingsFactory; _mapper = mapper; } @@ -32,9 +40,6 @@ public SettingsInstallController( [ProducesResponseType(typeof(InstallSettingsResponseModel), StatusCodes.Status200OK)] public async Task Settings(CancellationToken cancellationToken) { - // Register that the install has started - await _installHelper.SetInstallStatusAsync(false, string.Empty); - InstallSettingsModel installSettings = _installSettingsFactory.GetInstallSettings(); InstallSettingsResponseModel responseModel = _mapper.Map(installSettings)!; diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index de5e5d1f7a51..3ad4095a7cd0 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -17,6 +17,7 @@ public static class Web ///
public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; + [Obsolete("InstallerCookieName is no longer used and will be removed in Umbraco 19.")] public const string InstallerCookieName = "umb_installId"; /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs index f12bd612fc67..1c1f65f01b7d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -1,6 +1,8 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; +[Obsolete("Installation logging is no longer supported and this interface will be removed in Umbraco 19.")] public interface IInstallationRepository { + [Obsolete("This method no longer has any function and will be removed in Umbraco 19.")] Task SaveInstallLogAsync(InstallLog installLog); } diff --git a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs index e79edec7f712..991af0001ee8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/InstallationRepository.cs @@ -3,31 +3,15 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; +[Obsolete("Installation logging is no longer supported and this class will be removed in Umbraco 19.")] public class InstallationRepository : IInstallationRepository { - private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; - private static HttpClient? _httpClient; - private readonly IJsonSerializer _jsonSerializer; - public InstallationRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - - public async Task SaveInstallLogAsync(InstallLog installLog) + public InstallationRepository(IJsonSerializer jsonSerializer) { - try - { - if (_httpClient == null) - { - _httpClient = new HttpClient(); - } - - using var content = new StringContent(_jsonSerializer.Serialize(installLog), Encoding.UTF8, "application/json"); - await _httpClient.PostAsync(RestApiInstallUrl, content); - } - - // this occurs if the server for Our is down or cannot be reached - catch (HttpRequestException) - { - } } + + [Obsolete("This method no longer has any function and will be removed in Umbraco 19.")] + public Task SaveInstallLogAsync(InstallLog installLog) => Task.CompletedTask; } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index d04a0f99bf06..e067b1579159 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -8,23 +8,12 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Infrastructure.Install { + [Obsolete("InstallHelper is no longer used internally and will be removed in Umbraco 19.")] public sealed class InstallHelper { - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _connectionStrings; - private readonly IInstallationService _installationService; - private readonly ICookieManager _cookieManager; - private readonly IUserAgentProvider _userAgentProvider; - private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IFireAndForgetRunner _fireAndForgetRunner; - private readonly IEnumerable _databaseProviderMetadata; public InstallHelper( DatabaseBuilder databaseBuilder, @@ -38,74 +27,12 @@ public InstallHelper( IFireAndForgetRunner fireAndForgetRunner, IEnumerable databaseProviderMetadata) { - _logger = logger; - _umbracoVersion = umbracoVersion; - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _installationService = installationService; - _cookieManager = cookieManager; - _userAgentProvider = userAgentProvider; - _umbracoDatabaseFactory = umbracoDatabaseFactory; - _fireAndForgetRunner = fireAndForgetRunner; - _databaseProviderMetadata = databaseProviderMetadata; - } - - public Task SetInstallStatusAsync(bool isCompleted, string errorMsg) - { - try - { - var userAgent = _userAgentProvider.GetUserAgent(); - - // Check for current install ID - var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); - if (!Guid.TryParse(installCookie, out Guid installId)) - { - installId = Guid.NewGuid(); - - _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString(), false, false, "Unspecified"); - } - - var dbProvider = string.Empty; - if (IsBrandNewInstall == false) - { - // we don't have DatabaseProvider anymore... doing it differently - //dbProvider = ApplicationContext.Current.DatabaseContext.DatabaseProvider.ToString(); - dbProvider = _umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider; - } - - var installLog = new InstallLog( - installId: installId, - isUpgrade: IsBrandNewInstall == false, - installCompleted: isCompleted, - timestamp: DateTime.Now, - versionMajor: _umbracoVersion.Version.Major, - versionMinor: _umbracoVersion.Version.Minor, - versionPatch: _umbracoVersion.Version.Build, - versionComment: _umbracoVersion.Comment, - error: errorMsg, - userAgent: userAgent, - dbProvider: dbProvider); - - _fireAndForgetRunner.RunFireAndForget(() => _installationService.LogInstall(installLog)); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred in InstallStatus trying to check upgrades"); - } - - return Task.CompletedTask; } /// - /// Checks if this is a brand new install, meaning that there is no configured database connection or the database is empty. + /// This method used to send installer telemetry to Our.Umbraco.com but no longer does anything and will be removed in Umbraco 19. /// - /// - /// true if this is a brand new install; otherwise, false. - /// - private bool IsBrandNewInstall => - _connectionStrings.CurrentValue.IsConnectionStringConfigured() == false || - _databaseBuilder.IsDatabaseConfigured == false || - (_databaseBuilder.CanConnectToDatabase == false && _databaseProviderMetadata.CanForceCreateDatabase(_umbracoDatabaseFactory)) || - _databaseBuilder.IsUmbracoInstalled() == false; + [Obsolete("SetInstallStatusAsync no longer has any function and will be removed in Umbraco 19.")] + public Task SetInstallStatusAsync(bool isCompleted, string errorMsg) => Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs b/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs index b7717d9fc4df..99c973c6fb13 100644 --- a/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs +++ b/src/Umbraco.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs @@ -7,19 +7,21 @@ namespace Umbraco.Cms.Infrastructure.Installer.Steps; public class RegisterInstallCompleteStep : StepBase, IInstallStep, IUpgradeStep { - private readonly InstallHelper _installHelper; + [Obsolete("Please use the constructor without parameters. Scheduled for removal in Umbraco 19.")] + public RegisterInstallCompleteStep(InstallHelper installHelper) + : this() + { + } - public RegisterInstallCompleteStep(InstallHelper installHelper) => _installHelper = installHelper; + public RegisterInstallCompleteStep() + { + } public Task> ExecuteAsync(InstallData _) => Execute(); public Task> ExecuteAsync() => Execute(); - private async Task> Execute() - { - await _installHelper.SetInstallStatusAsync(true, string.Empty); - return Success(); - } + private Task> Execute() => Task.FromResult(Success()); public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); From b40ea0df8c26bc719a1909c137a911802d7d481e Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:01:09 +0100 Subject: [PATCH 13/45] Content Type Workspace: Create condition that checks content type uniques. (#20906) * Created condition for workspace content type unique. * Changed import. * Revert import. * Updated name in the alias example and also import. * Update src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/index.ts Co-authored-by: Mads Rasmussen * Update src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/workspace-view-unique.element.ts Co-authored-by: Mads Rasmussen * Moved the manifest definition to the manifest file. * Changed default export. * Updated example element to render the real GUID. * Fixed import. * Replaced CONTENT_WORKSPACE for PROPERTY_STRUCTURE_WORKSPACE context. * Moved content type unique condition to the content type folder. * Fixed import. * final adjustments --------- Co-authored-by: Mads Rasmussen --- ...-alias-condition-workspace-view.element.ts | 48 +++++++++++++++++++ ...unique-condition-workspace-view.element.ts | 48 +++++++++++++++++++ .../entity-content-type-condition/index.ts | 39 +++++++++++---- .../workspace-view.element.ts | 19 -------- .../content-type/conditions/constants.ts | 6 +-- .../content-type/conditions/manifests.ts | 5 +- .../content/content-type/conditions/types.ts | 27 +---------- .../workspace-content-type-alias/constants.ts | 4 ++ .../workspace-content-type-alias/manifests.ts | 11 +++++ .../workspace-content-type-alias/types.ts | 25 ++++++++++ .../workspace-content-type-alias.condition.ts | 9 +--- .../constants.ts | 4 ++ .../manifests.ts | 11 +++++ .../workspace-content-type-unique/types.ts | 25 ++++++++++ ...workspace-content-type-unique.condition.ts | 47 ++++++++++++++++++ 15 files changed, 261 insertions(+), 67 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-alias-condition-workspace-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-unique-condition-workspace-view.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/workspace-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/types.ts rename src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/{ => workspace-content-type-alias}/workspace-content-type-alias.condition.ts (85%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/workspace-content-type-unique.condition.ts diff --git a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-alias-condition-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-alias-condition-workspace-view.element.ts new file mode 100644 index 000000000000..0df9c4c0082d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-alias-condition-workspace-view.element.ts @@ -0,0 +1,48 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { html, customElement, state, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('example-content-type-alias-condition-workspace-view') +export class ExampleContentTypeAliasConditionWorkspaceViewElement extends UmbLitElement { + @state() + private _contentTypeAliases: string[] = []; + + constructor() { + super(); + + this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (context) => { + this.observe(context?.structure.contentTypeAliases, (contentTypeAliases) => { + this._contentTypeAliases = contentTypeAliases || []; + }); + }); + } + + override render() { + return html` +

Content Type Alias Condition Example

+

+ Content Type ${this._contentTypeAliases.length > 1 ? 'aliases' : 'alias'}: + ${this._contentTypeAliases} +

+
`; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + margin: var(--uui-size-layout-2); + } + `, + ]; +} + +export { ExampleContentTypeAliasConditionWorkspaceViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-content-type-alias-condition-workspace-view': ExampleContentTypeAliasConditionWorkspaceViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-unique-condition-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-unique-condition-workspace-view.element.ts new file mode 100644 index 000000000000..6ce9bc9eec03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/content-type-unique-condition-workspace-view.element.ts @@ -0,0 +1,48 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { html, customElement, state, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('example-content-type-unique-condition-workspace-view') +export class ExampleContentTypeUniqueConditionWorkspaceViewElement extends UmbLitElement { + @state() + private _contentTypeUniques: string[] = []; + + constructor() { + super(); + + this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (context) => { + this.observe(context?.structure.contentTypeUniques, (contentTypeUniques) => { + this._contentTypeUniques = contentTypeUniques || []; + }); + }); + } + + override render() { + return html` +

Content Type Unique Condition Example

+

+ Content Type ${this._contentTypeUniques.length > 1 ? 'ids' : 'id'}: + ${this._contentTypeUniques} +

+
`; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + margin: var(--uui-size-layout-2); + } + `, + ]; +} + +export { ExampleContentTypeUniqueConditionWorkspaceViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'example-content-type-unique-condition-workspace-view': ExampleContentTypeUniqueConditionWorkspaceViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/index.ts b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/index.ts index 43cc3a71a366..f4322158ff99 100644 --- a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/index.ts +++ b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/index.ts @@ -1,22 +1,43 @@ -import { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from '@umbraco-cms/backoffice/content-type'; +import { + UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS, + UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION, +} from '@umbraco-cms/backoffice/content-type'; -const workspace: UmbExtensionManifest = { +const workspaceViewAlias: UmbExtensionManifest = { type: 'workspaceView', - alias: 'Example.WorkspaceView.EntityContentTypeCondition', - name: 'Example Workspace View With Entity Content Type Condition', - element: () => import('./workspace-view.element.js'), + alias: 'Example.WorkspaceView.EntityContentTypeAliasCondition', + name: 'Example Workspace View With Entity Content Type Alias Condition', + element: () => import('./content-type-alias-condition-workspace-view.element.js'), meta: { icon: 'icon-bus', - label: 'Conditional', - pathname: 'conditional', + label: 'Conditional (Alias)', + pathname: 'conditional-alias', }, conditions: [ { alias: UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS, - //match : 'blogPost' + //match: 'blogPost', oneOf: ['blogPost', 'mediaType1'], }, ], }; -export const manifests = [workspace]; +const workspaceViewUnique: UmbExtensionManifest = { + type: 'workspaceView', + alias: 'Example.WorkspaceView.EntityContentTypeUniqueCondition', + name: 'Example Workspace View With Content Type Unique Condition', + element: () => import('./content-type-unique-condition-workspace-view.element.js'), + meta: { + icon: 'icon-science', + label: 'Conditional (Unique)', + pathname: 'conditional-unique', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION, + oneOf: ['721e85d3-0a2d-4f99-be55-61a5c5ed5c14', '1b88975d-60d0-4b84-809a-4a4deff38a66'], // Example uniques + }, + ], +}; + +export const manifests = [workspaceViewAlias, workspaceViewUnique]; diff --git a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/workspace-view.element.ts b/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/workspace-view.element.ts deleted file mode 100644 index b4390f0f625f..000000000000 --- a/src/Umbraco.Web.UI.Client/examples/entity-content-type-condition/workspace-view.element.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -@customElement('umb-example-entity-content-type-condition') -export class UmbWorkspaceExampleViewElement extends UmbLitElement { - override render() { - return html`

- This is a conditional element that is only shown in workspaces based on it's entities content type. -

`; - } -} - -export default UmbWorkspaceExampleViewElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-example-entity-content-type-condition': UmbWorkspaceExampleViewElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/constants.ts index 0bf665f7712c..8c5bd4462ece 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/constants.ts @@ -1,4 +1,2 @@ -/** - * Workspace Content Type Alias condition alias - */ -export const UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS = 'Umb.Condition.WorkspaceContentTypeAlias'; +export * from './workspace-content-type-alias/constants.js'; +export * from './workspace-content-type-unique/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/manifests.ts index 0e4865bde973..de68f5ce2632 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/manifests.ts @@ -1,3 +1,4 @@ -import { manifest as workspaceContentTypeAliasCondition } from './workspace-content-type-alias.condition.js'; +import { manifests as workspaceAliasCondition } from './workspace-content-type-alias/manifests.js'; +import { manifests as WorkspaceContentTypeUnique } from './workspace-content-type-unique/manifests.js'; -export const manifests: Array = [workspaceContentTypeAliasCondition]; +export const manifests: Array = [...workspaceAliasCondition, ...WorkspaceContentTypeUnique]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/types.ts index e0a50062e2f9..e6fbd340f910 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/types.ts @@ -1,25 +1,2 @@ -import type { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from './constants.js'; -import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; - -export type UmbWorkspaceContentTypeAliasConditionConfig = UmbConditionConfigBase< - typeof UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS -> & { - /** - * Define a content type alias in which workspace this extension should be available - * @example - * Depends on implementation, but i.e. "article", "image", "blockPage" - */ - match?: string; - /** - * Define one or more content type aliases in which workspace this extension should be available - * @example - * ["article", "image", "blockPage"] - */ - oneOf?: Array; -}; - -declare global { - interface UmbExtensionConditionConfigMap { - umbWorkspaceContentTypeAliasConditionConfig: UmbWorkspaceContentTypeAliasConditionConfig; - } -} +export type * from './workspace-content-type-alias/types.js'; +export type * from './workspace-content-type-unique/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/constants.ts new file mode 100644 index 000000000000..0bf665f7712c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/constants.ts @@ -0,0 +1,4 @@ +/** + * Workspace Content Type Alias condition alias + */ +export const UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS = 'Umb.Condition.WorkspaceContentTypeAlias'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/manifests.ts new file mode 100644 index 000000000000..dc0707b60b9e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from './constants.js'; +import { UmbWorkspaceContentTypeAliasCondition } from './workspace-content-type-alias.condition.js'; + +export const manifests: Array = [ + { + type: 'condition', + name: 'Workspace Content Type Alias Condition', + alias: UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS, + api: UmbWorkspaceContentTypeAliasCondition, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/types.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/types.ts new file mode 100644 index 000000000000..e0a50062e2f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/types.ts @@ -0,0 +1,25 @@ +import type { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from './constants.js'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbWorkspaceContentTypeAliasConditionConfig = UmbConditionConfigBase< + typeof UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS +> & { + /** + * Define a content type alias in which workspace this extension should be available + * @example + * Depends on implementation, but i.e. "article", "image", "blockPage" + */ + match?: string; + /** + * Define one or more content type aliases in which workspace this extension should be available + * @example + * ["article", "image", "blockPage"] + */ + oneOf?: Array; +}; + +declare global { + interface UmbExtensionConditionConfigMap { + umbWorkspaceContentTypeAliasConditionConfig: UmbWorkspaceContentTypeAliasConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/workspace-content-type-alias.condition.ts similarity index 85% rename from src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias.condition.ts rename to src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/workspace-content-type-alias.condition.ts index 532489b6255b..da85548886b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-alias/workspace-content-type-alias.condition.ts @@ -1,8 +1,8 @@ -import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '../contexts/index.js'; import type { UmbWorkspaceContentTypeAliasConditionConfig } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '../../contexts/property-structure-workspace.context-token.js'; const ObserveSymbol = Symbol(); @@ -44,10 +44,3 @@ export class UmbWorkspaceContentTypeAliasCondition } } } - -export const manifest: UmbExtensionManifest = { - type: 'condition', - name: 'Workspace Content Type Alias Condition', - alias: 'Umb.Condition.WorkspaceContentTypeAlias', - api: UmbWorkspaceContentTypeAliasCondition, -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/constants.ts new file mode 100644 index 000000000000..15db97c106b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/constants.ts @@ -0,0 +1,4 @@ +/** + * Workspace Content Type Unique condition + */ +export const UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION = 'Umb.Condition.WorkspaceContentTypeUnique'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/manifests.ts new file mode 100644 index 000000000000..bc20852f0e69 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION } from './constants.js'; +import { UmbWorkspaceContentTypeUniqueCondition } from './workspace-content-type-unique.condition.js'; + +export const manifests: Array = [ + { + type: 'condition', + name: 'Workspace Content Type Unique Condition', + alias: UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION, + api: UmbWorkspaceContentTypeUniqueCondition, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/types.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/types.ts new file mode 100644 index 000000000000..e41bbfd84e41 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/types.ts @@ -0,0 +1,25 @@ +import type { UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION } from './constants.js'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbWorkspaceContentTypeUniqueConditionConfig = UmbConditionConfigBase< + typeof UMB_WORKSPACE_CONTENT_TYPE_UNIQUE_CONDITION +> & { + /** + * Define a content type unique (GUID) in which workspace this extension should be available + * @example + * Depends on implementation, but i.e. "d59be02f-1df9-4228-aa1e-01917d806cda" + */ + match?: string; + /** + * Define one or more content type unique (GUIDs) in which workspace this extension should be available + * @example + * ["d59be02f-1df9-4228-aa1e-01917d806cda", "42d7572e-1ba1-458d-a765-95b60040c3ac"] + */ + oneOf?: Array; +}; + +declare global { + interface UmbExtensionConditionConfigMap { + umbWorkspaceContentTypeUniqueConditionConfig: UmbWorkspaceContentTypeUniqueConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/workspace-content-type-unique.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/workspace-content-type-unique.condition.ts new file mode 100644 index 000000000000..6d18dd772174 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/conditions/workspace-content-type-unique/workspace-content-type-unique.condition.ts @@ -0,0 +1,47 @@ +import type { UmbWorkspaceContentTypeUniqueConditionConfig } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '../../contexts/property-structure-workspace.context-token.js'; + +const ObserveSymbol = Symbol(); + +/** + * Condition to apply workspace extension based on a content type unique (GUID) + */ +export class UmbWorkspaceContentTypeUniqueCondition + extends UmbConditionBase + implements UmbExtensionCondition +{ + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); + + let permissionCheck: ((contentTypeUniques: string[]) => boolean) | undefined = undefined; + if (this.config.match) { + permissionCheck = (contentTypeUniques: string[]) => contentTypeUniques.includes(this.config.match!); + } else if (this.config.oneOf) { + permissionCheck = (contentTypeUniques: string[]) => + contentTypeUniques.some((item) => this.config.oneOf!.includes(item)); + } + + if (permissionCheck !== undefined) { + this.consumeContext(UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, (context) => { + this.observe( + context?.structure.contentTypeUniques, + (contentTypeUniques) => { + const result = contentTypeUniques ? permissionCheck!(contentTypeUniques) : false; + this.permitted = result; + }, + ObserveSymbol, + ); + }); + } else { + throw new Error( + 'Condition `Umb.Condition.WorkspaceContentTypeUnique` could not be initialized properly. Either "match" or "oneOf" must be defined', + ); + } + } +} From 820c34432a16a36ca59fbb16773299ef6672c34a Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:16:35 +0100 Subject: [PATCH 14/45] Preview: Fix preview showing published version when Save and Preview is clicked multiple times (closes #20981) (#20992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix preview showing published version when Save and Preview is clicked multiple times Fixes #20981 When clicking "Save and Preview" multiple times, the preview tab would show the published version instead of the latest saved version. This occurred because: 1. Each "Save and Preview" creates a new preview session with a new token 2. The preview window is reused (via named window target) 3. Without a URL change, the browser doesn't reload and misses the new session token 4. The stale page gets redirected to the published URL Solution: Add a cache-busting parameter (?rnd=timestamp) to the preview URL, forcing the browser to reload and pick up the new preview session token. This aligns with how SignalR refreshes work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Improve Save and Preview to avoid full page reloads when preview is already open When clicking "Save and Preview" multiple times with a preview tab already open, the entire preview tab would reload. This enhancement makes it behave like the "Save" button - only the iframe reloads, not the entire preview wrapper. Changes: - Store reference to preview window when opened - Check if preview window is still open before creating new session - If open, just focus it and let SignalR handle the iframe refresh - If closed, create new preview session and open new window This provides a smoother UX where subsequent saves don't cause the preview frame and controls to reload, only the content iframe refreshes via SignalR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Close preview window when ending preview session Changes the "End Preview" behavior to close the preview tab instead of navigating to the published URL. This provides a cleaner UX and ensures subsequent "Save and Preview" actions will always create a fresh preview session. Benefits: - Eliminates edge case where preview window remains open but is no longer in preview mode - Simpler behavior - preview session ends and window closes - Users can use "Preview website" button if they want to view published page Also removes unnecessary await on SignalR connection.stop() to prevent blocking if the connection cleanup hangs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix preview cookie expiration and add proper error handling This commit addresses cookie management issues in the preview system: 1. **Cookie Expiration API Enhancement** - Added `ExpireCookie` overload with security parameters (httpOnly, secure, sameSiteMode) - Added `SetCookieValue` overload with optional expires parameter - Marked old methods as obsolete for removal in Umbraco 19 - Ensures cookies are expired with matching security attributes 2. **PreviewService Cookie Handling** - Changed to use new `ExpireCookie` method with explicit security attributes - Maintains `Secure=true` and `SameSite=None` for cross-site scenarios - Uses new `SetCookieValue` overload with explicit expires parameter - Properly expires preview cookies when ending preview session 3. **Frontend Error Handling** - Added try-catch around preview window reference checks - Handles stale window references gracefully - Prevents potential errors from accessing closed window properties These changes ensure preview cookies are properly managed throughout their lifecycle and support both same-site and cross-site scenarios (e.g., when the backoffice is on a different domain/port during development). Fixes #20981 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Track document ID for preview window to prevent reusing window across different documents When navigating from one document to another in the backoffice, the preview window reference was being reused even though it was showing a different document. This meant clicking "Save and Preview" would just focus the existing window without updating it to show the new document. Now we track which document the preview window is showing and only reuse the window if: 1. The window is still open 2. The window is showing the same document This ensures each document gets its own preview session while still avoiding unnecessary full page reloads when repeatedly previewing the same document. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Remove updates to ICookieManager and use Cookies.Delete to remove cookie. * Fix file not found on click to save and preview. * Removed further currently unnecessary updates to the cookie manager interface and implementation. * Fixed failing unit test. --------- Co-authored-by: Claude Co-authored-by: Andy Butland --- src/Umbraco.Core/Services/PreviewService.cs | 5 +++- .../AspNetCore/AspNetCoreCookieManager.cs | 27 +++++++------------ .../workspace/document-workspace.context.ts | 25 ++++++++++++++--- .../preview/context/preview.context.ts | 13 ++++----- .../preview-environments.element.ts | 5 +++- .../AspNetCoreCookieManagerTests.cs | 2 +- 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Core/Services/PreviewService.cs b/src/Umbraco.Core/Services/PreviewService.cs index 9810d8f07e19..43e24f0e07f7 100644 --- a/src/Umbraco.Core/Services/PreviewService.cs +++ b/src/Umbraco.Core/Services/PreviewService.cs @@ -34,7 +34,10 @@ public async Task TryEnterPreviewAsync(IUser user) if (attempt.Success) { - _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, true, true, "None"); + // Preview cookies must use SameSite=None and Secure=true to support cross-site scenarios + // (e.g., when the backoffice is on a different domain/port than the frontend during development). + // SameSite=None requires Secure=true per browser specifications. + _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, httpOnly: true, secure: true, sameSiteMode: "None"); } return attempt.Success; diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 854699cd29d1..0d7aad9d4978 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -27,15 +27,7 @@ public void ExpireCookie(string cookieName) return; } - var cookieValue = httpContext.Request.Cookies[cookieName]; - - httpContext.Response.Cookies.Append( - cookieName, - cookieValue ?? string.Empty, - new CookieOptions - { - Expires = DateTime.Now.AddYears(-1), - }); + httpContext.Response.Cookies.Delete(cookieName); } /// @@ -45,15 +37,14 @@ public void ExpireCookie(string cookieName) public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode) { SameSiteMode sameSiteModeValue = ParseSameSiteMode(sameSiteMode); - _httpContextAccessor.HttpContext?.Response.Cookies.Append( - cookieName, - value, - new CookieOptions - { - HttpOnly = httpOnly, - SameSite = sameSiteModeValue, - Secure = secure, - }); + var options = new CookieOptions + { + HttpOnly = httpOnly, + SameSite = sameSiteModeValue, + Secure = secure, + }; + + _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, options); } private static SameSiteMode ParseSameSiteMode(string sameSiteMode) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 6dadb4ae7c95..e29677615d1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -72,6 +72,8 @@ export class UmbDocumentWorkspaceContext #documentSegmentRepository = new UmbDocumentSegmentRepository(this); #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; #localize = new UmbLocalizationController(this); + #previewWindow?: WindowProxy | null = null; + #previewWindowDocumentId?: string | null = null; constructor(host: UmbControllerHost) { super(host, { @@ -343,7 +345,20 @@ export class UmbDocumentWorkspaceContext await this.performCreateOrUpdate(variantIds, saveData); } - // Get the preview URL from the server. + // Check if preview window is still open and showing the same document + // If so, just focus it and let SignalR handle the refresh + try { + if (this.#previewWindow && !this.#previewWindow.closed && this.#previewWindowDocumentId === unique) { + this.#previewWindow.focus(); + return; + } + } catch { + // Window reference is stale, continue to create new preview session + this.#previewWindow = null; + this.#previewWindowDocumentId = null; + } + + // Preview not open, create new preview session and open window const previewRepository = new UmbPreviewRepository(this); const previewUrlData = await previewRepository.getPreviewUrl( unique, @@ -353,8 +368,12 @@ export class UmbDocumentWorkspaceContext ); if (previewUrlData.url) { - const previewWindow = window.open(previewUrlData.url, `umbpreview-${unique}`); - previewWindow?.focus(); + // Add cache-busting parameter to ensure the preview tab reloads with the new preview session + const previewUrl = new URL(previewUrlData.url, window.document.baseURI); + previewUrl.searchParams.set('rnd', Date.now().toString()); + this.#previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`); + this.#previewWindowDocumentId = unique; + this.#previewWindow?.focus(); return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts index de4b0267c76d..7e315c3a31e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts @@ -200,18 +200,15 @@ export class UmbPreviewContext extends UmbContextBase { async exitPreview() { await this.#previewRepository.exit(); + // Stop SignalR connection without waiting - window will close anyway if (this.#connection) { - await this.#connection.stop(); + this.#connection.stop(); this.#connection = undefined; } - let url = await this.#getPublishedUrl(); - - if (!url) { - url = this.#previewUrl.getValue() as string; - } - - window.location.replace(url); + // Close the preview window + // This ensures that subsequent "Save and Preview" actions will create a new preview session + window.close(); } iframeLoaded(iframe: HTMLIFrameElement) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts index 8b0bf0452adc..5cb43b2371a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts @@ -78,7 +78,10 @@ export class UmbPreviewEnvironmentsElement extends UmbLitElement { ); if (previewUrlData.url) { - const previewWindow = window.open(previewUrlData.url, `umbpreview-${this._unique}`); + // Add cache-busting parameter to ensure the preview tab reloads with the new preview session + const previewUrl = new URL(previewUrlData.url, window.document.baseURI); + previewUrl.searchParams.set('rnd', Date.now().toString()); + const previewWindow = window.open(previewUrl.toString(), `umbpreview-${this._unique}`); previewWindow?.focus(); return; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs index e367870a50df..58789979c5eb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs @@ -71,7 +71,7 @@ public void Can_Expire_Cookie() cookieManager.ExpireCookie(CookieName); var setCookieHeader = httpContext.Response.Headers.SetCookie.ToString(); - Assert.IsTrue(setCookieHeader.StartsWith(GetExpectedCookie())); + Assert.IsTrue(setCookieHeader.StartsWith("testCookie=")); Assert.IsTrue(setCookieHeader.Contains($"expires=")); } From 1c4b4c90c9eeb884c307bc5898b6f7daacefa9bc Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 1 Dec 2025 12:52:35 +0100 Subject: [PATCH 15/45] Rendering: Don't use element cache level on snapshot cache level properties (#21006) Don't use element cache level on snapshot cache level propreties --- src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs index 0ba4b6873bd6..17f37166307e 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs @@ -187,10 +187,10 @@ private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) switch (cacheLevel) { case PropertyCacheLevel.None: + case PropertyCacheLevel.Snapshot: // Snapshot is obsolete, so for now treat as None // never cache anything cacheValues = new CacheValues(); break; - case PropertyCacheLevel.Snapshot: // Snapshot is obsolete, so for now treat as element case PropertyCacheLevel.Element: // cache within the property object itself, ie within the content object cacheValues = _cacheValues ??= new CacheValues(); From 09844204b7f799be03d6d4e8dee135764f2cc469 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:29:07 +0100 Subject: [PATCH 16/45] History: Take `URL` objects into consideration when storing Backoffice history (#20986) * fix: allows URL to be passed to navigator * Also adds fix to Block workspace --------- Co-authored-by: leekelleher --- .../block/block/workspace/block-workspace.context.ts | 9 ++++++--- .../entity-detail/entity-detail-workspace-base.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 08a6090eb4be..3af007c20040 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -313,11 +313,14 @@ export class UmbBlockWorkspaceContext Date: Wed, 26 Nov 2025 15:02:07 +0100 Subject: [PATCH 17/45] Cache: Add awaits to memory cache rebuilds to fix race conditions (#20960) * Await rebuilds and fix multiple open DataReaders * Add additional missing awaits (cherry picked from commit eaf5960a4dec09dd8a315942e06dd2b04956ff48) --- .../Cache/Refreshers/Implement/ContentCacheRefresher.cs | 4 ++-- .../Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs | 4 ++-- .../Cache/Refreshers/Implement/DataTypeCacheRefresher.cs | 2 +- .../Cache/Refreshers/Implement/MediaCacheRefresher.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 7505781ad95a..c58e689a67ce 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -239,8 +239,8 @@ private void HandleNavigation(JsonPayload payload) if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { - _documentNavigationManagementService.RebuildAsync(); - _documentNavigationManagementService.RebuildBinAsync(); + _documentNavigationManagementService.RebuildAsync().GetAwaiter().GetResult(); + _documentNavigationManagementService.RebuildBinAsync().GetAwaiter().GetResult(); } if (payload.Key is null) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs index fc898e1be321..831520c4a951 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentTypeCacheRefresher.cs @@ -136,8 +136,8 @@ public override void Refresh(JsonPayload[] payloads) IEnumerable documentTypeIds = payloads.Where(x => x.ItemType == nameof(IContentType)).Select(x => x.Id); IEnumerable mediaTypeIds = payloads.Where(x => x.ItemType == nameof(IMediaType)).Select(x => x.Id); - _documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds); - _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds); + _documentCacheService.RebuildMemoryCacheByContentTypeAsync(documentTypeIds).GetAwaiter().GetResult(); + _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds).GetAwaiter().GetResult(); }); // now we can trigger the event diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs index 101be3d0ac57..dcc53e37f381 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DataTypeCacheRefresher.cs @@ -120,7 +120,7 @@ public override void Refresh(JsonPayload[] payloads) IEnumerable mediaTypeIds = removedContentTypes .Where(x => x.ItemType == PublishedItemType.Media) .Select(x => x.Id); - _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds); + _mediaCacheService.RebuildMemoryCacheByContentTypeAsync(mediaTypeIds).GetAwaiter().GetResult(); }); base.Refresh(payloads); } diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs index 45af4d2c8f56..f54c78de1b7a 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs @@ -194,8 +194,8 @@ private void HandleNavigation(JsonPayload payload) if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { - _mediaNavigationManagementService.RebuildAsync(); - _mediaNavigationManagementService.RebuildBinAsync(); + _mediaNavigationManagementService.RebuildAsync().GetAwaiter().GetResult(); + _mediaNavigationManagementService.RebuildBinAsync().GetAwaiter().GetResult(); } if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode)) From bbc0f1f8941fc8ccaba0d09ec27a06015093d970 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:50:51 +0100 Subject: [PATCH 18/45] Upgrade: Deprecates Mangement API controller for defunct our.umbraco.com version checker (#21011) * fix: deprecates the upgrade checker * fix: removes any deprecated UI that no longer has a function for upgrade checks in the backoffice * chore: generates new api types * chore: deprecates types * chore: returns direct task * docs: explains deprecation * chore: deprecated model --------- Co-authored-by: leekelleher --- .../Server/UpgradeCheckServerController.cs | 2 + src/Umbraco.Cms.Api.Management/OpenApi.json | 1 + .../Server/UpgradeCheckResponseModel.cs | 3 +- .../Repositories/IUpgradeCheckRepository.cs | 2 + .../Repositories/UpgradeCheckRepository.cs | 46 +------ src/Umbraco.Core/Services/IUpgradeService.cs | 2 + src/Umbraco.Core/Services/UpgradeService.cs | 1 + .../backoffice-header-logo.element.ts | 52 +------- .../src/assets/lang/da.ts | 1 - .../src/assets/lang/de.ts | 1 - .../src/assets/lang/en.ts | 1 - .../src/assets/lang/nb.ts | 1 - .../src/assets/lang/pt.ts | 1 - .../src/assets/lang/vi.ts | 1 - .../src/mocks/handlers/server.handlers.ts | 12 -- .../src/packages/core/backend-api/sdk.gen.ts | 3 + .../sysinfo/components/new-version.element.ts | 57 --------- .../src/packages/sysinfo/manifests.ts | 6 - .../src/packages/sysinfo/modals/index.ts | 1 - .../sysinfo/modals/new-version-modal.token.ts | 20 --- .../sysinfo/repository/sysinfo.repository.ts | 114 ++---------------- .../src/packages/sysinfo/types.ts | 3 + 22 files changed, 30 insertions(+), 301 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/sysinfo/components/new-version.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/sysinfo/modals/new-version-modal.token.ts diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs index 10a38944574a..a603532fe4d6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Server/UpgradeCheckServerController.cs @@ -13,6 +13,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Server; [ApiVersion("1.0")] [Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)] +[Obsolete("Upgrade checks are no longer supported and this controller will be removed in Umbraco 19.")] public class UpgradeCheckServerController : ServerControllerBase { private readonly IUpgradeService _upgradeService; @@ -27,6 +28,7 @@ public UpgradeCheckServerController(IUpgradeService upgradeService, IUmbracoVers [HttpGet("upgrade-check")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(UpgradeCheckResponseModel), StatusCodes.Status200OK)] + [Obsolete("Upgrade checks are no longer supported and this endpoint will be removed in Umbraco 19.")] public async Task UpgradeCheck(CancellationToken cancellationToken) { UpgradeResult upgradeResult = await _upgradeService.CheckUpgrade(_umbracoVersion.SemanticVersion); diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 4c495413c625..1a36bfd414f6 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -27854,6 +27854,7 @@ "description": "The authenticated user does not have access to this resource" } }, + "deprecated": true, "security": [ { "Backoffice-User": [ ] diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs index 0c84f0a8379f..edae04a0854a 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Server/UpgradeCheckResponseModel.cs @@ -1,5 +1,6 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Server; +namespace Umbraco.Cms.Api.Management.ViewModels.Server; +[Obsolete("Upgrade checks are no longer supported and this model will be removed in Umbraco 19.")] public class UpgradeCheckResponseModel { public required string Type { get; init; } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs index 7a0d8b6f7460..a782717e8382 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -2,7 +2,9 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; +[Obsolete("Upgrade checks are no longer supported and this interface will be removed in Umbraco 19.")] public interface IUpgradeCheckRepository { + [Obsolete("This method no longer has any function and will be removed in Umbraco 19.")] Task CheckUpgradeAsync(SemVersion version); } diff --git a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs index 3dab741accd4..ddd9c0cf8204 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UpgradeCheckRepository.cs @@ -1,54 +1,16 @@ -using System.Text; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Persistence.Repositories; +[Obsolete("Upgrade checks are no longer supported and this repository will be removed in Umbraco 19.")] public class UpgradeCheckRepository : IUpgradeCheckRepository { - private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; - private static HttpClient? _httpClient; - private readonly IJsonSerializer _jsonSerializer; - - public UpgradeCheckRepository(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; - - public async Task CheckUpgradeAsync(SemVersion version) + public UpgradeCheckRepository(IJsonSerializer jsonSerializer) { - try - { - _httpClient ??= new HttpClient { Timeout = TimeSpan.FromSeconds(1) }; - - using var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json"); - using HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); - var json = await task.Content.ReadAsStringAsync(); - UpgradeResult? result = _jsonSerializer.Deserialize(json); - - return result ?? new UpgradeResult("None", string.Empty, string.Empty); - } - catch (HttpRequestException) - { - // this occurs if the server for Our is down or cannot be reached - return new UpgradeResult("None", string.Empty, string.Empty); - } } - private sealed class CheckUpgradeDto - { - public CheckUpgradeDto(SemVersion version) - { - VersionMajor = version.Major; - VersionMinor = version.Minor; - VersionPatch = version.Patch; - VersionComment = version.Prerelease; - } - - public int VersionMajor { get; } - - public int VersionMinor { get; } - - public int VersionPatch { get; } - - public string VersionComment { get; } - } + [Obsolete("This method no longer has any function and will be removed in Umbraco 19.")] + public Task CheckUpgradeAsync(SemVersion version) => Task.FromResult(new UpgradeResult("None", string.Empty, string.Empty)); } diff --git a/src/Umbraco.Core/Services/IUpgradeService.cs b/src/Umbraco.Core/Services/IUpgradeService.cs index 2f1e65f00aa2..86cefd43f8ef 100644 --- a/src/Umbraco.Core/Services/IUpgradeService.cs +++ b/src/Umbraco.Core/Services/IUpgradeService.cs @@ -2,7 +2,9 @@ namespace Umbraco.Cms.Core.Services; +[Obsolete("Upgrade checks are no longer supported and this service will be removed in Umbraco 19.")] public interface IUpgradeService { + [Obsolete("This method no longer has any function and will be removed in Umbraco 19.")] Task CheckUpgrade(SemVersion version); } diff --git a/src/Umbraco.Core/Services/UpgradeService.cs b/src/Umbraco.Core/Services/UpgradeService.cs index 7a5269d2bf67..0c9a31d6a6b0 100644 --- a/src/Umbraco.Core/Services/UpgradeService.cs +++ b/src/Umbraco.Core/Services/UpgradeService.cs @@ -3,6 +3,7 @@ namespace Umbraco.Cms.Core.Services; +[Obsolete("Upgrade checks are no longer supported and this service will be removed in Umbraco 19.")] public class UpgradeService : IUpgradeService { private readonly IUpgradeCheckRepository _upgradeCheckRepository; diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts index bfd25e2dfcdd..03569d6b915d 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts @@ -1,11 +1,9 @@ import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { isCurrentUserAnAdmin } from '@umbraco-cms/backoffice/current-user'; +import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_NEWVERSION_MODAL, UMB_SYSINFO_MODAL } from '@umbraco-cms/backoffice/sysinfo'; -import type { UmbServerUpgradeCheck } from '@umbraco-cms/backoffice/sysinfo'; +import { UMB_SYSINFO_MODAL } from '@umbraco-cms/backoffice/sysinfo'; /** * The backoffice header logo element. @@ -22,14 +20,6 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement { @state() private _version?: string; - @state() - private _isUserAdmin = false; - - @state() - private _serverUpgradeCheck: UmbServerUpgradeCheck | null = null; - - #backofficeContext?: typeof UMB_BACKOFFICE_CONTEXT.TYPE; - constructor() { super(); @@ -42,23 +32,9 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement { }, '_observeVersion', ); - - this.#backofficeContext = context; }); } - protected override firstUpdated() { - this.#isAdmin(); - } - - async #isAdmin() { - this._isUserAdmin = await isCurrentUserAnAdmin(this); - - if (this._isUserAdmin) { - this._serverUpgradeCheck = this.#backofficeContext ? await this.#backofficeContext.serverUpgradeCheck() : null; - } - } - override render() { return html`