Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68de96d043 | ||
|
|
0681cdab16 | ||
|
|
7a4e5dd598 | ||
|
|
08466786ed |
@@ -30,7 +30,7 @@ This guide describes each step to make your first contribution:
|
||||
|
||||
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
|
||||
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task|qa|improvement}/{issue}-{description}`.
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
|
||||
|
||||
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
|
||||
|
||||
|
||||
+2
-3
@@ -96,9 +96,6 @@ tools/docfx/
|
||||
/tests/Umbraco.Tests.Integration/Views/
|
||||
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
/BenchmarkDotNet.Artifacts/
|
||||
playwright-report
|
||||
trace.zip
|
||||
/tests/Umbraco.Tests.AcceptanceTest/results
|
||||
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
@@ -111,3 +108,5 @@ trace.zip
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
playwright-report
|
||||
trace.zip
|
||||
|
||||
@@ -142,33 +142,8 @@ Web.UI → Web.Common → Infrastructure → Core
|
||||
### Branching Strategy
|
||||
|
||||
- **Main branch**: `main` (protected)
|
||||
- **Branch naming convention**: `v<version>/<type>/<description>`
|
||||
|
||||
**Format**: `v{major-version}/{type}/{kebab-case-description}`
|
||||
|
||||
**Version**: Read from `version.json` in the repository root. Use the major version number (e.g., `v17` for version 17.x.x).
|
||||
|
||||
**Types**:
|
||||
| Type | Use Case |
|
||||
|------|----------|
|
||||
| `feature` | New feature being introduced to the product |
|
||||
| `bugfix` | Fix to an existing issue with the product |
|
||||
| `qa` | Adding or updating unit, integration, or end-to-end tests |
|
||||
| `improvement` | Update to something that already exists but isn't broken (UI finessing, refactoring) |
|
||||
| `task` | Update that doesn't directly impact product behavior (dependency updates, build pipeline) |
|
||||
|
||||
**Description**: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
v17/bugfix/12345-correct-display-of-pending-migrations
|
||||
v17/feature/add-webhook-support
|
||||
v17/improvement/optimize-content-cache
|
||||
v17/qa/add-media-service-tests
|
||||
v17/task/update-ef-core-dependency
|
||||
```
|
||||
|
||||
See `.github/CONTRIBUTING.md` for full guidelines.
|
||||
- **Branch naming**:
|
||||
- See `.github/CONTRIBUTING.md` for full guidelines
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
|
||||
+20
-29
@@ -2,7 +2,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
@@ -13,27 +12,27 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
@@ -51,7 +50,6 @@
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="Markdig" Version="0.44.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
@@ -80,12 +78,5 @@
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Dazinator.Extensions.FileProviders references vulnerable versions of the following: -->
|
||||
<!-- TODO (V18): Remove these pinned dependencies when the Dazinator.Extensions.FileProviders dependency is removed. -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<!-- Markdown references vulnerable version of the following: -->
|
||||
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -196,14 +196,6 @@ Copyright: 2013-2024 .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Markdig: A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
|
||||
|
||||
URL: https://github.com/xoofx/markdig
|
||||
License: BSD-2-Clause license
|
||||
Copyright: 2018+, Alexandre Mutel. All rights reserved.
|
||||
|
||||
---
|
||||
|
||||
Markdown: A library for parsing and compiling Markdown
|
||||
|
||||
URL: https://github.com/hey-red/Markdown
|
||||
|
||||
@@ -215,7 +215,7 @@ stages:
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
dotnet tool install -g docfx
|
||||
choco install docfx --version=2.59.4 -y
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error installing DocFX")
|
||||
}
|
||||
|
||||
@@ -28,12 +28,6 @@ parameters:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Can we slow our tests down when running on SQLite? That way we might be able to avoid DB locks
|
||||
- name: skipSqliteAcceptanceTests
|
||||
displayName: Skip SQLite acceptance tests
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
solution: umbraco.sln
|
||||
@@ -348,7 +342,7 @@ stages:
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
timeoutInMinutes: 180
|
||||
condition: ${{ and(eq(parameters.skipDefaultConfigAcceptanceTests, false), eq(parameters.skipSqliteAcceptanceTests, false)) }}
|
||||
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
|
||||
@@ -587,23 +581,6 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# ContentSettingConfig
|
||||
WindowsContentSettingsConfig:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxContentSettingsConfig:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# SMTP
|
||||
LinuxSMTP:
|
||||
vmImage: "ubuntu-latest"
|
||||
@@ -716,7 +693,7 @@ stages:
|
||||
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
|
||||
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
|
||||
# Stop SMTP4dev container
|
||||
- bash: |
|
||||
echo "Stopping SMTP4dev container..."
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
@@ -13,31 +16,15 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
private readonly ISubTypesSelector _subTypesSelector;
|
||||
private readonly IDocumentInclusionSelector _documentInclusionSelector;
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector,
|
||||
IDocumentInclusionSelector documentInclusionSelector)
|
||||
{
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
_subTypesSelector = subTypesSelector;
|
||||
_documentInclusionSelector = documentInclusionSelector;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector)
|
||||
: this(
|
||||
operationIdSelector,
|
||||
schemaIdSelector,
|
||||
subTypesSelector,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
|
||||
{
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
_subTypesSelector = subTypesSelector;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
@@ -52,11 +39,19 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
});
|
||||
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
|
||||
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
|
||||
swaggerGenOptions.TagActionsBy(api =>
|
||||
api.GroupName is null
|
||||
? []
|
||||
: new[] { api.GroupName });
|
||||
swaggerGenOptions.DocInclusionPredicate((name, api) =>
|
||||
{
|
||||
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = api.ActionDescriptor.GetApiVersionMetadata();
|
||||
return apiVersionMetadata.Name == name
|
||||
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName);
|
||||
});
|
||||
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
|
||||
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
|
||||
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
|
||||
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
|
||||
|
||||
@@ -25,7 +25,6 @@ public static class UmbracoBuilderApiExtensions
|
||||
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
|
||||
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
|
||||
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
|
||||
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an API description should be included in a specific documentation set based on the document name
|
||||
/// and API metadata.
|
||||
/// </summary>
|
||||
public class DocumentInclusionSelector : IDocumentInclusionSelector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Include(string documentName, ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
|
||||
return apiVersionMetadata.Name == documentName
|
||||
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a method that determines whether a given API description should be included in a specific documentation
|
||||
/// document.
|
||||
/// </summary>
|
||||
public interface IDocumentInclusionSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in the generated documentation for the given
|
||||
/// document name.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the documentation document being generated.</param>
|
||||
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
|
||||
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
|
||||
bool Include(string documentName, ApiDescription apiDescription);
|
||||
}
|
||||
@@ -99,8 +99,8 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
|
||||
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
|
||||
{
|
||||
Response.Headers.Append("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Append("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
return RedirectPermanent(redirectRoute.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
TODO: Fix and remove overrides:
|
||||
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
|
||||
[CS0618/CS0612] update obsolete references
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),CS0618,CS0612</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Content;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Media;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -15,6 +21,7 @@ public class MediaUrlFactory : IMediaUrlFactory
|
||||
private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
|
||||
private readonly IAbsoluteUrlBuilder _absoluteUrlBuilder;
|
||||
|
||||
|
||||
public MediaUrlFactory(
|
||||
IOptions<ContentSettings> contentSettings,
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
@@ -32,26 +39,10 @@ public class MediaUrlFactory : IMediaUrlFactory
|
||||
.Select(mediaUrl => new MediaUrlInfo
|
||||
{
|
||||
Culture = null,
|
||||
Url = CreateMediaUrl(mediaUrl),
|
||||
Url = _absoluteUrlBuilder.ToAbsoluteUrl(mediaUrl).ToString(),
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
private string CreateMediaUrl(string mediaUrl)
|
||||
{
|
||||
var url = _absoluteUrlBuilder.ToAbsoluteUrl(mediaUrl).ToString();
|
||||
|
||||
if (_contentSettings.EnableMediaRecycleBinProtection is false)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
return _contentSettings.EnableMediaRecycleBinProtection
|
||||
? AddProtectedSuffixToMediaUrl(url)
|
||||
: url;
|
||||
}
|
||||
|
||||
private static string AddProtectedSuffixToMediaUrl(string url) => Path.ChangeExtension(url, Constants.Conventions.Media.TrashedMediaSuffix + Path.GetExtension(url));
|
||||
|
||||
public IEnumerable<MediaUrlInfoResponseModel> CreateUrlSets(IEnumerable<IMedia> mediaItems) =>
|
||||
mediaItems.Select(media => new MediaUrlInfoResponseModel(media.Key, CreateUrls(media))).ToArray();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Id = userGroup.Key,
|
||||
Name = userGroup.Name ?? string.Empty,
|
||||
Description = userGroup.Description ?? string.Empty,
|
||||
Alias = userGroup.Alias,
|
||||
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
|
||||
DocumentRootAccess = contentRootAccess,
|
||||
@@ -88,7 +87,6 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Id = userGroup.Key,
|
||||
Name = userGroup.Name ?? string.Empty,
|
||||
Description = userGroup.Description ?? string.Empty,
|
||||
Alias = userGroup.Alias,
|
||||
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
|
||||
MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey),
|
||||
@@ -134,7 +132,6 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Name = CleanUserGroupNameOrAliasForXss(requestModel.Name),
|
||||
Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias),
|
||||
Description = requestModel.Description,
|
||||
Icon = requestModel.Icon,
|
||||
HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages,
|
||||
Permissions = requestModel.FallbackPermissions,
|
||||
@@ -200,10 +197,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
|
||||
current.Name = CleanUserGroupNameOrAliasForXss(request.Name);
|
||||
current.Alias = CleanUserGroupNameOrAliasForXss(request.Alias);
|
||||
current.Description = request.Description;
|
||||
current.Icon = request.Icon;
|
||||
current.HasAccessToAllLanguages = request.HasAccessToAllLanguages;
|
||||
|
||||
|
||||
current.Permissions = request.FallbackPermissions;
|
||||
current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
@@ -11,12 +10,6 @@ internal sealed class NotificationHeaderFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Only apply to the Umbraco CMS Management API.
|
||||
if (context.DocumentName != ManagementApiConfiguration.ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.ApiDescription.HttpMethod == HttpMethod.Get.Method)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.ServerEvents;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.ServerEvents;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ServerEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Handles server event notifications for various entity types by routing create, update, delete, and recycle bin
|
||||
/// events to connected clients or systems.
|
||||
/// </summary>
|
||||
internal sealed class ServerEventSender :
|
||||
INotificationAsyncHandler<ContentSavedNotification>,
|
||||
INotificationAsyncHandler<ContentSavedBlueprintNotification>,
|
||||
@@ -63,16 +57,8 @@ internal sealed class ServerEventSender :
|
||||
INotificationAsyncHandler<MediaMovedToRecycleBinNotification>
|
||||
{
|
||||
private readonly IServerEventRouter _serverEventRouter;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerEventSender"/> class.
|
||||
/// </summary>
|
||||
public ServerEventSender(IServerEventRouter serverEventRouter, IIdKeyMap idKeyMap)
|
||||
{
|
||||
_serverEventRouter = serverEventRouter;
|
||||
_idKeyMap = idKeyMap;
|
||||
}
|
||||
public ServerEventSender(IServerEventRouter serverEventRouter) => _serverEventRouter = serverEventRouter;
|
||||
|
||||
private async Task NotifySavedAsync<T>(SavedNotification<T> notification, string source)
|
||||
where T : IEntity
|
||||
@@ -127,115 +113,66 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentSavedBlueprintNotification notification, CancellationToken cancellationToken) =>
|
||||
await RouteCreatedOrUpdatedEvent(Constants.ServerEvents.EventSource.DocumentBlueprint, notification.SavedBlueprint);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken)
|
||||
=> await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Member);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberGroupSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DataTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DataType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(LanguageSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Language);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ScriptSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Script);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(StylesheetSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(TemplateSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Template);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DictionaryItemSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DomainSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Domain);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PartialViewSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
|
||||
|
||||
// For public access entries, we also need to notify affected content items, so any client-side
|
||||
// cache for the document can be invalidated.
|
||||
await RouteDocumentUpdatedEventForPublicAccessModification(notification.SavedEntities);
|
||||
}
|
||||
|
||||
private async Task RouteDocumentUpdatedEventForPublicAccessModification(IEnumerable<PublicAccessEntry> entities)
|
||||
{
|
||||
foreach (PublicAccessEntry entity in entities)
|
||||
{
|
||||
Attempt<Guid> getKeyAttempt = _idKeyMap.GetKeyForId(
|
||||
entity.ProtectedNodeId,
|
||||
UmbracoObjectTypes.Document);
|
||||
if (getKeyAttempt.Success is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eventModel = new ServerEvent
|
||||
{
|
||||
EventType = Constants.ServerEvents.EventType.Updated,
|
||||
Key = getKeyAttempt.Result,
|
||||
EventSource = Constants.ServerEvents.EventSource.Document,
|
||||
};
|
||||
await _serverEventRouter.RouteEventAsync(eventModel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Relation);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserGroupSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// We still need to notify of saved entities like any other event source.
|
||||
@@ -255,15 +192,12 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(WebhookSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentDeletedBlueprintNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (Core.Models.IContent entity in notification.DeletedBlueprints)
|
||||
@@ -272,97 +206,69 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Member);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberGroupDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DataTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DataType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(LanguageDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Language);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ScriptDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Script);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(StylesheetDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(TemplateDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Template);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DictionaryItemDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DomainDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Domain);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PartialViewDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
|
||||
|
||||
// For public access entries, we also need to notify affected content items, so any client-side
|
||||
// cache for the document can be invalidated.
|
||||
await RouteDocumentUpdatedEventForPublicAccessModification(notification.DeletedEntities);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Relation);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.User);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(WebhookDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
}
|
||||
|
||||
@@ -11,21 +11,12 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for retrieving user start node entities with access information.
|
||||
/// </summary>
|
||||
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserStartNodeEntitiesService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">The entity service.</param>
|
||||
/// <param name="scopeProvider">The core scope provider.</param>
|
||||
/// <param name="idKeyMap">The ID to key mapping service.</param>
|
||||
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
|
||||
{
|
||||
_entityService = entityService;
|
||||
@@ -159,7 +150,8 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
int after,
|
||||
Ordering ordering,
|
||||
out long totalBefore,
|
||||
out long totalAfter)
|
||||
out long totalAfter
|
||||
)
|
||||
{
|
||||
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
|
||||
if (targetIdAttempt.Success is false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup;
|
||||
|
||||
@@ -22,11 +22,6 @@ public class UserGroupBase
|
||||
/// </summary>
|
||||
public required string Alias { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of the user group
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Icon for the user group
|
||||
/// </summary>
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
|
||||
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
|
||||
|
||||
internal sealed class UmbracoViewCompilerProvider : IViewCompilerProvider
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Persistence.SqlServer.Operations;
|
||||
|
||||
/// <summary>
|
||||
/// SQL Server implementation of <see cref="IPropertyDataReplacerOperation"/> that uses SqlBulkCopy
|
||||
/// with a temp table and MERGE statement for optimized performance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This approach combines:
|
||||
/// - SqlBulkCopy for fast data transfer to the server.
|
||||
/// - Temp table to stage the data.
|
||||
/// - MERGE statement for atomic UPDATE/INSERT/DELETE.
|
||||
/// </remarks>
|
||||
public class SqlServerPropertyDataReplacerOperation : IPropertyDataReplacerOperation
|
||||
{
|
||||
private const string TempTableName = "#umbracoPropertyDataStaging";
|
||||
|
||||
private static readonly string[] _columnNames =
|
||||
[
|
||||
"versionId", "propertyTypeId", "languageId", "segment",
|
||||
"intValue", "decimalValue", "dateValue", "varcharValue", "textValue"
|
||||
];
|
||||
|
||||
private const string CreateTempTableSql = $"""
|
||||
CREATE TABLE [{TempTableName}] (
|
||||
[versionId] INT NOT NULL,
|
||||
[propertyTypeId] INT NOT NULL,
|
||||
[languageId] INT NULL,
|
||||
[segment] NVARCHAR(256) NULL,
|
||||
[intValue] INT NULL,
|
||||
[decimalValue] DECIMAL(38, 6) NULL,
|
||||
[dateValue] DATETIME NULL,
|
||||
[varcharValue] NVARCHAR(512) NULL,
|
||||
[textValue] NVARCHAR(MAX) NULL
|
||||
);
|
||||
""";
|
||||
|
||||
private const string MergeAndCleanupSql = $"""
|
||||
-- Get distinct versionIds from the staged data.
|
||||
DECLARE @versionIds TABLE (versionId INT PRIMARY KEY);
|
||||
INSERT INTO @versionIds (versionId)
|
||||
SELECT DISTINCT versionId FROM [{TempTableName}];
|
||||
|
||||
-- Lock existing rows for the affected versionIds.
|
||||
SELECT id FROM [umbracoPropertyData] WITH (UPDLOCK, HOLDLOCK)
|
||||
WHERE versionId IN (SELECT versionId FROM @versionIds);
|
||||
|
||||
-- MERGE: UPDATE existing, INSERT new, DELETE removed.
|
||||
MERGE [umbracoPropertyData] AS target
|
||||
USING [{TempTableName}] AS source
|
||||
ON (
|
||||
target.versionId = source.versionId
|
||||
AND target.propertyTypeId = source.propertyTypeId
|
||||
AND (target.languageId = source.languageId OR (target.languageId IS NULL AND source.languageId IS NULL))
|
||||
AND (target.segment = source.segment OR (target.segment IS NULL AND source.segment IS NULL))
|
||||
)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET
|
||||
intValue = source.intValue,
|
||||
decimalValue = source.decimalValue,
|
||||
dateValue = source.dateValue,
|
||||
varcharValue = source.varcharValue,
|
||||
textValue = source.textValue
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (versionId, propertyTypeId, languageId, segment, intValue, decimalValue, dateValue, varcharValue, textValue)
|
||||
VALUES (source.versionId, source.propertyTypeId, source.languageId, source.segment,
|
||||
source.intValue, source.decimalValue, source.dateValue, source.varcharValue, source.textValue)
|
||||
WHEN NOT MATCHED BY SOURCE AND target.versionId IN (SELECT versionId FROM @versionIds) THEN
|
||||
DELETE;
|
||||
|
||||
-- Clean up the temp table.
|
||||
DROP TABLE [{TempTableName}];
|
||||
""";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? ProviderName => Constants.ProviderName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReplacePropertyData(IUmbracoDatabase database, int versionId, IEnumerable<PropertyDataDto> propertyDataDtos)
|
||||
{
|
||||
// Get the underlying SqlConnection and transaction.
|
||||
SqlConnection connection = NPocoDatabaseExtensions.GetTypedConnection<SqlConnection>(database.Connection);
|
||||
SqlTransaction? transaction = GetTransaction(database);
|
||||
|
||||
// Step 1: Create the temp table.
|
||||
using (var createCmd = new SqlCommand(CreateTempTableSql, connection, transaction))
|
||||
{
|
||||
createCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Step 2: Bulk copy the data into the temp table using SqlBulkCopy.
|
||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction))
|
||||
{
|
||||
bulkCopy.DestinationTableName = TempTableName;
|
||||
bulkCopy.BulkCopyTimeout = 0; // Use connection timeout
|
||||
bulkCopy.BatchSize = 4096; // Consistent with SqlServerBulkSqlInsertProvider
|
||||
|
||||
// Map columns explicitly by name.
|
||||
foreach (var columnName in _columnNames)
|
||||
{
|
||||
bulkCopy.ColumnMappings.Add(columnName, columnName);
|
||||
}
|
||||
|
||||
using var reader = new PropertyDataDtoDataReader(propertyDataDtos);
|
||||
bulkCopy.WriteToServer(reader);
|
||||
}
|
||||
|
||||
// Step 3: Execute the MERGE statement and clean up.
|
||||
using (var mergeCmd = new SqlCommand(MergeAndCleanupSql, connection, transaction))
|
||||
{
|
||||
mergeCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private static SqlTransaction? GetTransaction(IUmbracoDatabase database)
|
||||
{
|
||||
using DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty);
|
||||
return command.Transaction != null
|
||||
? NPocoDatabaseExtensions.GetTypedTransaction<SqlTransaction>(command.Transaction)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight IDataReader implementation for streaming PropertyDataDto to SqlBulkCopy.
|
||||
/// </summary>
|
||||
private sealed class PropertyDataDtoDataReader : IDataReader
|
||||
{
|
||||
private readonly IEnumerator<PropertyDataDto> _enumerator;
|
||||
private PropertyDataDto? _current;
|
||||
|
||||
public PropertyDataDtoDataReader(IEnumerable<PropertyDataDto> dtos)
|
||||
=> _enumerator = dtos.GetEnumerator();
|
||||
|
||||
public int FieldCount => _columnNames.Length;
|
||||
|
||||
public bool Read()
|
||||
{
|
||||
if (_enumerator.MoveNext())
|
||||
{
|
||||
_current = _enumerator.Current;
|
||||
return true;
|
||||
}
|
||||
|
||||
_current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public object GetValue(int i)
|
||||
{
|
||||
if (_current == null)
|
||||
{
|
||||
throw new InvalidOperationException("No current row.");
|
||||
}
|
||||
|
||||
return i switch
|
||||
{
|
||||
0 => _current.VersionId,
|
||||
1 => _current.PropertyTypeId,
|
||||
2 => _current.LanguageId.HasValue ? _current.LanguageId.Value : DBNull.Value,
|
||||
3 => _current.Segment ?? (object)DBNull.Value,
|
||||
4 => _current.IntegerValue.HasValue ? _current.IntegerValue.Value : DBNull.Value,
|
||||
5 => _current.DecimalValue.HasValue ? _current.DecimalValue.Value : DBNull.Value,
|
||||
6 => _current.DateValue.HasValue ? _current.DateValue.Value : DBNull.Value,
|
||||
7 => _current.VarcharValue ?? (object)DBNull.Value,
|
||||
8 => _current.TextValue ?? (object)DBNull.Value,
|
||||
_ => throw new IndexOutOfRangeException($"Column index {i} is out of range."),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetName(int i) => _columnNames[i];
|
||||
|
||||
public int GetOrdinal(string name) => Array.IndexOf(_columnNames, name);
|
||||
|
||||
public void Dispose() => _enumerator.Dispose();
|
||||
|
||||
// Required IDataReader members (minimal implementation for SqlBulkCopy)
|
||||
public void Close() => Dispose();
|
||||
|
||||
public int Depth => 0;
|
||||
|
||||
public bool IsClosed => false;
|
||||
|
||||
public int RecordsAffected => -1;
|
||||
|
||||
public DataTable GetSchemaTable() => throw new NotImplementedException();
|
||||
|
||||
public bool NextResult() => false;
|
||||
|
||||
// IDataRecord members
|
||||
public bool GetBoolean(int i) => throw new NotImplementedException();
|
||||
|
||||
public byte GetByte(int i) => throw new NotImplementedException();
|
||||
|
||||
public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotImplementedException();
|
||||
|
||||
public char GetChar(int i) => throw new NotImplementedException();
|
||||
|
||||
public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotImplementedException();
|
||||
|
||||
public IDataReader GetData(int i) => throw new NotImplementedException();
|
||||
|
||||
public string GetDataTypeName(int i) => throw new NotImplementedException();
|
||||
|
||||
public DateTime GetDateTime(int i) => throw new NotImplementedException();
|
||||
|
||||
public decimal GetDecimal(int i) => throw new NotImplementedException();
|
||||
|
||||
public double GetDouble(int i) => throw new NotImplementedException();
|
||||
|
||||
public Type GetFieldType(int i) => throw new NotImplementedException();
|
||||
|
||||
public float GetFloat(int i) => throw new NotImplementedException();
|
||||
|
||||
public Guid GetGuid(int i) => throw new NotImplementedException();
|
||||
|
||||
public short GetInt16(int i) => throw new NotImplementedException();
|
||||
|
||||
public int GetInt32(int i) => throw new NotImplementedException();
|
||||
|
||||
public long GetInt64(int i) => throw new NotImplementedException();
|
||||
|
||||
public string GetString(int i) => throw new NotImplementedException();
|
||||
|
||||
public int GetValues(object[] values) => throw new NotImplementedException();
|
||||
|
||||
public bool IsDBNull(int i) => GetValue(i) == DBNull.Value;
|
||||
|
||||
public object this[int i] => GetValue(i);
|
||||
|
||||
public object this[string name] => GetValue(GetOrdinal(name));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.DistributedLocking;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
|
||||
using Umbraco.Cms.Persistence.SqlServer.Interceptors;
|
||||
using Umbraco.Cms.Persistence.SqlServer.Operations;
|
||||
using Umbraco.Cms.Persistence.SqlServer.Services;
|
||||
|
||||
namespace Umbraco.Cms.Persistence.SqlServer;
|
||||
@@ -44,6 +45,10 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IProviderSpecificInterceptor, SqlServerAddRetryPolicyInterceptor>());
|
||||
|
||||
// Optimized database operations using SQL Server specific features.
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IPropertyDataReplacerOperation, SqlServerPropertyDataReplacerOperation>());
|
||||
|
||||
DbProviderFactories.UnregisterFactory(Constants.ProviderName);
|
||||
DbProviderFactories.RegisterFactory(Constants.ProviderName, SqlClientFactory.Instance);
|
||||
|
||||
|
||||
@@ -11,17 +11,6 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Provides cache refresh functionality for content items, ensuring that content-related caches are updated or
|
||||
/// invalidated in response to content changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ContentCacheRefresher coordinates cache invalidation for content, including memory caches, URL
|
||||
/// caches, navigation structures, and domain assignments. It responds to content change notifications and ensures that
|
||||
/// all relevant caches reflect the current state of published and unpublished content. This refresher is used
|
||||
/// internally to maintain cache consistency after content operations such as publish, unpublish, move,
|
||||
/// or delete.
|
||||
/// </remarks>
|
||||
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresherNotification,
|
||||
ContentCacheRefresher.JsonPayload>
|
||||
{
|
||||
@@ -36,9 +25,6 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
private readonly IPublishStatusManagementService _publishStatusManagementService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentCacheRefresher"/> class.
|
||||
/// </summary>
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -74,15 +60,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
#region Indirect
|
||||
|
||||
/// <summary>
|
||||
/// Clears cached content and public access data from the provided application caches.
|
||||
/// </summary>
|
||||
/// <param name="appCaches">The application caches instance from which to clear content and public access entries.</param>
|
||||
public static void RefreshContentTypes(AppCaches appCaches)
|
||||
{
|
||||
// We could try to have a mechanism to notify the PublishedCachesService
|
||||
// we could try to have a mechanism to notify the PublishedCachesService
|
||||
// and figure out whether published items were modified or not... keep it
|
||||
// simple for now, just clear the whole thing.
|
||||
// simple for now, just clear the whole thing
|
||||
appCaches.ClearPartialViewCache();
|
||||
|
||||
appCaches.IsolatedCaches.ClearCache<PublicAccessEntry>();
|
||||
@@ -93,22 +75,16 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
#region Define
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unique identifier for the cache refresher.
|
||||
/// </summary>
|
||||
public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Guid RefresherUniqueId => UniqueId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "ContentCacheRefresher";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresher
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RefreshInternal(JsonPayload[] payloads)
|
||||
{
|
||||
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
|
||||
@@ -145,15 +121,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
base.RefreshInternal(payloads);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(JsonPayload[] payloads)
|
||||
{
|
||||
var idsRemoved = new HashSet<int>();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
{
|
||||
// If the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content.
|
||||
// So track the IDs that have been removed.
|
||||
// if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
|
||||
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
|
||||
{
|
||||
idsRemoved.Add(payload.Id);
|
||||
@@ -164,18 +138,43 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
HandleNavigation(payload);
|
||||
HandlePublishedAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (payload.Id != default)
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
}
|
||||
if (payload.Key.HasValue)
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Key.Value);
|
||||
}
|
||||
|
||||
HandleIdKeyMap(payload);
|
||||
}
|
||||
|
||||
// Clear partial view cache when published content changes.
|
||||
// Clear partial view cache when published content changes
|
||||
if (ShouldClearPartialViewCache(payloads))
|
||||
{
|
||||
AppCaches.ClearPartialViewCache();
|
||||
}
|
||||
|
||||
// Clear the domain cache if any domain is assigned to removed content.
|
||||
HandleDomainCache(idsRemoved);
|
||||
if (idsRemoved.Count > 0)
|
||||
{
|
||||
var assignedDomains = _domainService.GetAll(true)
|
||||
?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList();
|
||||
|
||||
if (assignedDomains?.Count > 0)
|
||||
{
|
||||
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
|
||||
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
|
||||
// DomainCacheRefresher?
|
||||
ClearAllIsolatedCacheByEntityType<IDomain>();
|
||||
|
||||
// note: must do what's above FIRST else the repositories still have the old cached
|
||||
// content and when the PublishedCachesService is notified of changes it does not see
|
||||
// the new content...
|
||||
// notify
|
||||
_domainCacheService.Refresh(assignedDomains
|
||||
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
@@ -252,51 +251,18 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBranchUnpublished(JsonPayload payload) =>
|
||||
|
||||
private bool IsBranchUnpublished(JsonPayload payload)
|
||||
{
|
||||
// If unpublished cultures has one or more values, but published cultures does not, this means that the branch is unpublished entirely
|
||||
// And therefore should no longer be resolve-able from the cache, so we need to remove it instead.
|
||||
// Otherwise, some culture is still published, so it should be resolve-able from cache, and published cultures should instead be used.
|
||||
payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
|
||||
return payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
|
||||
(payload.PublishedCultures is null || payload.PublishedCultures.Length == 0);
|
||||
|
||||
private void HandleRouting(JsonPayload payload)
|
||||
{
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
|
||||
// Note that we need to clear the navigation service as the last thing.
|
||||
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out IEnumerable<Guid>? descendantsOrSelfKeys))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
|
||||
}
|
||||
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out IEnumerable<Guid>? descendantsOrSelfKeysInBin))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); // TODO: make async
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNavigation(JsonPayload payload)
|
||||
{
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentNavigationManagementService.RebuildAsync().GetAwaiter().GetResult();
|
||||
@@ -423,68 +389,50 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleIdKeyMap(JsonPayload payload)
|
||||
private void HandleRouting(JsonPayload payload)
|
||||
{
|
||||
// We only need to flush the ID/Key map when content is deleted.
|
||||
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove) is false)
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
{
|
||||
return;
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
|
||||
//Note the we need to clear the navigation service as the last thing
|
||||
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out var descendantsOrSelfKeys))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
|
||||
}
|
||||
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); //TODO make async
|
||||
}
|
||||
|
||||
if (payload.Id != default)
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (payload.Key.HasValue)
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Key.Value);
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void HandleDomainCache(HashSet<int> idsRemoved)
|
||||
{
|
||||
if (idsRemoved.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var assignedDomains = _domainService.GetAll(true)
|
||||
.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value))
|
||||
.ToList();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
if (assignedDomains.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
|
||||
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
|
||||
// DomainCacheRefresher?
|
||||
ClearAllIsolatedCacheByEntityType<IDomain>();
|
||||
|
||||
// note: must do what's above FIRST else the repositories still have the old cached
|
||||
// content and when the PublishedCachesService is notified of changes it does not see
|
||||
// the new content...
|
||||
// notify
|
||||
_domainCacheService.Refresh(assignedDomains
|
||||
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
|
||||
}
|
||||
|
||||
// These events should never trigger. Everything should be PAYLOAD/JSON.
|
||||
|
||||
/// <inheritdoc/>
|
||||
// these events should never trigger
|
||||
// everything should be PAYLOAD/JSON
|
||||
public override void RefreshAll() => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(int id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(Guid id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Remove(int id) => throw new NotSupportedException();
|
||||
|
||||
#endregion
|
||||
@@ -492,40 +440,19 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
#region Json
|
||||
|
||||
// TODO (V14): Change into a record
|
||||
/// <summary>
|
||||
/// Represents a JSON-serializable payload containing information about a content or tree change event, including
|
||||
/// identifiers, change types, and culture-specific publishing details.
|
||||
/// </summary>
|
||||
public class JsonPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique integer identifier for the entity.
|
||||
/// </summary>
|
||||
|
||||
public int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique GUID key associated with the entity, or null if no key is assigned.
|
||||
/// </summary>
|
||||
public Guid? Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the types of changes that have occurred in the tree.
|
||||
/// </summary>
|
||||
public TreeChangeTypes ChangeTypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content represents a document blueprint.
|
||||
/// </summary>
|
||||
public bool Blueprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of culture codes in which the content is published.
|
||||
/// </summary>
|
||||
public string[]? PublishedCultures { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of culture codes for which the content has been unpublished.
|
||||
/// </summary>
|
||||
public string[]? UnpublishedCultures { get; init; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
@@ -20,32 +18,7 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IContentTypeCommonRepository _contentTypeCommonRepository;
|
||||
|
||||
public DataTypeCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IPublishedModelFactory publishedModelFactory,
|
||||
IPublishedContentTypeFactory publishedContentTypeFactory,
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
IContentTypeCommonRepository contentTypeCommonRepository)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_publishedContentTypeFactory = publishedContentTypeFactory;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_contentTypeCommonRepository = contentTypeCommonRepository;
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V18.")]
|
||||
public DataTypeCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -57,19 +30,14 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
publishedModelFactory,
|
||||
publishedContentTypeFactory,
|
||||
publishedContentTypeCache,
|
||||
documentCacheService,
|
||||
mediaCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IContentTypeCommonRepository>())
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_publishedContentTypeFactory = publishedContentTypeFactory;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
}
|
||||
|
||||
#region Json
|
||||
@@ -116,9 +84,6 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
ClearAllIsolatedCacheByEntityType<IMember>();
|
||||
ClearAllIsolatedCacheByEntityType<IMemberType>();
|
||||
|
||||
// Also clear the 5 minute runtime cache held in ContentTypeCommonRepository.
|
||||
_contentTypeCommonRepository.ClearCache();
|
||||
|
||||
Attempt<IAppPolicyCache?> dataTypeCache = AppCaches.IsolatedCaches.Get<IDataType>();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
|
||||
@@ -29,7 +29,7 @@ public class TypeFinder : ITypeFinder
|
||||
"DataAnnotationsExtensions,", "DataAnnotationsExtensions.", "Dynamic,", "Examine,", "Examine.",
|
||||
"HtmlAgilityPack,", "HtmlAgilityPack.", "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate
|
||||
"JetBrains.Annotations,", "LightInject.", // DI
|
||||
"LightInject,", "Lucene.", "Markdig,", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
|
||||
"LightInject,", "Lucene.", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
|
||||
"NHibernate.", "Newtonsoft.", "NPoco,", "NuGet.", "RouteDebugger,", "Semver.", "Serilog.", "Serilog,",
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
|
||||
@@ -9,11 +9,6 @@ public interface IReadOnlyUserGroup
|
||||
{
|
||||
string? Name { get; }
|
||||
|
||||
string Alias { get; }
|
||||
|
||||
// TODO (V18): Remove the default implementations.
|
||||
string? Description { get { return null; } }
|
||||
|
||||
string? Icon { get; }
|
||||
|
||||
int Id { get; }
|
||||
@@ -24,6 +19,11 @@ public interface IReadOnlyUserGroup
|
||||
|
||||
int? StartMediaId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The alias
|
||||
/// </summary>
|
||||
string Alias { get; }
|
||||
|
||||
// This is set to return true as default to avoid breaking changes.
|
||||
bool HasAccessToAllLanguages => true;
|
||||
|
||||
|
||||
@@ -4,24 +4,12 @@ using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user group in Umbraco.
|
||||
/// </summary>
|
||||
public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alias of the user group.
|
||||
/// </summary>
|
||||
string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting content node ID for this user group.
|
||||
/// </summary>
|
||||
int? StartContentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting media node ID for this user group.
|
||||
/// </summary>
|
||||
int? StartMediaId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -34,18 +22,6 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// </summary>
|
||||
string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the user group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO (V18): Remove the default implementations.
|
||||
/// </remarks>
|
||||
string? Description
|
||||
{
|
||||
get => null;
|
||||
set { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this property is true it will give the group access to all languages
|
||||
/// </summary>
|
||||
@@ -64,57 +40,26 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// </remarks>
|
||||
ISet<string> Permissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the granular permissions for this user group.
|
||||
/// </summary>
|
||||
ISet<IGranularPermission> GranularPermissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of section aliases that this user group has access to.
|
||||
/// </summary>
|
||||
IEnumerable<string> AllowedSections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to a section for this user group.
|
||||
/// </summary>
|
||||
/// <param name="sectionAlias">The alias of the section to remove.</param>
|
||||
void RemoveAllowedSection(string sectionAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Adds access to a section for this user group.
|
||||
/// </summary>
|
||||
/// <param name="sectionAlias">The alias of the section to add.</param>
|
||||
void AddAllowedSection(string sectionAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to all sections for this user group.
|
||||
/// </summary>
|
||||
void ClearAllowedSections();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of language IDs that this user group has access to.
|
||||
/// </summary>
|
||||
IEnumerable<int> AllowedLanguages => Enumerable.Empty<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to a language for this user group.
|
||||
/// </summary>
|
||||
/// <param name="languageId">The ID of the language to remove.</param>
|
||||
void RemoveAllowedLanguage(int languageId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds access to a language for this user group.
|
||||
/// </summary>
|
||||
/// <param name="languageId">The ID of the language to add.</param>
|
||||
void AddAllowedLanguage(int languageId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to all languages for this user group.
|
||||
/// </summary>
|
||||
void ClearAllowedLanguages()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGroup>
|
||||
{
|
||||
[Obsolete("Please use the constructor that includes all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ReadOnlyUserGroup(
|
||||
int id,
|
||||
Guid key,
|
||||
@@ -18,43 +17,11 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
ISet<string> permissions,
|
||||
ISet<IGranularPermission> granularPermissions,
|
||||
bool hasAccessToAllLanguages)
|
||||
: this(
|
||||
id,
|
||||
key,
|
||||
name,
|
||||
null,
|
||||
icon,
|
||||
startContentId,
|
||||
startMediaId,
|
||||
alias,
|
||||
allowedLanguages,
|
||||
allowedSections,
|
||||
permissions,
|
||||
granularPermissions,
|
||||
hasAccessToAllLanguages)
|
||||
{
|
||||
}
|
||||
|
||||
public ReadOnlyUserGroup(
|
||||
int id,
|
||||
Guid key,
|
||||
string? name,
|
||||
string? description,
|
||||
string? icon,
|
||||
int? startContentId,
|
||||
int? startMediaId,
|
||||
string? alias,
|
||||
IEnumerable<int> allowedLanguages,
|
||||
IEnumerable<string> allowedSections,
|
||||
ISet<string> permissions,
|
||||
ISet<IGranularPermission> granularPermissions,
|
||||
bool hasAccessToAllLanguages)
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
Icon = icon;
|
||||
Id = id;
|
||||
Key = key;
|
||||
Name = name ?? string.Empty;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
Alias = alias ?? string.Empty;
|
||||
AllowedLanguages = allowedLanguages.ToArray();
|
||||
AllowedSections = allowedSections.ToArray();
|
||||
@@ -71,9 +38,22 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
|
||||
public Guid Key { get; }
|
||||
|
||||
public string Name { get; }
|
||||
public bool Equals(ReadOnlyUserGroup? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public string? Description { get; }
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(Alias, other.Alias);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? Icon { get; }
|
||||
|
||||
@@ -97,7 +77,7 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is null)
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -115,21 +95,6 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
return Equals((ReadOnlyUserGroup)obj);
|
||||
}
|
||||
|
||||
public bool Equals(ReadOnlyUserGroup? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(Alias, other.Alias);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode();
|
||||
|
||||
public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right);
|
||||
|
||||
@@ -30,7 +30,6 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
private string _alias;
|
||||
private string? _icon;
|
||||
private string _name;
|
||||
private string? _description;
|
||||
private bool _hasAccessToAllLanguages;
|
||||
private ISet<string> _permissions;
|
||||
private ISet<IGranularPermission> _granularPermissions;
|
||||
@@ -113,13 +112,6 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name));
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public string? Description
|
||||
{
|
||||
get => _description;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _description!, nameof(Description));
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public bool HasAccessToAllLanguages
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@ public static class UserGroupExtensions
|
||||
group.Id,
|
||||
group.Key,
|
||||
group.Name,
|
||||
group.Description,
|
||||
group.Icon,
|
||||
group.StartContentId,
|
||||
group.StartMediaId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
@@ -8,25 +8,33 @@ namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
public static class RepositoryCacheKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// A thread-safe cache for the keys so we don't keep allocating strings.
|
||||
/// A cache for the keys we don't keep allocating strings.
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<Type, string> _keys = new();
|
||||
private static readonly Dictionary<Type, string> Keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache key for the provided type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
|
||||
/// <returns>A cache key string in the format "uRepo_{TypeName}_".</returns>
|
||||
public static string GetKey<T>()
|
||||
=> _keys.GetOrAdd(typeof(T), static type => "uRepo_" + type.Name + "_");
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
// The following code is a micro-optimization to avoid an unnecessary lookup in the Keys dictionary, when writing the newly created key.
|
||||
// Previously, the code was:
|
||||
// return Keys.TryGetValue(type, out var key)
|
||||
// ? key
|
||||
// : Keys[type] = "uRepo_" + type.Name + "_";
|
||||
|
||||
// Look up the existing value or get a reference to the newly created default value.
|
||||
ref string? key = ref CollectionsMarshal.GetValueRefOrAddDefault(Keys, type, out _);
|
||||
|
||||
// As we have the reference, we can just assign it if null, without the expensive write back to the dictionary.
|
||||
return key ??= "uRepo_" + type.Name + "_";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache key for the provided type and Id.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity identifier.</typeparam>
|
||||
/// <param name="id">The entity identifier.</param>
|
||||
/// <returns>A cache key string in the format "uRepo_{TypeName}_{Id}", or an empty string if the id is the default value.</returns>
|
||||
public static string GetKey<T, TId>(TId? id)
|
||||
{
|
||||
if (EqualityComparer<TId?>.Default.Equals(id, default))
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
[DataContract]
|
||||
public class ConfigurationField
|
||||
{
|
||||
private readonly string? _view;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
|
||||
/// </summary>
|
||||
@@ -20,7 +22,6 @@ public class ConfigurationField
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
|
||||
/// </summary>
|
||||
/// <param name="validators">The validators for this configuration field.</param>
|
||||
public ConfigurationField(params IValueValidator[] validators)
|
||||
: this(validators.ToList())
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Core.Routing;
|
||||
public class DefaultUrlProvider : IUrlProvider
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILocalizedTextService? _localizedTextService;
|
||||
private readonly ILogger<DefaultUrlProvider> _logger;
|
||||
private readonly ISiteDomainMapper _siteDomainMapper;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
@@ -29,18 +30,6 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
private readonly UriUtility _uriUtility;
|
||||
private RequestHandlerSettings _requestSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requestSettings">The request handler settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="siteDomainMapper">The site domain mapper.</param>
|
||||
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
|
||||
/// <param name="uriUtility">The URI utility.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="navigationQueryService">The document navigation query service.</param>
|
||||
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
|
||||
/// <param name="publishedUrlProvider">The published URL provider.</param>
|
||||
public DefaultUrlProvider(
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
ILogger<DefaultUrlProvider> logger,
|
||||
@@ -65,17 +54,6 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
requestSettings.OnChange(x => _requestSettings = x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requestSettings">The request handler settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="siteDomainMapper">The site domain mapper.</param>
|
||||
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
|
||||
/// <param name="uriUtility">The URI utility.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="navigationQueryService">The document navigation query service.</param>
|
||||
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
|
||||
[Obsolete("Use the other constructor - Scheduled for removal in V18")]
|
||||
public DefaultUrlProvider(
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -7,19 +7,7 @@ using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a legacy published content request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class is deprecated and scheduled for removal. Its functionality is being
|
||||
/// migrated to the newer <see cref="IPublishedRequest"/> implementations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// TODO: Kill this, but we need to port all of its functionality.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete("This class is deprecated and will be removed in a future version. Use IPublishedRequest instead. Scheduled for removal in Umbraco 19.")]
|
||||
// TODO: Kill this, but we need to port all of it's functionality
|
||||
public class PublishedRequestOld // : IPublishedRequest
|
||||
{
|
||||
private readonly IPublishedRouter _publishedRouter;
|
||||
@@ -27,14 +15,13 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
private CultureInfo? _culture;
|
||||
private DomainAndUri? _domain;
|
||||
private bool _is404;
|
||||
private IPublishedContent? _publishedContent;
|
||||
|
||||
private bool _readonly; // after prepared
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishedRequestOld" /> class.
|
||||
/// Initializes a new instance of the <see cref="PublishedRequest" /> class.
|
||||
/// </summary>
|
||||
/// <param name="publishedRouter">The published router.</param>
|
||||
/// <param name="umbracoContext">The Umbraco context.</param>
|
||||
/// <param name="webRoutingSettings">The web routing settings.</param>
|
||||
/// <param name="uri">The optional URI for this request. If not provided, uses the cleaned Umbraco URL from the context.</param>
|
||||
public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions<WebRoutingSettings> webRoutingSettings, Uri? uri = null)
|
||||
{
|
||||
UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
|
||||
@@ -54,9 +41,6 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
/// <remarks>The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc.</remarks>
|
||||
public Uri Uri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether caching should be disabled for this request.
|
||||
/// </summary>
|
||||
public bool CacheabilityNoCache { get; set; }
|
||||
|
||||
///// <summary>
|
||||
@@ -118,15 +102,13 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the request is in a writeable state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is a no-op. The readonly check was removed because the backing field
|
||||
/// was never set to true, making the check unnecessary. Retained for API compatibility.
|
||||
/// </remarks>
|
||||
// utility for ensuring it is ok to set some properties
|
||||
public void EnsureWriteable()
|
||||
{
|
||||
if (_readonly)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only.");
|
||||
}
|
||||
}
|
||||
|
||||
// #region Events
|
||||
@@ -244,19 +226,18 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
/// <summary>
|
||||
/// Gets value indicating whether the current published content is the initial one.
|
||||
/// </summary>
|
||||
/// <remarks>Always returns false as _publishedContent field was removed (never assigned).</remarks>
|
||||
public bool IsInitialPublishedContent => false;
|
||||
public bool IsInitialPublishedContent =>
|
||||
InitialPublishedContent != null && InitialPublishedContent == _publishedContent;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the current PublishedContent is the initial one.
|
||||
/// </summary>
|
||||
/// <remarks>Note: _publishedContent field was removed (never assigned), so this sets InitialPublishedContent to null.</remarks>
|
||||
public void SetIsInitialPublishedContent()
|
||||
{
|
||||
EnsureWriteable();
|
||||
|
||||
// note: it can very well be null if the initial content was not found
|
||||
InitialPublishedContent = null;
|
||||
InitialPublishedContent = _publishedContent;
|
||||
IsInternalRedirectPublishedContent = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,12 +52,13 @@ internal abstract class ContentValidationServiceBase<TContentType>
|
||||
|
||||
// We don't have managed segments, so we have to make do with the ones passed in the model.
|
||||
var segments =
|
||||
new string?[] { null }
|
||||
.Union(contentEditingModelBase.Variants
|
||||
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
|
||||
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
|
||||
.WhereNotNull())
|
||||
.ToArray();
|
||||
new string?[] { null }
|
||||
.Union(contentEditingModelBase.Variants
|
||||
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
|
||||
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
|
||||
.WhereNotNull()
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
foreach (IPropertyType propertyType in invariantPropertyTypes)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines methods for resolving user keys to user IDs and vice versa without retrieving full user details.
|
||||
/// </summary>
|
||||
public interface IUserIdKeyResolver
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -548,8 +548,7 @@ namespace Umbraco.Cms.Core.Services
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
List<Guid> idsAsList = [.. ids];
|
||||
IQuery<IMember> query = Query<IMember>().Where(x => idsAsList.Contains(x.Key));
|
||||
IQuery<IMember> query = Query<IMember>().Where(x => ids.Contains(x.Key));
|
||||
return Task.FromResult(_memberRepository.Get(query));
|
||||
}
|
||||
|
||||
|
||||
+4
-9
@@ -5,28 +5,23 @@ using Umbraco.Cms.Core.Notifications;
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for seeding the in-memory publish status cache at application's startup
|
||||
/// by loading all data from the database.
|
||||
/// Responsible for seeding the in-memory publish status cache at application's startup
|
||||
/// by loading all data from the database.
|
||||
/// </summary>
|
||||
public sealed class PublishStatusInitializationNotificationHandler : INotificationAsyncHandler<PostRuntimePremigrationsUpgradeNotification>
|
||||
{
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IPublishStatusManagementService _publishStatusManagementService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishStatusInitializationNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runtimeState">The runtime state.</param>
|
||||
/// <param name="publishStatusManagementService">The publish status management service.</param>
|
||||
public PublishStatusInitializationNotificationHandler(
|
||||
IRuntimeState runtimeState,
|
||||
IPublishStatusManagementService publishStatusManagementService)
|
||||
IPublishStatusManagementService publishStatusManagementService
|
||||
)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_publishStatusManagementService = publishStatusManagementService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(PostRuntimePremigrationsUpgradeNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if(_runtimeState.Level < RuntimeLevel.Upgrade)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the Umbraco Service context, which provides access to all services.
|
||||
@@ -34,35 +34,8 @@ public class ServiceContext
|
||||
private readonly Lazy<IWebhookService>? _webhookService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceContext" /> class with lazy services.
|
||||
/// Initializes a new instance of the <see cref="ServiceContext" /> class with lazy services.
|
||||
/// </summary>
|
||||
/// <param name="publicAccessService">The public access service.</param>
|
||||
/// <param name="domainService">The domain service.</param>
|
||||
/// <param name="auditService">The audit service.</param>
|
||||
/// <param name="localizedTextService">The localized text service.</param>
|
||||
/// <param name="tagService">The tag service.</param>
|
||||
/// <param name="contentService">The content service.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <param name="memberService">The member service.</param>
|
||||
/// <param name="mediaService">The media service.</param>
|
||||
/// <param name="contentTypeService">The content type service.</param>
|
||||
/// <param name="mediaTypeService">The media type service.</param>
|
||||
/// <param name="dataTypeService">The data type service.</param>
|
||||
/// <param name="fileService">The file service.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="packagingService">The packaging service.</param>
|
||||
/// <param name="serverRegistrationService">The server registration service.</param>
|
||||
/// <param name="entityService">The entity service.</param>
|
||||
/// <param name="relationService">The relation service.</param>
|
||||
/// <param name="memberTypeService">The member type service.</param>
|
||||
/// <param name="memberGroupService">The member group service.</param>
|
||||
/// <param name="notificationService">The notification service.</param>
|
||||
/// <param name="externalLoginService">The external login service.</param>
|
||||
/// <param name="redirectUrlService">The redirect URL service.</param>
|
||||
/// <param name="consentService">The consent service.</param>
|
||||
/// <param name="keyValueService">The key-value service.</param>
|
||||
/// <param name="contentTypeBaseServiceProvider">The content type base service provider.</param>
|
||||
/// <param name="webhookService">The webhook service.</param>
|
||||
public ServiceContext(
|
||||
Lazy<IPublicAccessService>? publicAccessService,
|
||||
Lazy<IDomainService>? domainService,
|
||||
@@ -257,38 +230,10 @@ public class ServiceContext
|
||||
public IWebhookService? WebhookService => _webhookService?.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a partial service context with only some services (for tests).
|
||||
/// Creates a partial service context with only some services (for tests).
|
||||
/// </summary>
|
||||
/// <param name="contentService">The content service.</param>
|
||||
/// <param name="mediaService">The media service.</param>
|
||||
/// <param name="contentTypeService">The content type service.</param>
|
||||
/// <param name="mediaTypeService">The media type service.</param>
|
||||
/// <param name="dataTypeService">The data type service.</param>
|
||||
/// <param name="fileService">The file service.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="packagingService">The packaging service.</param>
|
||||
/// <param name="entityService">The entity service.</param>
|
||||
/// <param name="relationService">The relation service.</param>
|
||||
/// <param name="memberGroupService">The member group service.</param>
|
||||
/// <param name="memberTypeService">The member type service.</param>
|
||||
/// <param name="memberService">The member service.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <param name="tagService">The tag service.</param>
|
||||
/// <param name="notificationService">The notification service.</param>
|
||||
/// <param name="localizedTextService">The localized text service.</param>
|
||||
/// <param name="auditService">The audit service.</param>
|
||||
/// <param name="domainService">The domain service.</param>
|
||||
/// <param name="publicAccessService">The public access service.</param>
|
||||
/// <param name="externalLoginService">The external login service.</param>
|
||||
/// <param name="serverRegistrationService">The server registration service.</param>
|
||||
/// <param name="redirectUrlService">The redirect URL service.</param>
|
||||
/// <param name="consentService">The consent service.</param>
|
||||
/// <param name="keyValueService">The key-value service.</param>
|
||||
/// <param name="contentTypeBaseServiceProvider">The content type base service provider.</param>
|
||||
/// <param name="webhookService">The webhook service.</param>
|
||||
/// <returns>A new <see cref="ServiceContext"/> instance with the specified services.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Using a true constructor for this confuses DI containers.</para>
|
||||
/// <para>Using a true constructor for this confuses DI containers.</para>
|
||||
/// </remarks>
|
||||
public static ServiceContext CreatePartial(
|
||||
IContentService? contentService = null,
|
||||
@@ -351,6 +296,7 @@ public class ServiceContext
|
||||
Lazy(consentService),
|
||||
Lazy(keyValueService),
|
||||
Lazy(contentTypeBaseServiceProvider),
|
||||
Lazy(webhookService));
|
||||
Lazy(webhookService)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.Strings;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that converts Markdown-formatted text to HTML.
|
||||
/// </summary>
|
||||
public interface IMarkdownToHtmlConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the specified Markdown-formatted text to an HTML-encoded string.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The input string containing Markdown syntax to be converted.</param>
|
||||
/// <returns>A string containing the HTML representation of the input Markdown.</returns>
|
||||
public string ToHtml(string markdown);
|
||||
}
|
||||
@@ -7,22 +7,11 @@ using Umbraco.Cms.Core.Sync;
|
||||
|
||||
namespace Umbraco.Cms.Core.Webhooks.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Webhook event that fires when user group permissions are assigned.
|
||||
/// </summary>
|
||||
[WebhookEvent("User Group Permissions Assigned")]
|
||||
public class AssignedUserGroupPermissionsWebhookEvent : WebhookEventBase<AssignedUserGroupPermissionsNotification>
|
||||
{
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssignedUserGroupPermissionsWebhookEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="webhookFiringService">The webhook firing service.</param>
|
||||
/// <param name="webHookService">The webhook service.</param>
|
||||
/// <param name="webhookSettings">The webhook settings.</param>
|
||||
/// <param name="serverRoleAccessor">The server role accessor.</param>
|
||||
/// <param name="idKeyMap">The ID to key mapping service.</param>
|
||||
public AssignedUserGroupPermissionsWebhookEvent(
|
||||
IWebhookFiringService webhookFiringService,
|
||||
IWebhookService webHookService,
|
||||
@@ -34,14 +23,11 @@ public class AssignedUserGroupPermissionsWebhookEvent : WebhookEventBase<Assigne
|
||||
_idKeyMap = idKeyMap;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Alias => Constants.WebhookEvents.Aliases.AssignedUserGroupPermissions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertNotificationToRequestPayload(AssignedUserGroupPermissionsNotification notification)
|
||||
=> notification.EntityPermissions.Select(permission =>
|
||||
new
|
||||
{
|
||||
new {
|
||||
UserId = _idKeyMap.GetKeyForId(permission.EntityId, UmbracoObjectTypes.Unknown).Result,
|
||||
UserGroupId = _idKeyMap.GetKeyForId(permission.UserGroupId, UmbracoObjectTypes.Unknown).Result,
|
||||
});
|
||||
|
||||
@@ -46,8 +46,7 @@ public static class WebhookEventCollectionBuilderCmsUserExtensions
|
||||
/// The builder.
|
||||
/// </returns>
|
||||
public static WebhookEventCollectionBuilderCmsUser AddLogin(this WebhookEventCollectionBuilderCmsUser builder, WebhookPayloadType payloadType = WebhookPayloadType.Legacy)
|
||||
{
|
||||
switch (payloadType)
|
||||
{switch (payloadType)
|
||||
{
|
||||
case WebhookPayloadType.Extended:
|
||||
case WebhookPayloadType.Minimal:
|
||||
|
||||
@@ -25,20 +25,10 @@ namespace Umbraco.Cms.Core.Cache;
|
||||
internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyBase<TEntity, TId>
|
||||
where TEntity : class, IEntity
|
||||
{
|
||||
private static readonly TId[] _emptyIds = []; // const
|
||||
private static readonly TId[] EmptyIds = new TId[0]; // const
|
||||
private readonly Func<TEntity, TId> _entityGetId;
|
||||
private readonly bool _expires;
|
||||
private readonly Lock _getAllLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FullDataSetRepositoryCachePolicy{TEntity, TId}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache to use for storing entities.</param>
|
||||
/// <param name="scopeAccessor">The scope accessor for accessing the current scope.</param>
|
||||
/// <param name="repositoryCacheVersionService">The service for managing cache version synchronization.</param>
|
||||
/// <param name="cacheSyncService">The service for synchronizing cache changes across servers.</param>
|
||||
/// <param name="entityGetId">A function to extract the identifier from an entity.</param>
|
||||
/// <param name="expires">Whether cached items should expire after a timeout.</param>
|
||||
public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, IRepositoryCacheVersionService repositoryCacheVersionService, ICacheSyncService cacheSyncService, Func<TEntity, TId> entityGetId, bool expires)
|
||||
: base(cache, scopeAccessor, repositoryCacheVersionService, cacheSyncService)
|
||||
{
|
||||
@@ -49,7 +39,10 @@ internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : Repositor
|
||||
/// <inheritdoc />
|
||||
public override void Create(TEntity entity, Action<TEntity> persistNew)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -61,7 +54,7 @@ internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : Repositor
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetEntityTypeCacheKey() => RepositoryCacheKeys.GetKey<TEntity>();
|
||||
private string GetEntityTypeCacheKey() => RepositoryCacheKeys.GetKey<TEntity>();
|
||||
|
||||
private void InsertEntities(TEntity[]? entities)
|
||||
{
|
||||
@@ -95,7 +88,10 @@ internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : Repositor
|
||||
/// <inheritdoc />
|
||||
public override void Update(TEntity entity, Action<TEntity> persistUpdated)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -114,7 +110,10 @@ internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : Repositor
|
||||
/// <inheritdoc />
|
||||
public override void Delete(TEntity entity, Action<TEntity> persistDeleted)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entity));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -191,37 +190,19 @@ internal sealed class FullDataSetRepositoryCachePolicy<TEntity, TId> : Repositor
|
||||
/// <inheritdoc />
|
||||
public override void ClearAll() => Cache.Clear(GetEntityTypeCacheKey());
|
||||
|
||||
/// <summary>
|
||||
/// Gets all cached entities, or retrieves them from the repository if not cached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses double-check locking to prevent the "thundering herd" problem where multiple
|
||||
/// threads detecting a cache miss would all query the database simultaneously.
|
||||
/// Does NOT clone anything, so be nice with the returned values.
|
||||
/// </remarks>
|
||||
// does NOT clone anything, so be nice with the returned values
|
||||
internal IEnumerable<TEntity> GetAllCached(Func<TId[], IEnumerable<TEntity>?> performGetAll)
|
||||
{
|
||||
// Fast path - check cache without lock.
|
||||
// try the cache first
|
||||
DeepCloneableList<TEntity>? all = Cache.GetCacheItem<DeepCloneableList<TEntity>>(GetEntityTypeCacheKey());
|
||||
if (all != null)
|
||||
{
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
// Slow path - lock to prevent thundering herd on cache miss.
|
||||
lock (_getAllLock)
|
||||
{
|
||||
// Double-check inside lock - another thread may have populated the cache.
|
||||
all = Cache.GetCacheItem<DeepCloneableList<TEntity>>(GetEntityTypeCacheKey());
|
||||
if (all != null)
|
||||
{
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
// Only one thread queries the database.
|
||||
TEntity[]? entities = performGetAll(_emptyIds)?.WhereNotNull().ToArray();
|
||||
InsertEntities(entities); // may be an empty array...
|
||||
return entities ?? Enumerable.Empty<TEntity>();
|
||||
}
|
||||
// else get from repo and cache
|
||||
TEntity[]? entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray();
|
||||
InsertEntities(entities); // may be an empty array...
|
||||
return entities ?? Enumerable.Empty<TEntity>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.Remove<SimpleRichTextValueConverter>();
|
||||
|
||||
// register *all* checks, except those marked [HideFromTypeFinder] of course
|
||||
builder.Services.AddSingleton<Core.HealthChecks.NotificationMethods.IMarkdownToHtmlConverter, MarkdownToHtmlConverter>();
|
||||
builder.Services.AddSingleton<IMarkdownToHtmlConverter, MarkdownToHtmlConverter>();
|
||||
|
||||
builder.Services.AddSingleton<IContentLastChanceFinder, ContentFinderByConfigured404>();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DynamicRoot.QuerySteps;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
@@ -20,7 +21,10 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// </summary>
|
||||
internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder)
|
||||
{
|
||||
// repositories
|
||||
// Database provider operation factory - used by repositories for optimized provider-specific operations
|
||||
builder.Services.AddSingleton<IDatabaseProviderOperationFactory, DatabaseProviderOperationFactory>();
|
||||
|
||||
// Repositories.
|
||||
builder.Services.AddUnique<IAuditRepository, AuditRepository>();
|
||||
builder.Services.AddUnique<IAuditEntryRepository, AuditEntryRepository>();
|
||||
builder.Services.AddUnique<ICacheInstructionRepository, CacheInstructionRepository>();
|
||||
|
||||
@@ -23,7 +23,6 @@ using Umbraco.Cms.Infrastructure.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Services;
|
||||
using Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Telemetry.Providers;
|
||||
using Umbraco.Cms.Infrastructure.Templates.PartialViews;
|
||||
using Umbraco.Extensions;
|
||||
@@ -88,11 +87,6 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IMediaSearchService, MediaSearchService>();
|
||||
builder.Services.AddUnique<IDistributedJobService, DistributedJobService>();
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO (V18): Replace this with MarkdigMarkdownToHtmlConverter as the default implementation.
|
||||
builder.Services.AddUnique<IMarkdownToHtmlConverter, HeyRedMarkdownToHtmlConverter>();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using HeyRed.MarkdownSharp;
|
||||
using Umbraco.Cms.Core.HealthChecks;
|
||||
using Umbraco.Cms.Core.HealthChecks.NotificationMethods;
|
||||
|
||||
@@ -7,19 +6,10 @@ namespace Umbraco.Cms.Infrastructure.HealthChecks;
|
||||
|
||||
public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter
|
||||
{
|
||||
private readonly Core.Strings.IMarkdownToHtmlConverter _markdownToHtmlConverter;
|
||||
|
||||
public MarkdownToHtmlConverter(Core.Strings.IMarkdownToHtmlConverter markdownToHtmlConverter) => _markdownToHtmlConverter = markdownToHtmlConverter;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MarkdownToHtmlConverter()
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<Core.Strings.IMarkdownToHtmlConverter>())
|
||||
{
|
||||
}
|
||||
|
||||
public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity)
|
||||
{
|
||||
var html = _markdownToHtmlConverter.ToHtml(results.ResultsAsMarkDown(verbosity));
|
||||
var mark = new Markdown();
|
||||
var html = mark.Transform(results.ResultsAsMarkDown(verbosity));
|
||||
html = ApplyHtmlHighlighting(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -1312,7 +1312,6 @@ internal sealed class DatabaseDataCreator
|
||||
StartContentId = -1,
|
||||
Alias = Constants.Security.AdminGroupAlias,
|
||||
Name = "Administrators",
|
||||
Description = "Users with full access to all sections and functionality",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
UpdateDate = DateTime.UtcNow,
|
||||
Icon = "icon-medal",
|
||||
@@ -1330,7 +1329,6 @@ internal sealed class DatabaseDataCreator
|
||||
StartContentId = -1,
|
||||
Alias = WriterGroupAlias,
|
||||
Name = "Writers",
|
||||
Description = "Users with permission to create and update but not publish content",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
UpdateDate = DateTime.UtcNow,
|
||||
Icon = "icon-edit",
|
||||
@@ -1348,7 +1346,6 @@ internal sealed class DatabaseDataCreator
|
||||
StartContentId = -1,
|
||||
Alias = EditorGroupAlias,
|
||||
Name = "Editors",
|
||||
Description = "Users with full permission to create, update and publish content",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
UpdateDate = DateTime.UtcNow,
|
||||
Icon = "icon-tools",
|
||||
@@ -1366,7 +1363,6 @@ internal sealed class DatabaseDataCreator
|
||||
StartContentId = -1,
|
||||
Alias = TranslatorGroupAlias,
|
||||
Name = "Translators",
|
||||
Description = "Users with permission to manage dictionary entries",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
UpdateDate = DateTime.UtcNow,
|
||||
Icon = "icon-globe",
|
||||
@@ -1382,7 +1378,6 @@ internal sealed class DatabaseDataCreator
|
||||
Key = Constants.Security.SensitiveDataGroupKey,
|
||||
Alias = SensitiveDataGroupAlias,
|
||||
Name = "Sensitive data",
|
||||
Description = "Users with the specific permission to be able to manage properties and data marked as sensitive",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
UpdateDate = DateTime.UtcNow,
|
||||
Icon = "icon-lock",
|
||||
|
||||
@@ -147,9 +147,6 @@ public class UmbracoPlan : MigrationPlan
|
||||
// To 17.1.0
|
||||
To<V_17_1_0.ChangeValidationRegExpToNvarcharMax>("{1CE2E78B-E736-45D8-97A2-CE3EF2F31BCD}");
|
||||
|
||||
// To 17.2.0
|
||||
To<V_17_2_0.AddDescriptionToUserGroup>("{F1A2B3C4-D5E6-4789-ABCD-1234567890AB}");
|
||||
|
||||
// To 18.0.0
|
||||
// TODO (V18): Enable on 18 branch
|
||||
//// To<V_18_0_0.MigrateSingleBlockList>("{74332C49-B279-4945-8943-F8F00B1F5949}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Net;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
@@ -52,8 +52,7 @@ public class ChangeLogStatusCode : MigrationBase
|
||||
[PrimaryKeyColumn(AutoIncrement = true)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("webhookKey")]
|
||||
public Guid WebhookKey { get; set; }
|
||||
[Column("webhookKey")] public Guid WebhookKey { get; set; }
|
||||
|
||||
[Column(Name = "key")]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
@@ -201,17 +201,14 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
[Constraint(Default = "0")]
|
||||
public bool NoConsole { get; set; }
|
||||
|
||||
[Column("userName")]
|
||||
public string UserName { get; set; } = null!;
|
||||
[Column("userName")] public string UserName { get; set; } = null!;
|
||||
|
||||
[Column("userLogin")]
|
||||
[Length(125)]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public string? Login { get; set; }
|
||||
|
||||
[Column("userPassword")]
|
||||
[Length(500)]
|
||||
public string? Password { get; set; }
|
||||
[Column("userPassword")] [Length(500)] public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
|
||||
@@ -221,8 +218,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
[Length(500)]
|
||||
public string? PasswordConfig { get; set; }
|
||||
|
||||
[Column("userEmail")]
|
||||
public string Email { get; set; } = null!;
|
||||
[Column("userEmail")] public string Email { get; set; } = null!;
|
||||
|
||||
[Column("userLanguage")]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
@@ -324,17 +320,14 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
[Constraint(Default = "0")]
|
||||
public bool NoConsole { get; set; }
|
||||
|
||||
[Column("userName")]
|
||||
public string UserName { get; set; } = null!;
|
||||
[Column("userName")] public string UserName { get; set; } = null!;
|
||||
|
||||
[Column("userLogin")]
|
||||
[Length(125)]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public string? Login { get; set; }
|
||||
|
||||
[Column("userPassword")]
|
||||
[Length(500)]
|
||||
public string? Password { get; set; }
|
||||
[Column("userPassword")] [Length(500)] public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
|
||||
@@ -344,8 +337,7 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
[Length(500)]
|
||||
public string? PasswordConfig { get; set; }
|
||||
|
||||
[Column("userEmail")]
|
||||
public string Email { get; set; } = null!;
|
||||
[Column("userEmail")] public string Email { get; set; } = null!;
|
||||
|
||||
[Column("userLanguage")]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
|
||||
@@ -178,17 +178,14 @@ internal class MigrateTours : UnscopedMigrationBase
|
||||
[Constraint(Default = "0")]
|
||||
public bool NoConsole { get; set; }
|
||||
|
||||
[Column("userName")]
|
||||
public string UserName { get; set; } = null!;
|
||||
[Column("userName")] public string UserName { get; set; } = null!;
|
||||
|
||||
[Column("userLogin")]
|
||||
[Length(125)]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public string? Login { get; set; }
|
||||
|
||||
[Column("userPassword")]
|
||||
[Length(500)]
|
||||
public string? Password { get; set; }
|
||||
[Column("userPassword")] [Length(500)] public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
|
||||
@@ -198,8 +195,7 @@ internal class MigrateTours : UnscopedMigrationBase
|
||||
[Length(500)]
|
||||
public string? PasswordConfig { get; set; }
|
||||
|
||||
[Column("userEmail")]
|
||||
public string Email { get; set; } = null!;
|
||||
[Column("userEmail")] public string Email { get; set; } = null!;
|
||||
|
||||
[Column("userLanguage")]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_2_0
|
||||
{
|
||||
/// <summary>
|
||||
/// Migration to add a description column to the user group table.
|
||||
/// </summary>
|
||||
public class AddDescriptionToUserGroup : AsyncMigrationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddDescriptionToUserGroup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The migration context.</param>
|
||||
public AddDescriptionToUserGroup(
|
||||
IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task MigrateAsync()
|
||||
{
|
||||
if (TableExists(Constants.DatabaseSchema.Tables.UserGroup) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string ColumnName = "description";
|
||||
var hasColumn = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database)
|
||||
.Any(c =>
|
||||
c.TableName == Constants.DatabaseSchema.Tables.UserGroup &&
|
||||
c.ColumnName == ColumnName);
|
||||
|
||||
if (hasColumn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddColumn<UserGroupDto>(Constants.DatabaseSchema.Tables.UserGroup, ColumnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IDatabaseProviderOperationFactory"/> that indexes
|
||||
/// provider-specific operations by provider name and falls back to default implementations.
|
||||
/// </summary>
|
||||
internal class DatabaseProviderOperationFactory : IDatabaseProviderOperationFactory
|
||||
{
|
||||
private readonly Dictionary<string, IPropertyDataReplacerOperation> _propertyDataReplacers;
|
||||
private readonly IPropertyDataReplacerOperation _defaultPropertyDataReplacerOperation;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseProviderOperationFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyDataReplacers">The collection of provider-specific property data replacers.</param>
|
||||
public DatabaseProviderOperationFactory(IEnumerable<IPropertyDataReplacerOperation> propertyDataReplacers)
|
||||
{
|
||||
_propertyDataReplacers = propertyDataReplacers
|
||||
.Where(x => x.ProviderName is not null)
|
||||
.ToDictionary(x => x.ProviderName!, StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
_defaultPropertyDataReplacerOperation = new DefaultPropertyDataReplacerOperation();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPropertyDataReplacerOperation GetPropertyDataReplacerOperation(string providerName)
|
||||
{
|
||||
if (_propertyDataReplacers.TryGetValue(providerName, out IPropertyDataReplacerOperation? operation))
|
||||
{
|
||||
return operation;
|
||||
}
|
||||
|
||||
return _defaultPropertyDataReplacerOperation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="IPropertyDataReplacerOperation"/> as a default using database provider agnostic methods.
|
||||
/// </summary>
|
||||
internal class DefaultPropertyDataReplacerOperation : IPropertyDataReplacerOperation
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string? ProviderName => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReplacePropertyData(IUmbracoDatabase database, int versionId, IEnumerable<PropertyDataDto> propertyDataDtos)
|
||||
{
|
||||
// Replace the property data.
|
||||
// Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic
|
||||
// and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues.
|
||||
Sql<ISqlContext> propDataSql = database.SqlContext.Sql().Select("*").From<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionId).ForUpdate();
|
||||
List<PropertyDataDto>? existingPropData = database.Fetch<PropertyDataDto>(propDataSql);
|
||||
var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>();
|
||||
var existingPropDataIds = new List<int>();
|
||||
foreach (PropertyDataDto? p in existingPropData)
|
||||
{
|
||||
existingPropDataIds.Add(p.Id);
|
||||
propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p;
|
||||
}
|
||||
|
||||
var toUpdate = new List<PropertyDataDto>();
|
||||
var toInsert = new List<PropertyDataDto>();
|
||||
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
|
||||
{
|
||||
// Check if this already exists and update, else insert a new one
|
||||
if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData))
|
||||
{
|
||||
propertyDataDto.Id = propData.Id;
|
||||
toUpdate.Add(propertyDataDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
toInsert.Add(propertyDataDto);
|
||||
}
|
||||
|
||||
// track which ones have been processed
|
||||
existingPropDataIds.Remove(propertyDataDto.Id);
|
||||
}
|
||||
|
||||
if (toUpdate.Count > 0)
|
||||
{
|
||||
var updateBatch = toUpdate
|
||||
.Select(x => UpdateBatch.For(x))
|
||||
.ToList();
|
||||
database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
|
||||
}
|
||||
|
||||
if (toInsert.Count > 0)
|
||||
{
|
||||
database.InsertBulk(toInsert);
|
||||
}
|
||||
|
||||
// For any remaining that haven't been processed they need to be deleted
|
||||
if (existingPropDataIds.Count > 0)
|
||||
{
|
||||
database.Execute(database.SqlContext.Sql().Delete<PropertyDataDto>().WhereIn<PropertyDataDto>(x => x.Id, existingPropDataIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
[TableName(TableName)]
|
||||
[PrimaryKey("id")]
|
||||
[ExplicitColumns]
|
||||
internal sealed class PropertyDataDto
|
||||
public sealed class PropertyDataDto
|
||||
{
|
||||
public const string TableName = Constants.DatabaseSchema.Tables.PropertyData;
|
||||
public const int VarcharLength = 512;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
[TableName(TableName)]
|
||||
[PrimaryKey("id")]
|
||||
[ExplicitColumns]
|
||||
internal class PropertyTypeDto
|
||||
public class PropertyTypeDto
|
||||
{
|
||||
public const string TableName = Constants.DatabaseSchema.Tables.PropertyType;
|
||||
|
||||
|
||||
@@ -40,11 +40,6 @@ public class UserGroupDto
|
||||
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Column(Name = "description")]
|
||||
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Column("userGroupDefaultPermissions")]
|
||||
[Length(50)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
|
||||
@@ -30,7 +30,6 @@ internal static class UserGroupFactory
|
||||
userGroup.StartMediaId = dto.StartMediaId;
|
||||
userGroup.Permissions = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet();
|
||||
userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages;
|
||||
userGroup.Description = dto.Description;
|
||||
if (dto.UserGroup2AppDtos != null)
|
||||
{
|
||||
foreach (UserGroup2AppDto app in dto.UserGroup2AppDtos)
|
||||
@@ -85,7 +84,6 @@ internal static class UserGroupFactory
|
||||
Key = entity.Key,
|
||||
Alias = entity.Alias,
|
||||
Name = entity.Name,
|
||||
Description = entity.Description,
|
||||
UserGroup2AppDtos = new List<UserGroup2AppDto>(),
|
||||
CreateDate = entity.CreateDate,
|
||||
UpdateDate = entity.UpdateDate,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a factory for the creation of handlers for specific database operations that have been optimized for a given provider.
|
||||
/// </summary>
|
||||
public interface IDatabaseProviderOperationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an instance of an <see cref="IPropertyDataReplacerOperation"/> for the specified provider name.
|
||||
/// </summary>
|
||||
/// <param name="providerName">The name of the provider for which to obtain the property data replacer.</param>
|
||||
/// <returns>An <see cref="IPropertyDataReplacerOperation"/> instance associated with the specified provider name.</returns>
|
||||
IPropertyDataReplacerOperation GetPropertyDataReplacerOperation(string providerName);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Provides replacement of property data for content versions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Database-specific implementations can use features like Table-Valued Parameters (SQL Server)
|
||||
/// to perform the operation in a single round trip.
|
||||
/// </remarks>
|
||||
public interface IPropertyDataReplacerOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the database provider name this operation is specific to, or <c>null</c> for the default implementation.
|
||||
/// </summary>
|
||||
string? ProviderName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all property data for the specified version IDs atomically.
|
||||
/// </summary>
|
||||
/// <param name="database">The database instance.</param>
|
||||
/// <param name="versionId">The version Id.</param>
|
||||
/// <param name="propertyDataDtos">The property data to save.</param>
|
||||
/// <remarks>
|
||||
/// This method will:
|
||||
/// 1. Lock existing rows for the affected version IDs.
|
||||
/// 2. Update existing property data where keys match (versionId, propertyTypeId, languageId, segment).
|
||||
/// 3. Insert new property data.
|
||||
/// 4. Delete property data that is no longer present.
|
||||
/// </remarks>
|
||||
void ReplacePropertyData(IUmbracoDatabase database, int versionId, IEnumerable<PropertyDataDto> propertyDataDtos);
|
||||
}
|
||||
@@ -25,6 +25,5 @@ public sealed class UserGroupMapper : BaseMapper
|
||||
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon));
|
||||
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId));
|
||||
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId));
|
||||
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Description), nameof(UserGroupDto.Description));
|
||||
}
|
||||
}
|
||||
|
||||
+43
-58
@@ -51,7 +51,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
IDataTypeService dataTypeService,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
ICacheSyncService cacheSyncService,
|
||||
IDatabaseProviderOperationFactory databaseProviderOperationFactory)
|
||||
: base(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
@@ -66,6 +67,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
PropertyEditors = propertyEditors;
|
||||
_dataValueReferenceFactories = dataValueReferenceFactories;
|
||||
_eventAggregator = eventAggregator;
|
||||
DatabaseProviderOperationFactory = databaseProviderOperationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
protected ContentRepositoryBase(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches cache,
|
||||
ILogger<EntityRepositoryBase<TId, TEntity>> logger,
|
||||
ILanguageRepository languageRepository,
|
||||
IRelationRepository relationRepository,
|
||||
IRelationTypeRepository relationTypeRepository,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
: this(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
logger,
|
||||
languageRepository,
|
||||
relationRepository,
|
||||
relationTypeRepository,
|
||||
propertyEditors,
|
||||
dataValueReferenceFactories,
|
||||
dataTypeService,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
@@ -114,6 +147,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
protected PropertyEditorCollection PropertyEditors { get; }
|
||||
|
||||
protected IDatabaseProviderOperationFactory DatabaseProviderOperationFactory { get; }
|
||||
|
||||
#region Versions
|
||||
|
||||
// gets a specific version
|
||||
@@ -1130,68 +1165,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to atomically replace the property values for the entity version specified
|
||||
/// Used to atomically replace the property values for the entity version specified.
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="versionId"></param>
|
||||
/// <param name="publishedVersionId"></param>
|
||||
/// <param name="edited"></param>
|
||||
/// <param name="editedCultures"></param>
|
||||
|
||||
protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, out HashSet<string>? editedCultures)
|
||||
{
|
||||
// Replace the property data.
|
||||
// Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic
|
||||
// and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues.
|
||||
Sql<ISqlContext> propDataSql = SqlContext.Sql().Select("*").From<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionId).ForUpdate();
|
||||
List<PropertyDataDto>? existingPropData = Database.Fetch<PropertyDataDto>(propDataSql);
|
||||
var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>();
|
||||
var existingPropDataIds = new List<int>();
|
||||
foreach (PropertyDataDto? p in existingPropData)
|
||||
{
|
||||
existingPropDataIds.Add(p.Id);
|
||||
propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p;
|
||||
}
|
||||
|
||||
IEnumerable<PropertyDataDto> propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
|
||||
|
||||
var toUpdate = new List<PropertyDataDto>();
|
||||
var toInsert = new List<PropertyDataDto>();
|
||||
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
|
||||
{
|
||||
// Check if this already exists and update, else insert a new one
|
||||
if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData))
|
||||
{
|
||||
propertyDataDto.Id = propData.Id;
|
||||
toUpdate.Add(propertyDataDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
toInsert.Add(propertyDataDto);
|
||||
}
|
||||
|
||||
// track which ones have been processed
|
||||
existingPropDataIds.Remove(propertyDataDto.Id);
|
||||
}
|
||||
|
||||
if (toUpdate.Count > 0)
|
||||
{
|
||||
var updateBatch = toUpdate
|
||||
.Select(x => UpdateBatch.For(x))
|
||||
.ToList();
|
||||
Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
|
||||
}
|
||||
|
||||
if (toInsert.Count > 0)
|
||||
{
|
||||
Database.InsertBulk(toInsert);
|
||||
}
|
||||
|
||||
// For any remaining that haven't been processed they need to be deleted
|
||||
if (existingPropDataIds.Count > 0)
|
||||
{
|
||||
Database.Execute(SqlContext.Sql().Delete<PropertyDataDto>().WhereIn<PropertyDataDto>(x => x.Id, existingPropDataIds));
|
||||
}
|
||||
// Here we'll optimize the operation for SQL Server to help with database latency issues. On large content items with many properties,
|
||||
// replacing property data can be slow due to the multiple round-trips to the database. For SQL Server we have an optimized operation
|
||||
// that uses a TVP (table-valued parameter) to perform the operation in a single round-trip via a stored procedure.
|
||||
var providerName = Database.DatabaseType.GetProviderName();
|
||||
IPropertyDataReplacerOperation propertyDataReplacer = DatabaseProviderOperationFactory.GetPropertyDataReplacerOperation(providerName);
|
||||
propertyDataReplacer.ReplacePropertyData(Database, versionId, propertyDataDtos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -37,7 +37,10 @@ internal sealed class DocumentBlueprintRepository : DocumentRepository, IDocumen
|
||||
IDataTypeService dataTypeService,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator)
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService,
|
||||
IDatabaseProviderOperationFactory databaseProviderOperationFactory)
|
||||
: base(
|
||||
scopeAccessor,
|
||||
appCaches,
|
||||
@@ -53,7 +56,10 @@ internal sealed class DocumentBlueprintRepository : DocumentRepository, IDocumen
|
||||
dataValueReferenceFactories,
|
||||
dataTypeService,
|
||||
serializer,
|
||||
eventAggregator)
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService,
|
||||
databaseProviderOperationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+45
-2
@@ -59,7 +59,8 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
ICacheSyncService cacheSyncService,
|
||||
IDatabaseProviderOperationFactory databaseProviderOperationFactory)
|
||||
: base(
|
||||
scopeAccessor,
|
||||
appCaches,
|
||||
@@ -72,7 +73,8 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
|
||||
dataTypeService,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService)
|
||||
cacheSyncService,
|
||||
databaseProviderOperationFactory)
|
||||
{
|
||||
_contentTypeRepository =
|
||||
contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository));
|
||||
@@ -93,6 +95,47 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
|
||||
cacheSyncService);
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public DocumentRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches appCaches,
|
||||
ILogger<DocumentRepository> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
IContentTypeRepository contentTypeRepository,
|
||||
ITemplateRepository templateRepository,
|
||||
ITagRepository tagRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IRelationRepository relationRepository,
|
||||
IRelationTypeRepository relationTypeRepository,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
: this(
|
||||
scopeAccessor,
|
||||
appCaches,
|
||||
logger,
|
||||
loggerFactory,
|
||||
contentTypeRepository,
|
||||
templateRepository,
|
||||
tagRepository,
|
||||
languageRepository,
|
||||
relationRepository,
|
||||
relationTypeRepository,
|
||||
propertyEditors,
|
||||
dataValueReferenceFactories,
|
||||
dataTypeService,
|
||||
serializer,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DocumentRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
|
||||
+3
-7
@@ -27,13 +27,8 @@ public abstract class EntityRepositoryBase<TId, TEntity> : RepositoryBase, IRead
|
||||
private IQuery<TEntity>? _hasIdQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EntityRepositoryBase{TId, TEntity}" /> class.
|
||||
/// Initializes a new instance of the <see cref="EntityRepositoryBase{TId, TEntity}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="scopeAccessor">The scope accessor.</param>
|
||||
/// <param name="appCaches">The application caches.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="repositoryCacheVersionService">The repository cache version service.</param>
|
||||
/// <param name="cacheSyncService">The cache synchronization service.</param>
|
||||
protected EntityRepositoryBase(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches appCaches,
|
||||
@@ -233,7 +228,8 @@ public abstract class EntityRepositoryBase<TId, TEntity> : RepositoryBase, IRead
|
||||
ScopeAccessor,
|
||||
DefaultOptions,
|
||||
RepositoryCacheVersionService,
|
||||
CacheSyncService);
|
||||
CacheSyncService
|
||||
);
|
||||
|
||||
protected abstract TEntity? PerformGet(TId? id);
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
ICacheSyncService cacheSyncService,
|
||||
IDatabaseProviderOperationFactory databaseProviderOperationFactory)
|
||||
: base(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
@@ -65,7 +66,8 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
dataTypeService,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService)
|
||||
cacheSyncService,
|
||||
databaseProviderOperationFactory)
|
||||
{
|
||||
_cache = cache;
|
||||
_mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository));
|
||||
@@ -81,6 +83,47 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
cacheSyncService);
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MediaRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches cache,
|
||||
ILogger<MediaRepository> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
IMediaTypeRepository mediaTypeRepository,
|
||||
ITagRepository tagRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IRelationRepository relationRepository,
|
||||
IRelationTypeRepository relationTypeRepository,
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
: this(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
logger,
|
||||
loggerFactory,
|
||||
mediaTypeRepository,
|
||||
tagRepository,
|
||||
languageRepository,
|
||||
relationRepository,
|
||||
relationTypeRepository,
|
||||
propertyEditorCollection,
|
||||
mediaUrlGenerators,
|
||||
dataValueReferenceFactories,
|
||||
dataTypeService,
|
||||
serializer,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public MediaRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
@@ -98,7 +141,8 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
IDataTypeService dataTypeService,
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator)
|
||||
: this(scopeAccessor,
|
||||
: this(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
logger,
|
||||
loggerFactory,
|
||||
@@ -114,8 +158,8 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
serializer,
|
||||
eventAggregator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRepositoryCacheVersionService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheSyncService>()
|
||||
)
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheSyncService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
IEventAggregator eventAggregator,
|
||||
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
ICacheSyncService cacheSyncService,
|
||||
IDatabaseProviderOperationFactory databaseProviderOperationFactory)
|
||||
: base(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
@@ -73,7 +74,8 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
dataTypeService,
|
||||
eventAggregator,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService)
|
||||
cacheSyncService,
|
||||
databaseProviderOperationFactory)
|
||||
{
|
||||
_memberTypeRepository =
|
||||
memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository));
|
||||
@@ -86,6 +88,49 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
new MemberRepositoryUsernameCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions, repositoryCacheVersionService, cacheSyncService);
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MemberRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches cache,
|
||||
ILogger<MemberRepository> logger,
|
||||
IMemberTypeRepository memberTypeRepository,
|
||||
IMemberGroupRepository memberGroupRepository,
|
||||
ITagRepository tagRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IRelationRepository relationRepository,
|
||||
IRelationTypeRepository relationTypeRepository,
|
||||
IPasswordHasher passwordHasher,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
IJsonSerializer serializer,
|
||||
IEventAggregator eventAggregator,
|
||||
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration,
|
||||
IRepositoryCacheVersionService repositoryCacheVersionService,
|
||||
ICacheSyncService cacheSyncService)
|
||||
: this(
|
||||
scopeAccessor,
|
||||
cache,
|
||||
logger,
|
||||
memberTypeRepository,
|
||||
memberGroupRepository,
|
||||
tagRepository,
|
||||
languageRepository,
|
||||
relationRepository,
|
||||
relationTypeRepository,
|
||||
passwordHasher,
|
||||
propertyEditors,
|
||||
dataValueReferenceFactories,
|
||||
dataTypeService,
|
||||
serializer,
|
||||
eventAggregator,
|
||||
passwordConfiguration,
|
||||
repositoryCacheVersionService,
|
||||
cacheSyncService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public MemberRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
@@ -122,7 +167,8 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
eventAggregator,
|
||||
passwordConfiguration,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRepositoryCacheVersionService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheSyncService>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheSyncService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDatabaseProviderOperationFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -432,8 +432,7 @@ internal sealed class RelationRepository : EntityRepositoryBase<int, IRelation>,
|
||||
|
||||
protected override IEnumerable<string> GetDeleteClauses()
|
||||
{
|
||||
var list = new List<string>
|
||||
{
|
||||
var list = new List<string> {
|
||||
$"DELETE FROM {QuoteTableName(Constants.DatabaseSchema.Tables.Relation)} WHERE id = @id"
|
||||
};
|
||||
return list;
|
||||
|
||||
+10
-20
@@ -447,35 +447,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
private sealed class UnionHelperDto
|
||||
{
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
[Column("id")] public int Id { get; set; }
|
||||
|
||||
[Column("otherId")]
|
||||
public int OtherId { get; set; }
|
||||
[Column("otherId")] public int OtherId { get; set; }
|
||||
|
||||
[Column("key")]
|
||||
public Guid Key { get; set; }
|
||||
[Column("key")] public Guid Key { get; set; }
|
||||
|
||||
[Column("trashed")]
|
||||
public bool Trashed { get; set; }
|
||||
[Column("trashed")] public bool Trashed { get; set; }
|
||||
|
||||
[Column("nodeObjectType")]
|
||||
public Guid NodeObjectType { get; set; }
|
||||
[Column("nodeObjectType")] public Guid NodeObjectType { get; set; }
|
||||
|
||||
[Column("otherKey")]
|
||||
public Guid OtherKey { get; set; }
|
||||
[Column("otherKey")] public Guid OtherKey { get; set; }
|
||||
|
||||
[Column("alias")]
|
||||
public string? Alias { get; set; }
|
||||
[Column("alias")] public string? Alias { get; set; }
|
||||
|
||||
[Column("name")]
|
||||
public string? Name { get; set; }
|
||||
[Column("name")] public string? Name { get; set; }
|
||||
|
||||
[Column("isDependency")]
|
||||
public bool IsDependency { get; set; }
|
||||
[Column("isDependency")] public bool IsDependency { get; set; }
|
||||
|
||||
[Column("dual")]
|
||||
public bool Dual { get; set; }
|
||||
[Column("dual")] public bool Dual { get; set; }
|
||||
}
|
||||
|
||||
private RelationItem MapDtoToEntity(RelationItemDto dto) =>
|
||||
|
||||
@@ -481,7 +481,6 @@ public class UserGroupRepository : EntityRepositoryBase<int, IUserGroup>, IUserG
|
||||
x => x.UpdateDate,
|
||||
x => x.Alias,
|
||||
x => x.Name,
|
||||
x => x.Description,
|
||||
x => x.HasAccessToAllLanguages,
|
||||
x => x.Key,
|
||||
x => x.DefaultPermissions)
|
||||
|
||||
@@ -133,19 +133,7 @@ public class ImageCropperPropertyEditor : DataEditor,
|
||||
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaDeletedNotification notification)
|
||||
{
|
||||
if (_contentSettings.EnableMediaRecycleBinProtection)
|
||||
{
|
||||
RecycleBinMediaProtectionHelper.DeleteContainedFilesWithProtection(
|
||||
notification.DeletedEntities,
|
||||
ContainedFilePaths,
|
||||
_mediaFileManager);
|
||||
return;
|
||||
}
|
||||
|
||||
DeleteContainedFiles(notification.DeletedEntities);
|
||||
}
|
||||
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaSavingNotification notification)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
@@ -346,21 +348,18 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
|
||||
|
||||
Guid[] allowedTypes = configuration?.Filter?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(Guid.Parse).ToArray() ?? [];
|
||||
|
||||
// Don't need to validate if there's no filter.
|
||||
if (entityReferences is null || allowedTypes.Length == 0)
|
||||
// We can't validate if there is no object type, and we don't need to if there's no filter.
|
||||
if (entityReferences is null || allowedTypes.Length == 0 || configuration?.TreeSource?.ObjectType is null)
|
||||
{
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
// If no object type is specified, it's considered as document.
|
||||
var objectType = configuration?.TreeSource?.ObjectType ?? DocumentObjectType;
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
|
||||
Guid?[] uniqueContentTypeKeys = entityReferences
|
||||
.Select(x => x.Unique)
|
||||
.Distinct()
|
||||
.Select(x => GetContent(objectType, x))
|
||||
.Select(x => GetContent(configuration.TreeSource.ObjectType, x))
|
||||
.Select(x => x?.ContentType.Key)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
+2
-14
@@ -56,19 +56,7 @@ internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadNo
|
||||
public void Handle(ContentDeletedBlueprintNotification notification) => DeleteContainedFiles(notification.DeletedBlueprints);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaDeletedNotification notification)
|
||||
{
|
||||
if (_contentSettings.EnableMediaRecycleBinProtection)
|
||||
{
|
||||
RecycleBinMediaProtectionHelper.DeleteContainedFilesWithProtection(
|
||||
notification.DeletedEntities,
|
||||
ContainedFilePaths,
|
||||
MediaFileManager);
|
||||
return;
|
||||
}
|
||||
|
||||
DeleteContainedFiles(notification.DeletedEntities);
|
||||
}
|
||||
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaMovedToRecycleBinNotification notification)
|
||||
@@ -127,7 +115,7 @@ internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadNo
|
||||
private void RemoveSuffixFromContainedFiles(IEnumerable<IMedia> restoredMedia)
|
||||
{
|
||||
IEnumerable<string> filePathsToRename = ContainedFilePaths(restoredMedia);
|
||||
RecycleBinMediaProtectionHelper.RemoveSuffixFromContainedFiles(filePathsToRename, MediaFileManager);
|
||||
MediaFileManager.RemoveSuffixFromMediaFiles(filePathsToRename, Constants.Conventions.Media.TrashedMediaSuffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
-33
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
@@ -28,36 +27,4 @@ internal static class RecycleBinMediaProtectionHelper
|
||||
.Select(x => Path.ChangeExtension(x, Constants.Conventions.Media.TrashedMediaSuffix + Path.GetExtension(x)));
|
||||
mediaFileManager.RemoveSuffixFromMediaFiles(filePathsToRename, Constants.Conventions.Media.TrashedMediaSuffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all media files, accounting for recycle bin protection (trashed files have .deleted suffix on disk).
|
||||
/// </summary>
|
||||
/// <param name="deletedMedia">Deleted media entities.</param>
|
||||
/// <param name="containedFilePaths">Function to extract file paths from media entities.</param>
|
||||
/// <param name="mediaFileManager">The media file manager.</param>
|
||||
public static void DeleteContainedFilesWithProtection(
|
||||
IEnumerable<IMedia> deletedMedia,
|
||||
Func<IEnumerable<IMedia>, IEnumerable<string>> containedFilePaths,
|
||||
MediaFileManager mediaFileManager)
|
||||
{
|
||||
// Typically all deleted media will have Trashed == true since they come from the recycle bin.
|
||||
// However, media can be force-deleted programmatically bypassing the recycle bin, in which
|
||||
// case the files won't have the .deleted suffix on disk. We handle both cases here.
|
||||
var trashedMedia = deletedMedia.Where(m => m.Trashed).ToList();
|
||||
var nonTrashedMedia = deletedMedia.Where(m => !m.Trashed).ToList();
|
||||
|
||||
// Delete trashed media files (with .deleted suffix on disk).
|
||||
if (trashedMedia.Count > 0)
|
||||
{
|
||||
IEnumerable<string> trashedPaths = containedFilePaths(trashedMedia)
|
||||
.Select(x => Path.ChangeExtension(x, Constants.Conventions.Media.TrashedMediaSuffix + Path.GetExtension(x)));
|
||||
mediaFileManager.DeleteMediaFiles(trashedPaths);
|
||||
}
|
||||
|
||||
// Delete non-trashed media files (original paths).
|
||||
if (nonTrashedMedia.Count > 0)
|
||||
{
|
||||
mediaFileManager.DeleteMediaFiles(containedFilePaths(nonTrashedMedia));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-23
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using HeyRed.MarkdownSharp;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -16,22 +15,11 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver
|
||||
{
|
||||
private readonly HtmlLocalLinkParser _localLinkParser;
|
||||
private readonly HtmlUrlParser _urlParser;
|
||||
private readonly IMarkdownToHtmlConverter _markdownToHtmlConverter;
|
||||
|
||||
public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser, IMarkdownToHtmlConverter markdownToHtmlConverter)
|
||||
public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser)
|
||||
{
|
||||
_localLinkParser = localLinkParser;
|
||||
_urlParser = urlParser;
|
||||
_markdownToHtmlConverter = markdownToHtmlConverter;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser)
|
||||
: this(
|
||||
localLinkParser,
|
||||
urlParser,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMarkdownToHtmlConverter>())
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
@@ -61,15 +49,10 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
{
|
||||
// Convert markup to HTML for frontend rendering.
|
||||
// Source should come from ConvertSource and be a string (or null) already.
|
||||
if (inter is null)
|
||||
{
|
||||
return new HtmlEncodedString(string.Empty);
|
||||
}
|
||||
|
||||
var htmlString = _markdownToHtmlConverter.ToHtml((string)inter);
|
||||
return new HtmlEncodedString(htmlString);
|
||||
// convert markup to HTML for frontend rendering.
|
||||
// source should come from ConvertSource and be a string (or null) already
|
||||
var mark = new Markdown();
|
||||
return new HtmlEncodedString(inter == null ? string.Empty : mark.Transform((string)inter));
|
||||
}
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element;
|
||||
|
||||
@@ -15,24 +15,12 @@ using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCo
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IMainDomLock"/> using SQL database for distributed lock coordination.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation uses a database row in the KeyValue table to coordinate MainDom
|
||||
/// acquisition across multiple application instances in a load-balanced environment.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The lock is acquired by inserting or updating a row with a unique identifier. Other
|
||||
/// instances poll the database to detect when the current MainDom releases the lock.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class SqlMainDomLock : IMainDomLock
|
||||
{
|
||||
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||
private readonly IUmbracoDatabase? _db;
|
||||
private readonly UmbracoDatabaseFactory _dbFactory;
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
private readonly Lock _locker = new();
|
||||
@@ -43,16 +31,6 @@ public class SqlMainDomLock : IMainDomLock
|
||||
private bool _hasTable;
|
||||
private bool _mainDomChanging;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlMainDomLock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="globalSettings">The global settings.</param>
|
||||
/// <param name="connectionStrings">The connection strings configuration.</param>
|
||||
/// <param name="dbProviderFactoryCreator">The database provider factory creator.</param>
|
||||
/// <param name="mainDomKeyGenerator">The MainDom key generator.</param>
|
||||
/// <param name="databaseSchemaCreatorFactory">The database schema creator factory.</param>
|
||||
/// <param name="npocoMappers">The NPoco mapper collection.</param>
|
||||
public SqlMainDomLock(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
@@ -92,7 +70,6 @@ public class SqlMainDomLock : IMainDomLock
|
||||
/// </remarks>
|
||||
private string MainDomKey { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
{
|
||||
if (!_dbFactory.Configured)
|
||||
@@ -156,7 +133,6 @@ public class SqlMainDomLock : IMainDomLock
|
||||
return await WaitForExistingAsync(tempId, millisecondsTimeout);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ListenAsync()
|
||||
{
|
||||
if (_errorDuringAcquiring)
|
||||
@@ -420,13 +396,7 @@ public class SqlMainDomLock : IMainDomLock
|
||||
|
||||
private bool _disposedValue; // To detect redundant calls
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// <c>true</c> to release both managed and unmanaged resources;
|
||||
/// <c>false</c> to release only unmanaged resources.
|
||||
/// </param>
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
@@ -494,7 +464,7 @@ public class SqlMainDomLock : IMainDomLock
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// This code added to correctly implement the disposable pattern.
|
||||
public void Dispose() =>
|
||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
||||
Dispose(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -9,13 +9,8 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IUserIdKeyResolver"/> for resolving user keys to user IDs and vice versa without retrieving full user details.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It's okay that we never clear this, since you can never change a user's key/id
|
||||
/// and it'll be caught by the services if it doesn't exist.
|
||||
/// </remarks>
|
||||
// It's okay that we never clear this, since you can never change a user's key/id
|
||||
// and it'll be caught by the services if it doesn't exist.
|
||||
internal sealed class UserIdKeyResolver : IUserIdKeyResolver
|
||||
{
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
@@ -24,9 +19,6 @@ internal sealed class UserIdKeyResolver : IUserIdKeyResolver
|
||||
private readonly SemaphoreSlim _keytToIdLock = new(1, 1);
|
||||
private readonly SemaphoreSlim _idToKeyLock = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserIdKeyResolver"/> class.
|
||||
/// </summary>
|
||||
public UserIdKeyResolver(IScopeProvider scopeProvider) => _scopeProvider = scopeProvider;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -36,14 +28,6 @@ internal sealed class UserIdKeyResolver : IUserIdKeyResolver
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<int>> TryGetAsync(Guid key)
|
||||
{
|
||||
// The super-user Id and key is known, so we don't need a look-up here.
|
||||
if (key == Constants.Security.SuperUserKey)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return Attempt.Succeed(Constants.Security.SuperUserId);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
if (_keyToId.TryGetValue(key, out int id))
|
||||
{
|
||||
return Attempt.Succeed(id);
|
||||
@@ -92,14 +76,6 @@ internal sealed class UserIdKeyResolver : IUserIdKeyResolver
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<Guid>> TryGetAsync(int id)
|
||||
{
|
||||
// The super-user Id and key is known, so we don't need a look-up here.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (id is Constants.Security.SuperUserId)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
return Attempt.Succeed(Constants.Security.SuperUserKey);
|
||||
}
|
||||
|
||||
if (_idToKey.TryGetValue(id, out Guid key))
|
||||
{
|
||||
return Attempt.Succeed(key);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using HeyRed.MarkdownSharp;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Strings;
|
||||
|
||||
// TODO (V19): Remove this class along with the HeyRed.MarkdownSharp library entirely (remove reference from Directory.props and .csproj, and remove from NOTICES.txt).
|
||||
|
||||
/// <summary>
|
||||
/// Implements a service that converts Markdown-formatted text to HTML using the HeyRed.MarkdownSharp library.
|
||||
/// </summary>
|
||||
[Obsolete("Uses the deprecated HeyRed.MarkdownSharp library which will continue to be provided for the lifetime of Umbraco 17 as the default implementation of IMarkdownToHtmlConverter. The default will be changed to MarkdigMarkdownToHtmlConverter for Umbraco 18. Scheduled for removal along with the HeyRed.MarkdownSharp library in Umbraco 19.")]
|
||||
public class HeyRedMarkdownToHtmlConverter : IMarkdownToHtmlConverter
|
||||
{
|
||||
private static readonly Markdown _markdownConverter = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ToHtml(string markdown) => _markdownConverter.Transform(markdown);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Markdig;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Strings;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a service that converts Markdown-formatted text to HTML using the Markdig library.
|
||||
/// </summary>
|
||||
public class MarkdigMarkdownToHtmlConverter : IMarkdownToHtmlConverter
|
||||
{
|
||||
private static readonly MarkdownPipeline _markdownPipeline = new MarkdownPipelineBuilder().Build();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ToHtml(string markdown) => Markdown.ToHtml(markdown, _markdownPipeline);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<PackageId>Umbraco.Cms.Infrastructure</PackageId>
|
||||
<Title>Umbraco CMS - Infrastructure</Title>
|
||||
@@ -39,7 +39,6 @@
|
||||
<PackageReference Include="Examine.Core" />
|
||||
<PackageReference Include="HtmlAgilityPack" />
|
||||
<PackageReference Include="MailKit" />
|
||||
<PackageReference Include="Markdig" />
|
||||
<PackageReference Include="Markdown" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -21,9 +21,10 @@ internal sealed class CacheNodeFactory : ICacheNodeFactory
|
||||
|
||||
ContentData contentData = GetContentData(
|
||||
content,
|
||||
GetPublishedValue(content, preview),
|
||||
GetTemplateId(content, preview),
|
||||
content.PublishCultureInfos!.Values.Select(x => x.Culture).ToHashSet());
|
||||
GetPublishedValue(content, preview),
|
||||
GetTemplateId(content, preview),
|
||||
content.PublishCultureInfos!.Values.Select(x=>x.Culture).ToHashSet()
|
||||
);
|
||||
return new ContentCacheNode
|
||||
{
|
||||
Id = content.Id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
@@ -57,7 +56,7 @@ public sealed class ModelBindingExceptionAttribute : TypeFilterAttribute
|
||||
&& (filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException)
|
||||
&& IsMessageAboutTheSameModelType(filterContext.Exception.Message))
|
||||
{
|
||||
filterContext.HttpContext.Response.Headers.Append(HttpResponseHeader.RetryAfter.ToString(), "1");
|
||||
filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1");
|
||||
filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.GetEncodedUrl(), false);
|
||||
|
||||
filterContext.ExceptionHandled = true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Extensions;
|
||||
@@ -29,7 +28,7 @@ public class UmbracoUserTimeoutFilterAttribute : TypeFilterAttribute
|
||||
}
|
||||
|
||||
var remainingSeconds = context.HttpContext.User.GetRemainingAuthSeconds();
|
||||
context.HttpContext.Response.Headers.Append(
|
||||
context.HttpContext.Response.Headers.Add(
|
||||
"X-Umb-User-Seconds",
|
||||
remainingSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
[SA1117] params all on same line
|
||||
[SA1401] make fields private
|
||||
[SA1134] own line attributes
|
||||
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
|
||||
[CS0618]/[SYSLIB0051] adjust obsolete references
|
||||
[IDE0040]/[SA1400] access modifiers
|
||||
[SA1405] Debug assert message text
|
||||
[CS0419]/[CS1574] cref ambiguities
|
||||
[SA1649] file name match type
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),SA1117,SA1401,SA1134,CS0618,SYSLIB0051,IDE0040,SA1400,SA1405,CS0419,CS1574,SA1649</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),SA1117,SA1401,SA1134,ASP0019,CS0618,SYSLIB0051,IDE0040,SA1400,SA1405,CS0419,CS1574,SA1649</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { http, HttpResponse } = window.MockServiceWorker;
|
||||
const { rest } = window.MockServiceWorker;
|
||||
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import {
|
||||
@@ -12,10 +12,14 @@ test.describe('installer tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
http.get(umbracoPath('/server/status'), () => {
|
||||
return HttpResponse.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.INSTALL,
|
||||
});
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.INSTALL,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -58,17 +62,18 @@ test.describe('installer tests', () => {
|
||||
test('installer fails', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
http.post(umbracoPath('/install/setup'), () => {
|
||||
return HttpResponse.json<ProblemDetails>(
|
||||
{
|
||||
rest.post(umbracoPath('/install/setup'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'validation',
|
||||
detail: 'Something went wrong',
|
||||
errors: {
|
||||
databaseName: ['The database name is required'],
|
||||
},
|
||||
},
|
||||
{ status: 400 },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
const { http, HttpResponse } = window.MockServiceWorker;
|
||||
import { expect, test } from './test.js';
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import {
|
||||
type ProblemDetails,
|
||||
RuntimeLevelModel,
|
||||
type ServerStatusResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { expect, test } from './test.js';
|
||||
|
||||
test.describe('upgrader tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
http.get(umbracoPath('/server/status'), () => {
|
||||
return HttpResponse.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.UPGRADE,
|
||||
});
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.UPGRADE,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -39,14 +43,15 @@ test.describe('upgrader tests', () => {
|
||||
test('upgrader fails and shows error', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
http.post(umbracoPath('/upgrade/authorize'), () => {
|
||||
return HttpResponse.json<ProblemDetails>(
|
||||
{
|
||||
rest.post(umbracoPath('/upgrade/authorize'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'error',
|
||||
detail: 'Something went wrong',
|
||||
},
|
||||
{ status: 400 },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
+3
-3
@@ -13736,9 +13736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
|
||||
@@ -319,7 +319,6 @@ export class UmbAppElement extends UmbLitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
overflow: hidden;
|
||||
min-width: 920px;
|
||||
}
|
||||
|
||||
:host,
|
||||
|
||||
@@ -238,7 +238,6 @@ export default {
|
||||
},
|
||||
content: {
|
||||
isPublished: 'Is Published',
|
||||
unpublishedChanges: 'Unpublished changes',
|
||||
about: 'About this page',
|
||||
alias: 'Alias',
|
||||
alternativeTextHelp: '(how would you describe the picture over the phone)',
|
||||
@@ -276,7 +275,6 @@ export default {
|
||||
getUrlException: 'Could not get the URL',
|
||||
routeError: 'This document is published but its URL would collide with content %0%',
|
||||
routeErrorCannotRoute: 'This document is published but its URL cannot be routed',
|
||||
protected: 'Protected',
|
||||
publish: 'Publish',
|
||||
published: 'Published',
|
||||
publishedPendingChanges: 'Published (pending changes)',
|
||||
@@ -289,7 +287,6 @@ export default {
|
||||
releaseDate: 'Publish at',
|
||||
unpublishDate: 'Unpublish at',
|
||||
removeDate: 'Clear date',
|
||||
scheduledPublishing: 'Scheduled publishing',
|
||||
setDate: 'Set date',
|
||||
sortDone: 'Sort order is updated',
|
||||
sortHelp:
|
||||
|
||||
@@ -174,13 +174,11 @@ export default {
|
||||
parentNotPublishedAnomaly: 'Ups: este documento está publicado pero no está en la caché (error interno)',
|
||||
getUrlException: 'No se pudo obtener la URL',
|
||||
routeError: 'Este documento está publicado pero tu URL colisionará con contenido %0%',
|
||||
protected: 'Protegido',
|
||||
publish: 'Publicar',
|
||||
publishStatus: 'Estado de la Publicación',
|
||||
releaseDate: 'Publicar el',
|
||||
unpublishDate: 'Retirar publicación el',
|
||||
removeDate: 'Fecha de Eliminación',
|
||||
scheduledPublishing: 'Publicación programada',
|
||||
sortDone: 'El Orden esta actualizado',
|
||||
sortHelp:
|
||||
'Para organizar los nodos, simplemente arrastra los nodos o realice un clic en uno de los encabezados de columna. Puedes seleccionar múltiple nodos manteniendo presionados "Shift" o "Control" mientras seleccionas',
|
||||
@@ -188,7 +186,6 @@ export default {
|
||||
titleOptional: 'Título (opcional)',
|
||||
altTextOptional: 'Texto alternativo (opcional)',
|
||||
type: 'Tipo',
|
||||
unpublishedChanges: 'Cambios no publicados',
|
||||
unpublish: 'Ocultar',
|
||||
updateDate: 'Última actualización',
|
||||
updateDateDesc: 'Fecha/hora este documento fue modificado',
|
||||
|
||||
@@ -27,7 +27,6 @@ export const data: Array<UmbMockUserGroupModel> = [
|
||||
id: 'user-group-administrators-id',
|
||||
name: 'Administrators',
|
||||
alias: 'admin',
|
||||
description: 'Administrators have full access to all settings and features within the CMS.',
|
||||
icon: 'icon-medal',
|
||||
fallbackPermissions: [
|
||||
UMB_USER_PERMISSION_DOCUMENT_READ,
|
||||
@@ -92,8 +91,6 @@ export const data: Array<UmbMockUserGroupModel> = [
|
||||
id: 'user-group-editors-id',
|
||||
name: 'Editors',
|
||||
alias: 'editors',
|
||||
description:
|
||||
'The Editors group is responsible for creating, updating, and managing Content and Media within the platform. While they do not have access to system-level areas such as Settings, Users, or Packages, they play a crucial role in maintaining the website’s daily content operations. Editors start from the Media root node when handling media files, ensuring they can upload, modify, and organize assets relevant to their work.',
|
||||
icon: 'icon-tools',
|
||||
documentStartNode: { id: 'all-property-editors-document-id' },
|
||||
fallbackPermissions: [
|
||||
|
||||
@@ -2,10 +2,9 @@ const { http, HttpResponse } = window.MockServiceWorker;
|
||||
import { umbDataTypeMockDb } from '../../data/data-type/data-type.db.js';
|
||||
import { UMB_SLUG } from './slug.js';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import type { CreateFolderRequestModel, UpdateFolderResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export const folderHandlers = [
|
||||
http.post<object, CreateFolderRequestModel>(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
http.post(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400, statusText: 'no body found' });
|
||||
|
||||
@@ -27,17 +26,14 @@ export const folderHandlers = [
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.put<{ id: string }, UpdateFolderResponseModel>(
|
||||
umbracoPath(`${UMB_SLUG}/folder/:id`),
|
||||
async ({ request, params }) => {
|
||||
const id = params.id;
|
||||
if (!id) return new HttpResponse(null, { status: 400, statusText: 'no id found' });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400, statusText: 'no body found' });
|
||||
umbDataTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
},
|
||||
),
|
||||
http.put(umbracoPath(`${UMB_SLUG}/folder/:id`), async ({ request, params }) => {
|
||||
const id = params.id as string;
|
||||
if (!id) return new HttpResponse(null, { status: 400, statusText: 'no id found' });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400, statusText: 'no body found' });
|
||||
umbDataTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
}),
|
||||
|
||||
http.delete(umbracoPath(`${UMB_SLUG}/folder/:id`), ({ params }) => {
|
||||
const id = params.id as string;
|
||||
|
||||
@@ -2,10 +2,9 @@ const { http, HttpResponse } = window.MockServiceWorker;
|
||||
import { umbDocumentTypeMockDb } from '../../data/document-type/document-type.db.js';
|
||||
import { UMB_SLUG } from './slug.js';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import type { CreateFolderRequestModel, UpdateFolderResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export const folderHandlers = [
|
||||
http.post<object, CreateFolderRequestModel>(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
http.post(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
|
||||
@@ -27,17 +26,14 @@ export const folderHandlers = [
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.put<{ id: string }, UpdateFolderResponseModel>(
|
||||
umbracoPath(`${UMB_SLUG}/folder/:id`),
|
||||
async ({ request, params }) => {
|
||||
const id = params.id;
|
||||
if (!id) return new HttpResponse(null, { status: 400 });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
umbDocumentTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
},
|
||||
),
|
||||
http.put(umbracoPath(`${UMB_SLUG}/folder/:id`), async ({ request, params }) => {
|
||||
const id = params.id as string;
|
||||
if (!id) return new HttpResponse(null, { status: 400 });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
umbDocumentTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
}),
|
||||
|
||||
http.delete(umbracoPath(`${UMB_SLUG}/folder/:id`), ({ params }) => {
|
||||
const id = params.id as string;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { umbDocumentMockDb } from '../data/document/document.db.js';
|
||||
import type { DynamicRootRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
const { http, HttpResponse } = window.MockServiceWorker;
|
||||
|
||||
export const handlers = [
|
||||
http.post(umbracoPath('/dynamic-root/query'), async () => {
|
||||
http.post<DynamicRootRequestModel>(umbracoPath('/dynamic-root/query'), async () => {
|
||||
const response = umbDocumentMockDb.tree
|
||||
.getRoot()
|
||||
.items.map((item) => item.id)
|
||||
|
||||
@@ -48,8 +48,8 @@ export const handlers = [
|
||||
}
|
||||
}),
|
||||
|
||||
http.post<object, HealthCheckActionRequestModel>(umbracoPath('/health-check/execute-action'), async ({ request }) => {
|
||||
const body = await request.json();
|
||||
http.post<HealthCheckActionRequestModel>(umbracoPath('/health-check/execute-action'), async ({ request }) => {
|
||||
const body = await request.json<HealthCheckActionRequestModel>();
|
||||
const healthCheckId = body.healthCheck.id;
|
||||
// Find the health check based on the healthCheckId from the healthGroups[].checks
|
||||
const healthCheck = healthGroups.flatMap((group) => group.checks).find((check) => check?.id === healthCheckId);
|
||||
|
||||
@@ -77,8 +77,8 @@ export const handlers = [
|
||||
});
|
||||
}),
|
||||
|
||||
http.post<object, DatabaseInstallRequestModel>(umbracoPath('/install/validate-database'), async ({ request }) => {
|
||||
const body = await request.json();
|
||||
http.post(umbracoPath('/install/validate-database'), async ({ request }) => {
|
||||
const body = await request.json<DatabaseInstallRequestModel>();
|
||||
|
||||
if (body.name === 'validate') {
|
||||
return HttpResponse.json<ProblemDetails>(
|
||||
@@ -94,8 +94,8 @@ export const handlers = [
|
||||
return new HttpResponse(null, { status: 201 });
|
||||
}),
|
||||
|
||||
http.post<object, InstallRequestModel>(umbracoPath('/install/setup'), async ({ request }) => {
|
||||
const body = await request.json();
|
||||
http.post(umbracoPath('/install/setup'), async ({ request }) => {
|
||||
const body = await request.json<InstallRequestModel>();
|
||||
|
||||
if (body.database?.name === 'fail') {
|
||||
// Note: ctx.delay() is not directly supported in v2, needs to be implemented differently if delay is needed
|
||||
|
||||
@@ -2,10 +2,9 @@ const { http, HttpResponse } = window.MockServiceWorker;
|
||||
import { umbMediaTypeMockDb } from '../../data/media-type/media-type.db.js';
|
||||
import { UMB_SLUG } from './slug.js';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import type { CreateFolderRequestModel, UpdateFolderResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
export const folderHandlers = [
|
||||
http.post<object, CreateFolderRequestModel>(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
http.post(umbracoPath(`${UMB_SLUG}/folder`), async ({ request }) => {
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
|
||||
@@ -27,17 +26,14 @@ export const folderHandlers = [
|
||||
return HttpResponse.json(response);
|
||||
}),
|
||||
|
||||
http.put<{ id: string }, UpdateFolderResponseModel>(
|
||||
umbracoPath(`${UMB_SLUG}/folder/:id`),
|
||||
async ({ request, params }) => {
|
||||
const id = params.id;
|
||||
if (!id) return new HttpResponse(null, { status: 400 });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
umbMediaTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
},
|
||||
),
|
||||
http.put(umbracoPath(`${UMB_SLUG}/folder/:id`), async ({ request, params }) => {
|
||||
const id = params.id as string;
|
||||
if (!id) return new HttpResponse(null, { status: 400 });
|
||||
const requestBody = await request.json();
|
||||
if (!requestBody) return new HttpResponse(null, { status: 400 });
|
||||
umbMediaTypeMockDb.folder.update(id, requestBody);
|
||||
return new HttpResponse(null, { status: 200 });
|
||||
}),
|
||||
|
||||
http.delete(umbracoPath(`${UMB_SLUG}/folder/:id`), ({ params }) => {
|
||||
const id = params.id as string;
|
||||
|
||||
@@ -51,9 +51,9 @@ export const handlers = [
|
||||
});
|
||||
}),
|
||||
|
||||
http.post<object, CreatePackageRequestModel>(umbracoPath('/package/created'), async ({ request }) => {
|
||||
http.post(umbracoPath('/package/created'), async ({ request }) => {
|
||||
//save
|
||||
const data = await request.json();
|
||||
const data: CreatePackageRequestModel = await request.json();
|
||||
const newPackage: PackageDefinitionResponseModel = { ...data, id: UmbId.new(), packagePath: '' };
|
||||
packageArray.push(newPackage);
|
||||
return HttpResponse.json<PackageDefinitionResponseModel>(newPackage);
|
||||
@@ -68,9 +68,9 @@ export const handlers = [
|
||||
return HttpResponse.json<PackageDefinitionResponseModel>(found);
|
||||
}),
|
||||
|
||||
http.put<object, PackageDefinitionResponseModel>(umbracoPath('/package/created/:id'), async ({ request }) => {
|
||||
http.put(umbracoPath('/package/created/:id'), async ({ request }) => {
|
||||
//update
|
||||
const data = await request.json();
|
||||
const data: PackageDefinitionResponseModel = await request.json();
|
||||
if (!data.id) return;
|
||||
const index = packageArray.findIndex((x) => x.id === data.id);
|
||||
packageArray[index] = data;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user