Merge remote-tracking branch 'refs/remotes/origin/v14/dev' into v15/dev
# Conflicts: # build/nightly-E2E-test-pipelines.yml # src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs # src/Umbraco.Web.UI.Client # src/Umbraco.Web.UI.Login/package-lock.json # version.json
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
|
||||
@@ -28,7 +29,7 @@
|
||||
|
||||
<!-- Package Validation -->
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>true</GenerateCompatibilitySuppressionFile>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
|
||||
@@ -5,10 +5,6 @@ parameters:
|
||||
displayName: Run SQL Server Integration Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: sqlServerAcceptanceTests
|
||||
displayName: Run SQL Server Acceptance Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: myGetDeploy
|
||||
displayName: Deploy to MyGet
|
||||
type: boolean
|
||||
@@ -553,8 +549,6 @@ stages:
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) # Outcommented due to timeouts
|
||||
condition: eq(${{parameters.sqlServerAcceptanceTests}}, True)
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
@@ -590,7 +584,8 @@ stages:
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -708,7 +703,7 @@ stages:
|
||||
dependsOn:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E
|
||||
# - E2E
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
|
||||
@@ -4,7 +4,7 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
@@ -24,8 +24,8 @@ variables:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
parameters:
|
||||
- name: runSqlServerE2ETests
|
||||
displayName: Run the SQL Server E2E Tests
|
||||
- name: runSmokeTests
|
||||
displayName: Run the smoke tests
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -206,7 +206,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run test --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -242,7 +245,6 @@ stages:
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
condition: and(succeeded(), ${{ eq(parameters.runSqlServerE2ETests, true) }})
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
# Connection string
|
||||
@@ -279,7 +281,8 @@ stages:
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -350,7 +353,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run test --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -26,7 +26,7 @@ steps:
|
||||
useSameBranch: true
|
||||
waitForQueuedBuildsToFinish: false
|
||||
storeInEnvironmentVariable: false
|
||||
templateParameters: 'sqlServerIntegrationTests: true, sqlServerAcceptanceTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
authenticationMethod: 'OAuth Token'
|
||||
enableBuildInQueueCondition: false
|
||||
dependentOnSuccessfulBuildCondition: false
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - API Common</Title>
|
||||
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<Title>Umbraco CMS - Delivery API</Title>
|
||||
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers and remove this override -->
|
||||
<WarningsNotAsErrors>ASP0019</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
|
||||
@@ -18,18 +18,21 @@ public abstract class CreateDocumentControllerBase : DocumentControllerBase
|
||||
|
||||
protected async Task<IActionResult> HandleRequest(CreateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
|
||||
{
|
||||
IEnumerable<string> cultures = requestModel.Variants
|
||||
.Where(v => v.Culture is not null)
|
||||
.Select(v => v.Culture!);
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
|
||||
// The values are ignored in the ContentEditingService
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
// IEnumerable<string> cultures = requestModel.Variants
|
||||
// .Where(v => v.Culture is not null)
|
||||
// .Select(v => v.Culture!);
|
||||
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
// User,
|
||||
// ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures),
|
||||
// AuthorizationPolicies.ContentPermissionByResource);
|
||||
//
|
||||
// if (!authorizationResult.Succeeded)
|
||||
// {
|
||||
// return Forbidden();
|
||||
// }
|
||||
|
||||
return await authorizedHandler();
|
||||
}
|
||||
|
||||
@@ -17,18 +17,21 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase
|
||||
|
||||
protected async Task<IActionResult> HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
|
||||
{
|
||||
IEnumerable<string> cultures = requestModel.Variants
|
||||
.Where(v => v.Culture is not null)
|
||||
.Select(v => v.Culture!);
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
|
||||
// The values are ignored in the ContentEditingService
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
// IEnumerable<string> cultures = requestModel.Variants
|
||||
// .Where(v => v.Culture is not null)
|
||||
// .Select(v => v.Culture!);
|
||||
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
// User,
|
||||
// ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures),
|
||||
// AuthorizationPolicies.ContentPermissionByResource);
|
||||
//
|
||||
// if (!authorizationResult.Succeeded)
|
||||
// {
|
||||
// return Forbidden();
|
||||
// }
|
||||
|
||||
return await authorizedHandler();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Examine;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -38,7 +38,7 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken, string indexName)
|
||||
{
|
||||
if (!_examineManager.TryGetIndex(indexName, out var index))
|
||||
if (!_examineManager.TryGetIndex(indexName, out IIndex? index))
|
||||
{
|
||||
var invalidModelProblem = new ProblemDetails
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder;
|
||||
@@ -16,12 +17,13 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
|
||||
{
|
||||
private ModelsBuilderSettings _modelsBuilderSettings;
|
||||
private readonly ModelsGenerationError _mbErrors;
|
||||
private readonly ModelsGenerator _modelGenerator;
|
||||
private readonly IModelsGenerator _modelGenerator;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public BuildModelsBuilderController(
|
||||
IOptionsMonitor<ModelsBuilderSettings> modelsBuilderSettings,
|
||||
ModelsGenerationError mbErrors,
|
||||
ModelsGenerator modelGenerator)
|
||||
IModelsGenerator modelGenerator)
|
||||
{
|
||||
_mbErrors = mbErrors;
|
||||
_modelGenerator = modelGenerator;
|
||||
@@ -30,6 +32,26 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
|
||||
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor that accepts IModelsGenerator only. Will be removed in V16.")]
|
||||
public BuildModelsBuilderController(
|
||||
IOptionsMonitor<ModelsBuilderSettings> modelsBuilderSettings,
|
||||
ModelsGenerationError mbErrors,
|
||||
ModelsGenerator modelGenerator)
|
||||
: this(modelsBuilderSettings, mbErrors, (IModelsGenerator)modelGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
// this constructor is required for the DI, otherwise it'll throw an "Ambiguous Constructor" errors at boot time.
|
||||
[Obsolete("Please use the constructor that accepts IModelsGenerator only. Will be removed in V16.")]
|
||||
public BuildModelsBuilderController(
|
||||
IOptionsMonitor<ModelsBuilderSettings> modelsBuilderSettings,
|
||||
ModelsGenerationError mbErrors,
|
||||
IModelsGenerator modelGenerator,
|
||||
ModelsGenerator notUsed)
|
||||
: this(modelsBuilderSettings, mbErrors, modelGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("build")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status428PreconditionRequired)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
@@ -281,7 +281,7 @@ public class BackOfficeController : SecurityControllerBase
|
||||
/// <summary>
|
||||
/// Called when a user links an external login provider in the back office
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="requestModel"></param>
|
||||
/// <returns></returns>
|
||||
// This method is marked as AllowAnonymous and protected with a secret (linkKey) inside the model for the following reasons
|
||||
// - when a js client uses the fetch api (or old ajax requests) they can send a bearer token
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
<AssemblyName>Umbraco.Cms.Api.Management</AssemblyName>
|
||||
<RootNamespace>Umbraco.Cms.Api.Management</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonPatch.Net" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - Imaging - ImageSharp</Title>
|
||||
<Description>Adds imaging support using ImageSharp/ImageSharp.Web to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - Imaging - ImageSharp 2</Title>
|
||||
<Description>Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - Persistence - Entity Framework Core - SQL Server migrations</Title>
|
||||
<Description>Adds support for Entity Framework Core SQL Server migrations to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - Persistence - Entity Framework Core - SQLite migrations</Title>
|
||||
<Description>Adds support for Entity Framework Core SQLite migrations to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -115,12 +115,7 @@ internal class SqliteEFCoreDistributedLockingMechanism<T> : IDistributedLockingM
|
||||
// Mostly no-op just check that we didn't end up ReadUncommitted for real.
|
||||
private void ObtainReadLock()
|
||||
{
|
||||
IEfCoreScope<T>? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope;
|
||||
|
||||
if (efCoreScope is null)
|
||||
{
|
||||
throw new PanicException("No current ambient scope");
|
||||
}
|
||||
IEfCoreScope<T>? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope ?? throw new PanicException("No current ambient scope");
|
||||
|
||||
efCoreScope.ExecuteWithContextAsync<Task>(async database =>
|
||||
{
|
||||
@@ -136,12 +131,7 @@ internal class SqliteEFCoreDistributedLockingMechanism<T> : IDistributedLockingM
|
||||
// lock occurs for entire database as opposed to row/table.
|
||||
private void ObtainWriteLock()
|
||||
{
|
||||
IEfCoreScope<T>? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope;
|
||||
|
||||
if (efCoreScope is null)
|
||||
{
|
||||
throw new PanicException("No ambient scope");
|
||||
}
|
||||
IEfCoreScope<T>? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope ?? throw new PanicException("No ambient scope");
|
||||
|
||||
efCoreScope.ExecuteWithContextAsync<Task>(async database =>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<Title>Umbraco CMS - Persistence - Entity Framework Core</Title>
|
||||
<Description>Adds support for Entity Framework Core to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998] remove async or make method synchronous,
|
||||
and remove this override -->
|
||||
<WarningsNotAsErrors>IDE0270,CS0108,CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
|
||||
@@ -420,7 +420,13 @@ public class LocalDb
|
||||
/// </remarks>
|
||||
public bool CreateDatabase(string databaseName, string filesPath)
|
||||
{
|
||||
GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename,
|
||||
GetDatabaseFiles(
|
||||
databaseName,
|
||||
filesPath,
|
||||
out var logName,
|
||||
out _,
|
||||
out _,
|
||||
out var mdfFilename,
|
||||
out var ldfFilename);
|
||||
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
@@ -463,7 +469,9 @@ public class LocalDb
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SetCommand(
|
||||
cmd,
|
||||
@"
|
||||
SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)",
|
||||
databaseName);
|
||||
|
||||
@@ -554,11 +562,7 @@ public class LocalDb
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var mdf = GetDatabase(cmd, databaseName);
|
||||
if (mdf == null)
|
||||
{
|
||||
throw new InvalidOperationException("Database does not exist.");
|
||||
}
|
||||
var mdf = GetDatabase(cmd, databaseName) ?? throw new InvalidOperationException("Database does not exist.");
|
||||
|
||||
DetachDatabase(cmd, databaseName);
|
||||
|
||||
@@ -597,9 +601,12 @@ public class LocalDb
|
||||
/// <param name="ldfName">The LDF logical name.</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
public void GetFilenames(string databaseName,
|
||||
out string? mdfName, out string? ldfName,
|
||||
out string? mdfFilename, out string? ldfFilename)
|
||||
public void GetFilenames(
|
||||
string databaseName,
|
||||
out string? mdfName,
|
||||
out string? ldfName,
|
||||
out string? mdfFilename,
|
||||
out string? ldfFilename)
|
||||
{
|
||||
using (var conn = new SqlConnection(_masterCstr))
|
||||
using (SqlCommand? cmd = conn.CreateCommand())
|
||||
@@ -621,7 +628,9 @@ public class LocalDb
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SetCommand(
|
||||
cmd,
|
||||
@"
|
||||
DECLARE @sql VARCHAR(MAX);
|
||||
SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';'
|
||||
FROM master.sys.sysprocesses
|
||||
@@ -640,7 +649,9 @@ public class LocalDb
|
||||
/// <returns>The full filename of the MDF file, if the database exists, otherwise null.</returns>
|
||||
private static string? GetDatabase(SqlCommand cmd, string databaseName)
|
||||
{
|
||||
SetCommand(cmd, @"
|
||||
SetCommand(
|
||||
cmd,
|
||||
@"
|
||||
SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)",
|
||||
databaseName);
|
||||
|
||||
@@ -707,7 +718,7 @@ public class LocalDb
|
||||
File.Delete(mdf);
|
||||
}
|
||||
|
||||
ldf = ldf ?? GetLogFilename(mdf);
|
||||
ldf ??= GetLogFilename(mdf);
|
||||
if (File.Exists(ldf))
|
||||
{
|
||||
File.Delete(ldf);
|
||||
@@ -726,7 +737,7 @@ public class LocalDb
|
||||
throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename));
|
||||
}
|
||||
|
||||
return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf";
|
||||
return mdfFilename[..^".mdf".Length] + "_log.ldf";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -743,7 +754,9 @@ public class LocalDb
|
||||
|
||||
var unused1 = cmd.ExecuteNonQuery();
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SetCommand(
|
||||
cmd,
|
||||
@"
|
||||
EXEC sp_detach_db @dbname=@0",
|
||||
databaseName);
|
||||
|
||||
@@ -758,8 +771,14 @@ public class LocalDb
|
||||
/// <param name="filesPath">The directory containing database files.</param>
|
||||
private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath)
|
||||
{
|
||||
GetDatabaseFiles(databaseName, filesPath,
|
||||
out var logName, out _, out _, out var mdfFilename, out var ldfFilename);
|
||||
GetDatabaseFiles(
|
||||
databaseName,
|
||||
filesPath,
|
||||
out var logName,
|
||||
out _,
|
||||
out _,
|
||||
out var mdfFilename,
|
||||
out var ldfFilename);
|
||||
|
||||
// cannot use parameters on CREATE DATABASE
|
||||
// ie "CREATE DATABASE @0 ..." does not work
|
||||
@@ -802,13 +821,19 @@ public class LocalDb
|
||||
/// <param name="ldfName">The LDF logical name.</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
private void GetFilenames(SqlCommand cmd, string databaseName,
|
||||
out string? mdfName, out string? ldfName,
|
||||
out string? mdfFilename, out string? ldfFilename)
|
||||
private void GetFilenames(
|
||||
SqlCommand cmd,
|
||||
string databaseName,
|
||||
out string? mdfName,
|
||||
out string? ldfName,
|
||||
out string? mdfFilename,
|
||||
out string? ldfFilename)
|
||||
{
|
||||
mdfName = ldfName = mdfFilename = ldfFilename = null;
|
||||
|
||||
SetCommand(cmd, @"
|
||||
SetCommand(
|
||||
cmd,
|
||||
@"
|
||||
SELECT DB_NAME(database_id), type_desc, name, physical_name
|
||||
FROM master.sys.master_files
|
||||
WHERE database_id=DB_ID(@0)",
|
||||
@@ -854,21 +879,32 @@ public class LocalDb
|
||||
/// <paramref name="delete" />.
|
||||
/// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp.
|
||||
/// </remarks>
|
||||
public void CopyDatabaseFiles(string databaseName, string filesPath,
|
||||
string? targetDatabaseName = null, string? targetFilesPath = null,
|
||||
string? sourceExtension = null, string? targetExtension = null,
|
||||
bool overwrite = false, bool delete = false)
|
||||
public void CopyDatabaseFiles(
|
||||
string databaseName,
|
||||
string filesPath,
|
||||
string? targetDatabaseName = null,
|
||||
string? targetFilesPath = null,
|
||||
string? sourceExtension = null,
|
||||
string? targetExtension = null,
|
||||
bool overwrite = false,
|
||||
bool delete = false)
|
||||
{
|
||||
var nop = (targetFilesPath == null || targetFilesPath == filesPath)
|
||||
&& (targetDatabaseName == null || targetDatabaseName == databaseName)
|
||||
&& (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension);
|
||||
&& ((sourceExtension == null && targetExtension == null) || sourceExtension == targetExtension);
|
||||
if (nop && delete == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GetDatabaseFiles(databaseName, filesPath,
|
||||
out _, out _, out _, out var mdfFilename, out var ldfFilename);
|
||||
GetDatabaseFiles(
|
||||
databaseName,
|
||||
filesPath,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out var mdfFilename,
|
||||
out var ldfFilename);
|
||||
|
||||
if (sourceExtension != null)
|
||||
{
|
||||
@@ -892,8 +928,14 @@ public class LocalDb
|
||||
else
|
||||
{
|
||||
// copy or copy+delete ie move
|
||||
GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath,
|
||||
out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename);
|
||||
GetDatabaseFiles(
|
||||
targetDatabaseName ?? databaseName,
|
||||
targetFilesPath ?? filesPath,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out var targetMdfFilename,
|
||||
out var targetLdfFilename);
|
||||
|
||||
if (targetExtension != null)
|
||||
{
|
||||
@@ -936,8 +978,14 @@ public class LocalDb
|
||||
/// </remarks>
|
||||
public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null)
|
||||
{
|
||||
GetDatabaseFiles(databaseName, filesPath,
|
||||
out _, out _, out _, out var mdfFilename, out var ldfFilename);
|
||||
GetDatabaseFiles(
|
||||
databaseName,
|
||||
filesPath,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out var mdfFilename,
|
||||
out var ldfFilename);
|
||||
|
||||
if (extension != null)
|
||||
{
|
||||
@@ -958,10 +1006,14 @@ public class LocalDb
|
||||
/// <param name="baseLogFilename">The base log filename (the LDF filename without the .ldf extension).</param>
|
||||
/// <param name="mdfFilename">The MDF filename.</param>
|
||||
/// <param name="ldfFilename">The LDF filename.</param>
|
||||
private static void GetDatabaseFiles(string databaseName, string filesPath,
|
||||
private static void GetDatabaseFiles(
|
||||
string databaseName,
|
||||
string filesPath,
|
||||
out string logName,
|
||||
out string baseFilename, out string baseLogFilename,
|
||||
out string mdfFilename, out string ldfFilename)
|
||||
out string baseFilename,
|
||||
out string baseLogFilename,
|
||||
out string mdfFilename,
|
||||
out string ldfFilename)
|
||||
{
|
||||
logName = databaseName + "_log";
|
||||
baseFilename = Path.Combine(filesPath, databaseName);
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<Title>Umbraco CMS - Persistence - SQL Server</Title>
|
||||
<Description>Adds support for SQL Server to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [SA1405] Simplify null checks, [SA1121] resolve hiding inherited members, [SA1117] remove async or make method synchronous,
|
||||
[IDE1006] fix naming rule violation, [CS0618] handle member obsolete appropriately, [IDE0270] simplify null check, [IDE0057] simplify substring,
|
||||
[IDE0054] use compound assignment, [CSO618] use NVARCARMAX, [IDE0048] add parenthesis for clarity, [CS1574] resolve ML comment cref attribute and remove this override -->
|
||||
<WarningsNotAsErrors>SA1405,SA1121,SA1117,SA1116,IDE1006,CS0618,IDE0270,IDE0057,IDE0054,CSO618,IDE0048,CS1574</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because NPoco.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<Title>Umbraco CMS - Persistence - SQLite</Title>
|
||||
<Description>Adds support for SQLite to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [CS0114] Resolve hiding inherited members and remove this override -->
|
||||
<WarningsNotAsErrors>CS0114</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [NU5123] Rename files so path is shorter -->
|
||||
<WarningsNotAsErrors>NU5123</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IncludeSymbols>false</IncludeSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998] remove async or make method synchronous,
|
||||
and remove this override -->
|
||||
<WarningsNotAsErrors>IDE0270,CS0108,CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Delivery\Umbraco.Cms.Api.Delivery.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Cms.StaticAssets\Umbraco.Cms.StaticAssets.csproj" />
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IncludeSymbols>false</IncludeSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Targets\Umbraco.Cms.Targets.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Cms.Imaging.ImageSharp\Umbraco.Cms.Imaging.ImageSharp.csproj" />
|
||||
|
||||
@@ -13,7 +13,7 @@ public static partial class Constants
|
||||
public static readonly string[] UmbracoCoreAssemblyNames =
|
||||
{
|
||||
"Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene",
|
||||
"Umbraco.Web.Common", "Umbraco.Web.BackOffice", "Umbraco.Web.Website",
|
||||
"Umbraco.Web.Common", "Umbraco.Cms.Api.Common","Umbraco.Cms.Api.Delivery","Umbraco.Cms.Api.Management", "Umbraco.Web.Website",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public static partial class Constants
|
||||
[Obsolete("This link is not used anymore in the XSS protected check.")]
|
||||
public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection";
|
||||
public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers";
|
||||
public const string CspHeaderCheck = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP";
|
||||
|
||||
public static class HttpsCheck
|
||||
{
|
||||
|
||||
@@ -467,6 +467,12 @@
|
||||
You can read about this on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection" target="_blank" rel="noopener" class="btn-link -underline">Mozilla</a> website ]]></key>
|
||||
<key alias="xssProtectionCheckHeaderNotFound">
|
||||
<![CDATA[The header <strong>X-XSS-Protection</strong> was not found.]]></key>
|
||||
<key alias="contentSecurityPolicyCheckHeaderFound">
|
||||
<![CDATA[The header <strong>Content-Security-Policy (CSP)</strong> was found. ]]>
|
||||
</key>
|
||||
<key alias="contentSecurityPolicyCheckHeaderNotFound">
|
||||
<![CDATA[The header <strong>Content-Security-Policy</strong> (CSP) used to prevent cross-site scripting (XSS) attacks and other code injection vulnerabilities was not found.]]>
|
||||
</key>
|
||||
<key alias="excessiveHeadersFound"><![CDATA[The following headers revealing information about the website technology were found: <strong>%0%</strong>.]]></key>
|
||||
<key alias="excessiveHeadersNotFound">No headers revealing information about the website technology were found.
|
||||
</key>
|
||||
|
||||
@@ -455,6 +455,12 @@
|
||||
You can read about this on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection" target="_blank" rel="noopener" class="btn-link -underline">Mozilla</a> website ]]></key>
|
||||
<key alias="xssProtectionCheckHeaderNotFound">
|
||||
<![CDATA[The header <strong>X-XSS-Protection</strong> was not found.]]></key>
|
||||
<key alias="contentSecurityPolicyCheckHeaderFound">
|
||||
<![CDATA[The header <strong>Content-Security-Policy (CSP)</strong> was found. ]]>
|
||||
</key>
|
||||
<key alias="contentSecurityPolicyCheckHeaderNotFound">
|
||||
<![CDATA[The header <strong>Content-Security-Policy</strong> (CSP) used to prevent cross-site scripting (XSS) attacks and other code injection vulnerabilities was not found.]]>
|
||||
</key>
|
||||
<key alias="excessiveHeadersFound">
|
||||
<![CDATA[The following headers revealing information about the website technology were found: <strong>%0%</strong>.]]></key>
|
||||
<key alias="excessiveHeadersNotFound">No headers revealing information about the website technology were found.
|
||||
|
||||
@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Models.Entities;
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods that return udis for Umbraco entities.
|
||||
/// Provides extension methods that return UDIs for Umbraco entities.
|
||||
/// </summary>
|
||||
public static class UdiGetterExtensions
|
||||
{
|
||||
@@ -19,11 +19,177 @@ public static class UdiGetterExtensions
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this ITemplate entity)
|
||||
public static Udi GetUdi(this IEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed();
|
||||
return entity switch
|
||||
{
|
||||
// Concrete types
|
||||
EntityContainer container => container.GetUdi(),
|
||||
Script script => script.GetUdi(),
|
||||
Stylesheet stylesheet => stylesheet.GetUdi(),
|
||||
// Interfaces
|
||||
IContentBase contentBase => contentBase.GetUdi(),
|
||||
IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(),
|
||||
IDataType dataType => dataType.GetUdi(),
|
||||
IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(),
|
||||
ILanguage language => language.GetUdi(),
|
||||
IMemberGroup memberGroup => memberGroup.GetUdi(),
|
||||
IPartialView partialView => partialView.GetUdi(),
|
||||
IRelationType relationType => relationType.GetUdi(),
|
||||
ITemplate template => template.GetUdi(),
|
||||
IWebhook webhook => webhook.GetUdi(),
|
||||
_ => throw new NotSupportedException($"Entity type {entity.GetType().FullName} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this EntityContainer entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string entityType;
|
||||
if (entity.ContainedObjectType == Constants.ObjectTypes.DataType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DataTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.MediaTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Contained object type {entity.ContainedObjectType} is not supported.");
|
||||
}
|
||||
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this Script entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this Stylesheet entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContentBase entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return entity switch
|
||||
{
|
||||
IContent content => content.GetUdi(),
|
||||
IMedia media => media.GetUdi(),
|
||||
IMember member => member.GetUdi(),
|
||||
_ => throw new NotSupportedException($"Content base type {entity.GetType().FullName} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContent entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document;
|
||||
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IMedia entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IMember entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContentTypeComposition entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return entity switch
|
||||
{
|
||||
IContentType contentType => contentType.GetUdi(),
|
||||
IMediaType mediaType => mediaType.GetUdi(),
|
||||
IMemberType memberType => memberType.GetUdi(),
|
||||
_ => throw new NotSupportedException($"Composition type {entity.GetType().FullName} is not supported."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,42 +234,6 @@ public static class UdiGetterExtensions
|
||||
return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IMemberGroup entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContentTypeComposition entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string entityType = entity switch
|
||||
{
|
||||
IContentType => Constants.UdiEntityType.DocumentType,
|
||||
IMediaType => Constants.UdiEntityType.MediaType,
|
||||
IMemberType => Constants.UdiEntityType.MemberType,
|
||||
_ => throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)),
|
||||
};
|
||||
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
@@ -118,129 +248,6 @@ public static class UdiGetterExtensions
|
||||
return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this EntityContainer entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string entityType;
|
||||
if (entity.ContainedObjectType == Constants.ObjectTypes.DataType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DataTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.MediaTypeContainer;
|
||||
}
|
||||
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
|
||||
{
|
||||
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType));
|
||||
}
|
||||
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IMedia entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContent entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document;
|
||||
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IMember entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this Stylesheet entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this Script entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UDI from a path.
|
||||
/// </summary>
|
||||
/// <param name="entityType">The type of the entity.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
private static StringUdi GetUdiFromPath(string entityType, string path)
|
||||
{
|
||||
string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/");
|
||||
|
||||
return new StringUdi(entityType, id).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
@@ -262,11 +269,11 @@ public static class UdiGetterExtensions
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this IPartialView entity)
|
||||
public static StringUdi GetUdi(this ILanguage entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path);
|
||||
return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -276,19 +283,25 @@ public static class UdiGetterExtensions
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IContentBase entity)
|
||||
public static GuidUdi GetUdi(this IMemberGroup entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
string type = entity switch
|
||||
{
|
||||
IContent => Constants.UdiEntityType.Document,
|
||||
IMedia => Constants.UdiEntityType.Media,
|
||||
IMember => Constants.UdiEntityType.Member,
|
||||
_ => throw new NotSupportedException(string.Format("Content base type {0} is not supported.", entity.GetType().FullName)),
|
||||
};
|
||||
return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
return new GuidUdi(type, entity.Key).EnsureClosed();
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this IPartialView entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -305,6 +318,20 @@ public static class UdiGetterExtensions
|
||||
return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this ITemplate entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
@@ -320,56 +347,17 @@ public static class UdiGetterExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// Gets the UDI from a path.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <param name="entityType">The type of the entity.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static StringUdi GetUdi(this ILanguage entity)
|
||||
private static StringUdi GetUdiFromPath(string entityType, string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/");
|
||||
|
||||
return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static Udi GetUdi(this IEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return entity switch
|
||||
{
|
||||
// Concrete types
|
||||
EntityContainer container => container.GetUdi(),
|
||||
Stylesheet stylesheet => stylesheet.GetUdi(),
|
||||
Script script => script.GetUdi(),
|
||||
// Content types
|
||||
IContentType contentType => contentType.GetUdi(),
|
||||
IMediaType mediaType => mediaType.GetUdi(),
|
||||
IMemberType memberType => memberType.GetUdi(),
|
||||
IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(),
|
||||
// Content
|
||||
IContent content => content.GetUdi(),
|
||||
IMedia media => media.GetUdi(),
|
||||
IMember member => member.GetUdi(),
|
||||
IContentBase contentBase => contentBase.GetUdi(),
|
||||
// Other
|
||||
IDataType dataTypeComposition => dataTypeComposition.GetUdi(),
|
||||
IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(),
|
||||
ILanguage language => language.GetUdi(),
|
||||
IMemberGroup memberGroup => memberGroup.GetUdi(),
|
||||
IPartialView partialView => partialView.GetUdi(),
|
||||
IRelationType relationType => relationType.GetUdi(),
|
||||
ITemplate template => template.GetUdi(),
|
||||
IWebhook webhook => webhook.GetUdi(),
|
||||
_ => throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)),
|
||||
};
|
||||
return new StringUdi(entityType, id).EnsureClosed();
|
||||
}
|
||||
}
|
||||
|
||||
31
src/Umbraco.Core/HealthChecks/Checks/Security/CspCheck.cs
Normal file
31
src/Umbraco.Core/HealthChecks/Checks/Security/CspCheck.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.HealthChecks.Checks.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Health check for the recommended production setup regarding the content-security-policy header.
|
||||
/// </summary>
|
||||
[HealthCheck(
|
||||
"10BEBF47-C128-4C5E-9680-5059BEAFBBDF",
|
||||
"Content Security Policy (CSP)",
|
||||
Description = "Checks whether the site contains a Content-Security-Policy (CSP) header.",
|
||||
Group = "Security")]
|
||||
public class CspCheck : BaseHttpHeaderCheck
|
||||
{
|
||||
private const string LocalizationPrefix = "contentSecurityPolicy";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CspCheck" /> class.
|
||||
/// </summary>
|
||||
public CspCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService)
|
||||
: base(hostingEnvironment, textService, "Content-Security-Policy", LocalizationPrefix, false, false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.CspHeaderCheck;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class DisposableTimer : DisposableObjectSlim
|
||||
_endMessageArgs = endMessageArgs;
|
||||
_failMessageArgs = failMessageArgs;
|
||||
_thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds;
|
||||
_timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish
|
||||
_timingId = Guid.NewGuid().ToString("N")[..7]; // keep it short-ish
|
||||
|
||||
if (thresholdMilliseconds == 0)
|
||||
{
|
||||
|
||||
@@ -6,5 +6,16 @@ namespace Umbraco.Cms.Core.Media.EmbedProviders;
|
||||
/// Wrapper class for OEmbed response.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class OEmbedResponse : OEmbedResponseBase<double>;
|
||||
public class OEmbedResponse : OEmbedResponseBase<double>
|
||||
{
|
||||
|
||||
// these is only here to avoid breaking changes. In theory it should still be source code compatible to remove them.
|
||||
public new double? ThumbnailHeight => base.ThumbnailHeight;
|
||||
|
||||
public new double? ThumbnailWidth => base.ThumbnailWidth;
|
||||
|
||||
public new double? Height => base.Height;
|
||||
|
||||
public new double? Width => base.Width;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public abstract class OEmbedResponseBase<T>
|
||||
public string? ThumbnailUrl { get; set; }
|
||||
|
||||
[DataMember(Name = "thumbnail_height")]
|
||||
public T? ThumbnailHeight { get; set; }
|
||||
public virtual T? ThumbnailHeight { get; set; }
|
||||
|
||||
[DataMember(Name = "thumbnail_width")]
|
||||
public T? ThumbnailWidth { get; set; }
|
||||
|
||||
@@ -58,8 +58,6 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
/// <param name="userCount"></param>
|
||||
/// <param name="alias"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="permissions"></param>
|
||||
/// <param name="granularPermissions"></param>
|
||||
/// <param name="icon"></param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
public UserGroup(
|
||||
|
||||
@@ -54,7 +54,7 @@ public interface ITrackedReferencesRepository
|
||||
/// Gets a page of items which are in relation with the current item.
|
||||
/// Basically, shows the items which depend on the current item.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier of the entity to retrieve relations for.</param>
|
||||
/// <param name="key">The identifier of the entity to retrieve relations for.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="filterMustBeIsDependency">
|
||||
@@ -81,7 +81,7 @@ public interface ITrackedReferencesRepository
|
||||
/// <summary>
|
||||
/// Gets a page of items used in any kind of relation from selected integer ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The identifiers of the entities to check for relations.</param>
|
||||
/// <param name="keys">The identifiers of the entities to check for relations.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="filterMustBeIsDependency">
|
||||
|
||||
@@ -2,7 +2,6 @@ using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
@@ -77,8 +77,8 @@ public class ContentFinderByUrlAndTemplate : ContentFinderByUrl
|
||||
|
||||
// look for template in last position
|
||||
var pos = path.LastIndexOf('/');
|
||||
var templateAlias = path.Substring(pos + 1);
|
||||
path = pos == 0 ? "/" : path.Substring(0, pos);;
|
||||
var templateAlias = path[(pos + 1)..];
|
||||
path = pos == 0 ? "/" : path[..pos];
|
||||
|
||||
ITemplate? template = _fileService.GetTemplate(templateAlias);
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
|
||||
// need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned)
|
||||
var pos = route.IndexOf('/');
|
||||
var path = pos == 0 ? route : route.Substring(pos);
|
||||
var path = pos == 0 ? route : route[pos..];
|
||||
|
||||
var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path));
|
||||
uri = _uriUtility.UriFromUmbraco(uri, _requestSettings);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -12,7 +16,11 @@ internal sealed class ContentEditingService
|
||||
{
|
||||
private readonly ITemplateService _templateService;
|
||||
private readonly ILogger<ContentEditingService> _logger;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
|
||||
public ContentEditingService(
|
||||
IContentService contentService,
|
||||
IContentTypeService contentTypeService,
|
||||
@@ -24,10 +32,46 @@ internal sealed class ContentEditingService
|
||||
IUserIdKeyResolver userIdKeyResolver,
|
||||
ITreeEntitySortingService treeEntitySortingService,
|
||||
IContentValidationService contentValidationService)
|
||||
: this(
|
||||
contentService,
|
||||
contentTypeService,
|
||||
propertyEditorCollection,
|
||||
dataTypeService,
|
||||
templateService,
|
||||
logger,
|
||||
scopeProvider,
|
||||
userIdKeyResolver,
|
||||
treeEntitySortingService,
|
||||
contentValidationService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILanguageService>()
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ContentEditingService(
|
||||
IContentService contentService,
|
||||
IContentTypeService contentTypeService,
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IDataTypeService dataTypeService,
|
||||
ITemplateService templateService,
|
||||
ILogger<ContentEditingService> logger,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IUserIdKeyResolver userIdKeyResolver,
|
||||
ITreeEntitySortingService treeEntitySortingService,
|
||||
IContentValidationService contentValidationService,
|
||||
IUserService userService,
|
||||
ILocalizationService localizationService,
|
||||
ILanguageService languageService)
|
||||
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService)
|
||||
{
|
||||
_templateService = templateService;
|
||||
_logger = logger;
|
||||
_userService = userService;
|
||||
_localizationService = localizationService;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
public async Task<IContent?> GetAsync(Guid key)
|
||||
@@ -74,7 +118,7 @@ internal sealed class ContentEditingService
|
||||
ContentEditingOperationStatus validationStatus = result.Status;
|
||||
ContentValidationResult validationResult = result.Result.ValidationResult;
|
||||
|
||||
IContent content = result.Result.Content!;
|
||||
IContent content = await EnsureOnlyAllowedFieldsAreUpdated(result.Result.Content!, userKey);
|
||||
ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, createModel.TemplateKey);
|
||||
if (updateTemplateStatus != ContentEditingOperationStatus.Success)
|
||||
{
|
||||
@@ -87,6 +131,53 @@ internal sealed class ContentEditingService
|
||||
: Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A temporary method that ensures the data is sent in is overridden by the original data, in cases where the user do not have permissions to change the data.
|
||||
/// </summary>
|
||||
private async Task<IContent> EnsureOnlyAllowedFieldsAreUpdated(IContent contentWithPotentialUnallowedChanges, Guid userKey)
|
||||
{
|
||||
if (contentWithPotentialUnallowedChanges.ContentType.VariesByCulture() is false)
|
||||
{
|
||||
return contentWithPotentialUnallowedChanges;
|
||||
}
|
||||
|
||||
IContent? existingContent = await GetAsync(contentWithPotentialUnallowedChanges.Key);
|
||||
|
||||
IUser? user = await _userService.GetAsync(userKey);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return contentWithPotentialUnallowedChanges;
|
||||
}
|
||||
|
||||
var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!;
|
||||
|
||||
var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet();
|
||||
|
||||
foreach (var culture in contentWithPotentialUnallowedChanges.EditedCultures ?? contentWithPotentialUnallowedChanges.PublishedCultures)
|
||||
{
|
||||
if (allowedCultures.Contains(culture))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// else override the updates values with the original values.
|
||||
foreach (IProperty property in contentWithPotentialUnallowedChanges.Properties)
|
||||
{
|
||||
if (property.PropertyType.VariesByCulture() is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = existingContent?.Properties.First(x=>x.Alias == property.Alias).GetValue(culture, null, false);
|
||||
property.SetValue(value, culture, null);
|
||||
}
|
||||
}
|
||||
|
||||
return contentWithPotentialUnallowedChanges;
|
||||
}
|
||||
|
||||
public async Task<Attempt<ContentUpdateResult, ContentEditingOperationStatus>> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey)
|
||||
{
|
||||
IContent? content = ContentService.GetById(key);
|
||||
@@ -111,6 +202,8 @@ internal sealed class ContentEditingService
|
||||
ContentEditingOperationStatus validationStatus = result.Status;
|
||||
ContentValidationResult validationResult = result.Result.ValidationResult;
|
||||
|
||||
content = await EnsureOnlyAllowedFieldsAreUpdated(content, userKey);
|
||||
|
||||
ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, updateModel.TemplateKey);
|
||||
if (updateTemplateStatus != ContentEditingOperationStatus.Success)
|
||||
{
|
||||
|
||||
@@ -620,10 +620,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
if (ordering == null)
|
||||
{
|
||||
ordering = Ordering.By("sortOrder");
|
||||
}
|
||||
ordering ??= Ordering.By("sortOrder");
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
@@ -651,10 +648,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
if (ordering == null)
|
||||
{
|
||||
ordering = Ordering.By("sortOrder");
|
||||
}
|
||||
ordering ??= Ordering.By("sortOrder");
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
@@ -811,10 +805,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
if (ordering == null)
|
||||
{
|
||||
ordering = Ordering.By("sortOrder");
|
||||
}
|
||||
ordering ??= Ordering.By("sortOrder");
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
@@ -828,10 +819,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, IQuery<IContent>? filter = null, Ordering? ordering = null)
|
||||
{
|
||||
if (ordering == null)
|
||||
{
|
||||
ordering = Ordering.By("Path");
|
||||
}
|
||||
ordering ??= Ordering.By("Path");
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
@@ -969,10 +957,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
if (ordering == null)
|
||||
{
|
||||
ordering = Ordering.By("Path");
|
||||
}
|
||||
ordering ??= Ordering.By("Path");
|
||||
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
IQuery<IContent>? query = Query<IContent>()?
|
||||
@@ -1409,6 +1394,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="allLangs"></param>
|
||||
/// <param name="notificationState"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="branchOne"></param>
|
||||
/// <param name="branchRoot"></param>
|
||||
@@ -1879,7 +1865,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
// publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
IProperty[]? invalidProperties = null;
|
||||
var impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
CultureImpact impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
var tryPublish = d.PublishCulture(impact) &&
|
||||
_propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
@@ -1961,7 +1947,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
{
|
||||
return culturesToPublish.All(culture =>
|
||||
{
|
||||
var impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
|
||||
CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
|
||||
return content.PublishCulture(impact) &&
|
||||
_propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
|
||||
});
|
||||
@@ -1977,10 +1963,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
// if published, republish
|
||||
if (published)
|
||||
{
|
||||
if (cultures == null)
|
||||
{
|
||||
cultures = new HashSet<string>(); // empty means 'already published'
|
||||
}
|
||||
cultures ??= new HashSet<string>(); // empty means 'already published'
|
||||
|
||||
if (edited)
|
||||
{
|
||||
@@ -1996,10 +1979,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
return cultures; // null means 'nothing to do'
|
||||
}
|
||||
|
||||
if (cultures == null)
|
||||
{
|
||||
cultures = new HashSet<string>();
|
||||
}
|
||||
cultures ??= new HashSet<string>();
|
||||
|
||||
cultures.Add(c); // <culture> means 'publish this culture'
|
||||
return cultures;
|
||||
@@ -2751,7 +2731,6 @@ public class ContentService : RepositoryService, IContentService
|
||||
/// </summary>
|
||||
/// <param name="content">The <see cref="IContent" /> to copy</param>
|
||||
/// <param name="parentId">Id of the Content's new Parent</param>
|
||||
/// <param name="parentKey">Key of the Content's new Parent</param>
|
||||
/// <param name="relateToOriginal">Boolean indicating whether the copy should be related to the original</param>
|
||||
/// <param name="recursive">A value indicating whether to recursively copy children.</param>
|
||||
/// <param name="userId">Optional Id of the User copying the Content</param>
|
||||
@@ -3420,6 +3399,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="evtMsgs"></param>
|
||||
/// <param name="notificationState"></param>
|
||||
/// <returns></returns>
|
||||
private PublishResult StrategyCanUnpublish(
|
||||
ICoreScope scope,
|
||||
@@ -3428,7 +3408,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
IDictionary<string, object?>? notificationState)
|
||||
{
|
||||
// raise Unpublishing notification
|
||||
var notification = new ContentUnpublishingNotification(content, evtMsgs).WithState(notificationState);
|
||||
ContentUnpublishingNotification notification = new ContentUnpublishingNotification(content, evtMsgs).WithState(notificationState);
|
||||
var notificationResult = scope.Notifications.PublishCancelable(notification);
|
||||
|
||||
if (notificationResult)
|
||||
|
||||
@@ -555,7 +555,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
return Attempt.FailWithStatus(DataTypeOperationStatus.DuplicateKey, dataType);
|
||||
}
|
||||
|
||||
var result = await SaveAsync(dataType, () => DataTypeOperationStatus.Success, userKey, AuditType.New);
|
||||
Attempt<IDataType, DataTypeOperationStatus> result = await SaveAsync(dataType, () => DataTypeOperationStatus.Success, userKey, AuditType.New);
|
||||
|
||||
scope.Complete();
|
||||
|
||||
|
||||
@@ -523,8 +523,6 @@ public class FileService : RepositoryService, IFileService
|
||||
/// </summary>
|
||||
/// <param name="templates">List of <see cref="Template" /> to save</param>
|
||||
/// <param name="userId">Optional id of the user</param>
|
||||
// FIXME: we need to re-implement PackageDataInstallation.ImportTemplates so it imports templates in the correct order
|
||||
// instead of relying on being able to save invalid templates (child templates whose master has yet to be created)
|
||||
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
|
||||
public void SaveTemplate(IEnumerable<ITemplate> templates, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
|
||||
@@ -105,10 +105,9 @@ public interface IAuditService : IService
|
||||
/// <summary>
|
||||
/// Returns paged items in the audit trail for a given user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="pageIndex"></param>
|
||||
/// <param name="pageSize"></param>
|
||||
/// <param name="totalRecords"></param>
|
||||
/// <param name="userKey"></param>
|
||||
/// <param name="skip"></param>
|
||||
/// <param name="take"></param>
|
||||
/// <param name="orderDirection">
|
||||
/// By default this will always be ordered descending (newest first)
|
||||
/// </param>
|
||||
@@ -117,9 +116,7 @@ public interface IAuditService : IService
|
||||
/// or the custom filter
|
||||
/// so we need to do that here
|
||||
/// </param>
|
||||
/// <param name="customFilter">
|
||||
/// Optional filter to be applied
|
||||
/// </param>
|
||||
/// <param name="sinceDate"></param>
|
||||
/// <returns></returns>
|
||||
Task<PagedModel<IAuditItem>> GetPagedItemsByUserAsync(
|
||||
Guid userKey,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
@@ -9,7 +9,7 @@ public interface IContentPublishingService
|
||||
/// Publishes a single content item.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the root content.</param>
|
||||
/// <param name="cultures">The cultures to publish.</param>
|
||||
/// <param name="cultureAndSchedule">The cultures to publish and their publishing schedules.</param>
|
||||
/// <param name="userKey">The identifier of the user performing the operation.</param>
|
||||
/// <returns>Result of the publish operation.</returns>
|
||||
Task<Attempt<ContentPublishingResult, ContentPublishingOperationStatus>> PublishAsync(Guid key, CultureAndScheduleModel cultureAndSchedule, Guid userKey);
|
||||
|
||||
@@ -196,7 +196,7 @@ public interface IDataTypeService : IService
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorAlias">Alias of the property editor</param>
|
||||
/// <returns>Collection of <see cref="IDataType" /> configured for the property editor</returns>
|
||||
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias);
|
||||
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias) => Task.FromResult(GetByEditorAlias(propertyEditorAlias));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all <see cref="IDataType" /> for a given editor UI alias
|
||||
@@ -246,5 +246,5 @@ public interface IDataTypeService : IService
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorAlias">Aliases of the property editors</param>
|
||||
/// <returns>Collection of <see cref="IDataType" /> configured for the property editors</returns>
|
||||
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias);
|
||||
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias) => Task.FromResult(propertyEditorAlias.SelectMany(x=>GetByEditorAlias(x)));
|
||||
}
|
||||
|
||||
@@ -64,16 +64,28 @@ public interface IPackageDataInstallation
|
||||
/// <returns>An enumerable list of generated languages</returns>
|
||||
IReadOnlyList<ILanguage> ImportLanguages(IEnumerable<XElement> languageElements, int userId);
|
||||
|
||||
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
|
||||
IEnumerable<ITemplate> ImportTemplate(XElement templateElement, int userId);
|
||||
|
||||
Task<IEnumerable<ITemplate>> ImportTemplateAsync(XElement templateElement, int userId) => Task.FromResult(ImportTemplate(templateElement, userId));
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="ITemplate"/>
|
||||
/// </summary>
|
||||
/// <param name="templateElements">Xml to import</param>
|
||||
/// <param name="userId">Optional user id</param>
|
||||
/// <returns>An enumerable list of generated Templates</returns>
|
||||
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
|
||||
IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId);
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="ITemplate"/>
|
||||
/// </summary>
|
||||
/// <param name="templateElements">Xml to import</param>
|
||||
/// <param name="userId">Optional user id</param>
|
||||
/// <returns>An enumerable list of generated Templates</returns>
|
||||
Task<IReadOnlyList<ITemplate>> ImportTemplatesAsync(IReadOnlyCollection<XElement> templateElements, int userId) => Task.FromResult(ImportTemplates(templateElements, userId));
|
||||
|
||||
Guid GetContentTypeKey(XElement contentType);
|
||||
|
||||
string? GetEntityTypeAlias(XElement entityType);
|
||||
|
||||
@@ -177,9 +177,8 @@ public interface IRelationService : IService
|
||||
/// Gets a paged result of <see cref="IRelation" />
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="pageIndex"></param>
|
||||
/// <param name="pageSize"></param>
|
||||
/// <param name="totalRecords"></param>
|
||||
/// <param name="skip"></param>
|
||||
/// <param name="take"></param>
|
||||
/// <param name="ordering"></param>
|
||||
/// <returns></returns>
|
||||
Task<Attempt<PagedModel<IRelation>, RelationOperationStatus>> GetPagedByRelationTypeKeyAsync(Guid key, int skip, int take, Ordering? ordering = null);
|
||||
@@ -403,6 +402,8 @@ public interface IRelationService : IService
|
||||
/// Gets the Relation types in a paged manner.
|
||||
/// Currently implements the paging in memory on the name attribute because the underlying repository does not support paging yet
|
||||
/// </summary>
|
||||
/// <param name="skip"></param>
|
||||
/// <param name="take"></param>
|
||||
/// <param name="ids"></param>
|
||||
/// <returns></returns>
|
||||
Task<PagedModel<IRelationType>> GetPagedRelationTypesAsync(int skip, int take, params int[] ids);
|
||||
|
||||
@@ -54,7 +54,7 @@ public interface ITrackedReferencesService
|
||||
/// Gets a paged result of items which are in relation with the current item.
|
||||
/// Basically, shows the items which depend on the current item.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier of the entity to retrieve relations for.</param>
|
||||
/// <param name="key">The identifier of the entity to retrieve relations for.</param>
|
||||
/// <param name="skip">The amount of items to skip</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="filterMustBeIsDependency">
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ContentTypeImportService : IContentTypeImportService
|
||||
/// Imports the contentType
|
||||
/// </summary>
|
||||
/// <param name="temporaryFileId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="userKey"></param>
|
||||
/// <param name="contentTypeId">the id of the contentType to overwrite, null if a new contentType should be created</param>
|
||||
/// <returns></returns>
|
||||
public async Task<Attempt<IContentType?, ContentTypeImportOperationStatus>> Import(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
@@ -220,7 +220,7 @@ internal class LocalizationService : RepositoryService, ILocalizationService
|
||||
/// <param name="userId">Optional id of the user saving the dictionary item</param>
|
||||
[Obsolete("Please use IDictionaryItemService for dictionary item operations. Will be removed in V15.")]
|
||||
public void Save(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
|
||||
{ ;
|
||||
{
|
||||
Guid currentUserKey = _userIdKeyResolver.GetAsync(userId).GetAwaiter().GetResult();
|
||||
if (dictionaryItem.Id > 0)
|
||||
{
|
||||
|
||||
@@ -5,10 +5,7 @@ public enum ContentTypeOperationStatus
|
||||
Success,
|
||||
DuplicateAlias,
|
||||
InvalidAlias,
|
||||
NameCannotBeEmpty,
|
||||
NameTooLong,
|
||||
InvalidPropertyTypeAlias,
|
||||
PropertyTypeAliasCannotEqualContentTypeAlias,
|
||||
DuplicatePropertyTypeAlias,
|
||||
DataTypeNotFound,
|
||||
InvalidInheritance,
|
||||
@@ -21,6 +18,9 @@ public enum ContentTypeOperationStatus
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
CancelledByNotification,
|
||||
PropertyTypeAliasCannotEqualContentTypeAlias,
|
||||
NameCannotBeEmpty,
|
||||
NameTooLong,
|
||||
InvalidElementFlagDocumentHasContent,
|
||||
InvalidElementFlagElementIsUsedInPropertyEditorConfiguration,
|
||||
InvalidElementFlagComparedToParent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.Services.Querying.RecycleBin;
|
||||
|
||||
/// <summary>
|
||||
/// < 10 = Success
|
||||
/// <10 = Success.
|
||||
/// </summary>
|
||||
public enum RecycleBinQueryResultType
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -84,7 +84,7 @@ internal class RedirectUrlService : RepositoryService, IRedirectUrlService
|
||||
|
||||
public async Task<IRedirectUrl?> GetMostRecentRedirectUrlAsync(string url)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return await _redirectUrlRepository.GetMostRecentUrlAsync(url);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ internal class RedirectUrlService : RepositoryService, IRedirectUrlService
|
||||
return await GetMostRecentRedirectUrlAsync(url);
|
||||
}
|
||||
|
||||
using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ using System.Linq.Expressions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Editors;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
@@ -156,7 +154,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
if (permissions.Any(x => x.EntityId == nodeId))
|
||||
{
|
||||
EntityPermission found = permissions.First(x => x.EntityId == nodeId);
|
||||
var assignedPermissionsArray = found.AssignedPermissions;
|
||||
ISet<string> assignedPermissionsArray = found.AssignedPermissions;
|
||||
|
||||
// Working with permissions assigned directly to a user AND to their groups, so maybe several per node
|
||||
// and we need to get the most permissive set
|
||||
@@ -668,7 +666,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserCreationResult());
|
||||
}
|
||||
|
||||
var userGroups = _userGroupRepository.GetMany().Where(x=>model.UserGroupKeys.Contains(x.Key)).ToArray();
|
||||
IUserGroup[] userGroups = _userGroupRepository.GetMany().Where(x=>model.UserGroupKeys.Contains(x.Key)).ToArray();
|
||||
|
||||
if (userGroups.Length != model.UserGroupKeys.Count)
|
||||
{
|
||||
@@ -789,7 +787,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new UserInvitationResult());
|
||||
}
|
||||
|
||||
var userGroups = _userGroupRepository.GetMany().Where(x=>model.UserGroupKeys.Contains(x.Key)).ToArray();
|
||||
IUserGroup[] userGroups = _userGroupRepository.GetMany().Where(x => model.UserGroupKeys.Contains(x.Key)).ToArray();
|
||||
|
||||
if (userGroups.Length != model.UserGroupKeys.Count)
|
||||
{
|
||||
@@ -2303,7 +2301,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
var results = new List<NodePermissions>();
|
||||
foreach (KeyValuePair<Guid, int> node in nodes)
|
||||
{
|
||||
var permissions = permissionsCollection.GetAllPermissions(node.Value);
|
||||
ISet<string> permissions = permissionsCollection.GetAllPermissions(node.Value);
|
||||
results.Add(new NodePermissions { NodeKey = node.Key, Permissions = permissions });
|
||||
}
|
||||
|
||||
@@ -2366,7 +2364,7 @@ internal class UserService : RepositoryService, IUserService
|
||||
var results = new List<NodePermissions>();
|
||||
foreach (int nodeId in idKeyMap.Keys)
|
||||
{
|
||||
var permissions = permissionCollection.GetAllPermissions(nodeId);
|
||||
ISet<string> permissions = permissionCollection.GetAllPermissions(nodeId);
|
||||
results.Add(new NodePermissions { NodeKey = idKeyMap[nodeId], Permissions = permissions });
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ internal sealed class WebProfilerService : IWebProfilerService
|
||||
//FIXME when we can get current user
|
||||
return Attempt.Succeed(-1);
|
||||
|
||||
#pragma warning disable CS0162 // Unreachable code detected
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Attempt<int>? userIdAttempt = _backOfficeSecurityAccessor?.BackOfficeSecurity?.GetUserId();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
#pragma warning restore CS0162 // Unreachable code detected
|
||||
|
||||
return (userIdAttempt.HasValue && userIdAttempt.Value.Success)
|
||||
? Attempt.Succeed(userIdAttempt.Value.Result)
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<Description>Contains the core assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Core</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
|
||||
|
||||
@@ -9,6 +9,15 @@ namespace Umbraco.Cms.Core.DependencyInjection;
|
||||
/// </summary>
|
||||
public static class WebhookEventCollectionBuilderCmsExtensions
|
||||
{
|
||||
private static readonly Type[] _defaultTypes =
|
||||
[
|
||||
typeof(ContentDeletedWebhookEvent),
|
||||
typeof(ContentPublishedWebhookEvent),
|
||||
typeof(ContentUnpublishedWebhookEvent),
|
||||
typeof(MediaDeletedWebhookEvent),
|
||||
typeof(MediaSavedWebhookEvent),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Adds the default webhook events.
|
||||
/// </summary>
|
||||
@@ -21,12 +30,24 @@ public static class WebhookEventCollectionBuilderCmsExtensions
|
||||
/// </remarks>
|
||||
public static WebhookEventCollectionBuilderCms AddDefault(this WebhookEventCollectionBuilderCms builder)
|
||||
{
|
||||
builder.Builder
|
||||
.Add<ContentDeletedWebhookEvent>()
|
||||
.Add<ContentPublishedWebhookEvent>()
|
||||
.Add<ContentUnpublishedWebhookEvent>()
|
||||
.Add<MediaDeletedWebhookEvent>()
|
||||
.Add<MediaSavedWebhookEvent>();
|
||||
builder.Builder.Add(_defaultTypes);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the default webhook events.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
/// <returns>
|
||||
/// The builder.
|
||||
/// </returns>
|
||||
public static WebhookEventCollectionBuilderCms RemoveDefault(this WebhookEventCollectionBuilderCms builder)
|
||||
{
|
||||
foreach (Type type in _defaultTypes)
|
||||
{
|
||||
builder.Builder.Remove(type);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<Description>Adds Examine searching support using Lucene to Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Infrastructure.Examine</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [CS0618] Handle member obsolete appropriately and remove this override -->
|
||||
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Examine" />
|
||||
<!-- Take top-level depedendency on System.Security.Cryptography.Xml, because Examine depends on a vulnerable version -->
|
||||
|
||||
@@ -10,13 +10,15 @@ public interface IRecurringBackgroundJob
|
||||
static readonly TimeSpan DefaultDelay = System.TimeSpan.FromMinutes(3);
|
||||
static readonly ServerRole[] DefaultServerRoles = new[] { ServerRole.Single, ServerRole.SchedulingPublisher };
|
||||
|
||||
/// <param name="period">Timespan representing how often the task should recur.</param>
|
||||
/// <summary>
|
||||
/// Timespan representing how often the task should recur.
|
||||
/// </summary>
|
||||
TimeSpan Period { get; }
|
||||
|
||||
/// <param name="delay">
|
||||
/// <summary>
|
||||
/// Timespan representing the initial delay after application start-up before the first run of the task
|
||||
/// occurs.
|
||||
/// </param>
|
||||
/// </summary>
|
||||
TimeSpan Delay { get => DefaultDelay; }
|
||||
|
||||
ServerRole[] ServerRoles { get => DefaultServerRoles; }
|
||||
|
||||
@@ -47,6 +47,7 @@ public class HealthCheckNotifierJob : IRecurringBackgroundJob
|
||||
/// <param name="logger">The typed logger.</param>
|
||||
/// <param name="profilingLogger">The profiling logger.</param>
|
||||
/// <param name="cronTabParser">Parser of crontab expressions.</param>
|
||||
/// <param name="eventAggregator"></param>
|
||||
public HealthCheckNotifierJob(
|
||||
IOptionsMonitor<HealthChecksSettings> healthChecksSettings,
|
||||
HealthCheckCollection healthChecks,
|
||||
|
||||
@@ -113,7 +113,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
factory.GetRequiredService<IShortStringHelper>(),
|
||||
factory.GetRequiredService<IConfigurationEditorJsonSerializer>(),
|
||||
factory.GetRequiredService<IMediaService>(),
|
||||
factory.GetRequiredService<IMediaTypeService>());
|
||||
factory.GetRequiredService<IMediaTypeService>(),
|
||||
factory.GetRequiredService<ITemplateContentParserService>(),
|
||||
factory.GetRequiredService<ITemplateService>());
|
||||
|
||||
private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(
|
||||
IServiceProvider container)
|
||||
|
||||
@@ -91,10 +91,7 @@ public class FilePermissionHelper : IFilePermissionHelper
|
||||
continue;
|
||||
}
|
||||
|
||||
if (temp == null)
|
||||
{
|
||||
temp = new List<string>();
|
||||
}
|
||||
temp ??= new List<string>();
|
||||
|
||||
temp.Add(dir.TrimStartExact(_basePath));
|
||||
success = false;
|
||||
@@ -116,10 +113,7 @@ public class FilePermissionHelper : IFilePermissionHelper
|
||||
continue;
|
||||
}
|
||||
|
||||
if (temp == null)
|
||||
{
|
||||
temp = new List<string>();
|
||||
}
|
||||
temp ??= new List<string>();
|
||||
|
||||
temp.Add(file.TrimStartExact(_basePath));
|
||||
success = false;
|
||||
@@ -144,10 +138,7 @@ public class FilePermissionHelper : IFilePermissionHelper
|
||||
continue;
|
||||
}
|
||||
|
||||
if (temp == null)
|
||||
{
|
||||
temp = new List<string>();
|
||||
}
|
||||
temp ??= new List<string>();
|
||||
|
||||
temp.Add(dir);
|
||||
success = false;
|
||||
|
||||
@@ -49,7 +49,5 @@ public class LogViewerConfig : ILogViewerConfig
|
||||
IReadOnlyList<SavedLogSearch> result = GetSavedSearches()!;
|
||||
scope.Complete();
|
||||
return result;
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,15 @@ public class UmbracoPlan : MigrationPlan
|
||||
|
||||
// To 14.0.0
|
||||
To<V_14_0_0.AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
|
||||
To<V_14_0_0.AddGuidsToUserGroups>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
|
||||
To<V_14_0_0.AddUserGroup2PermisionTable>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
|
||||
To<V_14_0_0.AddGuidsToUsers>("{A8E01644-9F2E-4988-8341-587EF5B7EA69}");
|
||||
To<NoopMigration>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
|
||||
To<NoopMigration>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
|
||||
To<NoopMigration>("{A8E01644-9F2E-4988-8341-587EF5B7EA69}");
|
||||
To<V_14_0_0.UpdateDefaultGuidsOfCreatedPackages>("{E073DBC0-9E8E-4C92-8210-9CB18364F46E}");
|
||||
To<V_14_0_0.RenameTechnologyLeakingPropertyEditorAliases>("{80D282A4-5497-47FF-991F-BC0BCE603121}");
|
||||
To<V_14_0_0.MigrateSchduledPublishesToUtc>("{96525697-E9DC-4198-B136-25AD033442B8}");
|
||||
To<V_14_0_0.AddListViewKeysToDocumentTypes>("{7FC5AC9B-6F56-415B-913E-4A900629B853}");
|
||||
To<V_14_0_0.MigrateDataTypeConfigurations>("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}");
|
||||
To<V_14_0_0.MigrateCharPermissionsToStrings>("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}");
|
||||
To<NoopMigration>("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}");
|
||||
To<V_14_0_0.DeleteMacroTables>("{0D82C836-96DD-480D-A924-7964E458BD34}");
|
||||
To<V_14_0_0.MoveDocumentBlueprintsToFolders>("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}");
|
||||
To<V_14_0_0.MigrateTours>("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}");
|
||||
|
||||
@@ -12,7 +12,6 @@ public class UmbracoPremigrationPlan : MigrationPlan
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoPlan" /> class.
|
||||
/// </summary>
|
||||
/// <param name="umbracoVersion">The Umbraco version.</param>
|
||||
public UmbracoPremigrationPlan()
|
||||
: base(Constants.Conventions.Migrations.UmbracoUpgradePlanPremigrationsName)
|
||||
=> DefinePlan();
|
||||
@@ -54,5 +53,9 @@ public class UmbracoPremigrationPlan : MigrationPlan
|
||||
|
||||
// To 14.0.0
|
||||
To<V_14_0_0.UpdateToOpenIddictV5>("{76FBF80E-37E6-462E-ADC1-25668F56151D}");
|
||||
To<V_14_0_0.AddGuidsToUserGroups>("{37CF4AC3-8489-44BC-A7E8-64908FEEC656}");
|
||||
To<V_14_0_0.AddUserGroup2PermisionTable>("{7BCB5352-B2ED-4D4B-B27D-ECDED930B50A}");
|
||||
To<V_14_0_0.AddGuidsToUsers>("{3E69BF9B-BEAB-41B1-BB11-15383CCA1C7F}");
|
||||
To<V_14_0_0.MigrateCharPermissionsToStrings>("{F12C609B-86B9-4386-AFA4-78E02857247C}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
// If the new column already exists we'll do nothing.
|
||||
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// SQL server can simply add the column, but for SQLite this won't work,
|
||||
// so we'll have to create a new table and copy over data.
|
||||
if (DatabaseType != DatabaseType.SQLite)
|
||||
@@ -37,11 +43,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
|
||||
using IDisposable notificationSuppression = scope.Notifications.Suppress();
|
||||
ScopeDatabase(scope);
|
||||
|
||||
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
|
||||
AddColumnIfNotExists<UserGroupDto>(columns, NewColumnName);
|
||||
|
||||
@@ -68,12 +69,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
|
||||
using IDisposable notificationSuppression = scope.Notifications.Suppress();
|
||||
ScopeDatabase(scope);
|
||||
|
||||
// If the new column already exists we'll do nothing.
|
||||
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This isn't pretty,
|
||||
// But since you cannot alter columns, we have to copy the data over and delete the old table.
|
||||
// However we cannot do this due to foreign keys, so temporarily disable these keys while migrating.
|
||||
|
||||
@@ -26,6 +26,12 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName))
|
||||
{
|
||||
Context.Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidateBackofficeUserAccess = true;
|
||||
using IScope scope = _scopeProvider.CreateScope();
|
||||
using IDisposable notificationSuppression = scope.Notifications.Suppress();
|
||||
@@ -75,11 +81,6 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
|
||||
|
||||
private void MigrateSqlite()
|
||||
{
|
||||
if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We commit the initial transaction started by the scope. This is required in order to disable the foreign keys.
|
||||
* We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal.
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Collections;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
@@ -34,6 +36,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
private readonly IConfigurationEditorJsonSerializer _serializer;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly ITemplateContentParserService _templateContentParserService;
|
||||
private readonly ITemplateService _templateService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IContentService _contentService;
|
||||
@@ -52,7 +56,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
IShortStringHelper shortStringHelper,
|
||||
IConfigurationEditorJsonSerializer serializer,
|
||||
IMediaService mediaService,
|
||||
IMediaTypeService mediaTypeService)
|
||||
IMediaTypeService mediaTypeService,
|
||||
ITemplateContentParserService templateContentParserService,
|
||||
ITemplateService templateService)
|
||||
{
|
||||
_dataValueEditorFactory = dataValueEditorFactory;
|
||||
_logger = logger;
|
||||
@@ -68,6 +74,44 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
_serializer = serializer;
|
||||
_mediaService = mediaService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_templateContentParserService = templateContentParserService;
|
||||
_templateService = templateService;
|
||||
}
|
||||
|
||||
[Obsolete("Please use new constructor, scheduled for removal in v15")]
|
||||
public PackageDataInstallation(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
ILogger<PackageDataInstallation> logger,
|
||||
IFileService fileService,
|
||||
ILocalizationService localizationService,
|
||||
IDataTypeService dataTypeService,
|
||||
IEntityService entityService,
|
||||
IContentTypeService contentTypeService,
|
||||
IContentService contentService,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
IScopeProvider scopeProvider,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IConfigurationEditorJsonSerializer serializer,
|
||||
IMediaService mediaService,
|
||||
IMediaTypeService mediaTypeService)
|
||||
: this(
|
||||
dataValueEditorFactory,
|
||||
logger,
|
||||
fileService,
|
||||
localizationService,
|
||||
dataTypeService,
|
||||
entityService,
|
||||
contentTypeService,
|
||||
contentService,
|
||||
propertyEditors,
|
||||
scopeProvider,
|
||||
shortStringHelper,
|
||||
serializer,
|
||||
mediaService,
|
||||
mediaTypeService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITemplateContentParserService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITemplateService>())
|
||||
{
|
||||
}
|
||||
|
||||
// Also remove factory service registration when this constructor is removed
|
||||
@@ -103,7 +147,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
shortStringHelper,
|
||||
serializer,
|
||||
mediaService,
|
||||
mediaTypeService)
|
||||
mediaTypeService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITemplateContentParserService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITemplateService>())
|
||||
{ }
|
||||
|
||||
#region Install/Uninstall
|
||||
@@ -1651,16 +1697,25 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
|
||||
#region Templates
|
||||
|
||||
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
|
||||
public IEnumerable<ITemplate> ImportTemplate(XElement templateElement, int userId)
|
||||
=> ImportTemplates(new[] {templateElement}, userId);
|
||||
|
||||
public async Task<IEnumerable<ITemplate>> ImportTemplateAsync(XElement templateElement, int userId)
|
||||
=> ImportTemplatesAsync(new[] {templateElement}, userId).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
|
||||
public IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
|
||||
=> ImportTemplatesAsync(templateElements, userId).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// Imports and saves package xml as <see cref="ITemplate"/>
|
||||
/// </summary>
|
||||
/// <param name="templateElements">Xml to import</param>
|
||||
/// <param name="userId">Optional user id</param>
|
||||
/// <returns>An enumerable list of generated Templates</returns>
|
||||
public IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
|
||||
public async Task<IReadOnlyList<ITemplate>> ImportTemplatesAsync(IReadOnlyCollection<XElement> templateElements, int userId)
|
||||
{
|
||||
var templates = new List<ITemplate>();
|
||||
|
||||
@@ -1670,20 +1725,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
{
|
||||
var dependencies = new List<string>();
|
||||
XElement elementCopy = tempElement;
|
||||
//Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting.
|
||||
if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false &&
|
||||
templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master")))
|
||||
|
||||
//Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting.'
|
||||
var masterTemplate = _templateContentParserService.MasterTemplateAlias(tempElement.Value);
|
||||
if (masterTemplate is not null && templateElements.Any(x => (string?)x.Element("Alias") == masterTemplate))
|
||||
{
|
||||
dependencies.Add((string)elementCopy.Element("Master")!);
|
||||
dependencies.Add(masterTemplate);
|
||||
}
|
||||
else if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false &&
|
||||
templateElements.Any(x =>
|
||||
(string?)x.Element("Alias") == (string?)elementCopy.Element("Master")) == false)
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.",
|
||||
(string?)elementCopy.Element("Alias"),
|
||||
(string?)elementCopy.Element("Master"));
|
||||
masterTemplate);
|
||||
}
|
||||
|
||||
graph.AddItem(TopoGraph.CreateNode((string)elementCopy.Element("Alias")!, elementCopy, dependencies));
|
||||
@@ -1700,9 +1754,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
var design = templateElement.Element("Design")?.Value;
|
||||
XElement? masterElement = templateElement.Element("Master");
|
||||
|
||||
var existingTemplate = _fileService.GetTemplate(alias) as Template;
|
||||
var existingTemplate = await _templateService.GetAsync(alias) as Template;
|
||||
|
||||
Template? template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias);
|
||||
Template template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias);
|
||||
|
||||
// For new templates, use the serialized key if avaialble.
|
||||
if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key))
|
||||
@@ -1725,9 +1779,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging
|
||||
templates.Add(template);
|
||||
}
|
||||
|
||||
if (templates.Any())
|
||||
foreach (ITemplate template in templates)
|
||||
{
|
||||
_fileService.SaveTemplate(templates, userId);
|
||||
if (template.Id > 0)
|
||||
{
|
||||
await _templateService.UpdateAsync(template, Constants.Security.SuperUserKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _templateService.CreateAsync(template, Constants.Security.SuperUserKey);
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
|
||||
@@ -55,6 +55,6 @@ internal class LogDto
|
||||
/// </summary>
|
||||
[Column("parameters")]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
[Length(500)]
|
||||
[Length(4000)]
|
||||
public string? Parameters { get; set; }
|
||||
}
|
||||
|
||||
@@ -411,8 +411,8 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
||||
List<int> userIds = dtos.Count == 1 ? new List<int> {dtos[0].Id} : dtos.Select(x => x.Id).ToList();
|
||||
Dictionary<int, UserDto>? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x);
|
||||
|
||||
List<int> groupIds = new List<int>();
|
||||
List<Guid> groupKeys = new List<Guid>();
|
||||
var groupIds = new List<int>();
|
||||
var groupKeys = new List<Guid>();
|
||||
Sql<ISqlContext> sql;
|
||||
try
|
||||
{
|
||||
@@ -595,9 +595,9 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
||||
|
||||
// map languages
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
foreach (UserGroupDto group in groups.Values)
|
||||
{
|
||||
if (groups2languages.TryGetValue(group.Id, out var list))
|
||||
if (groups2languages.TryGetValue(group.Id, out IGrouping<int, UserGroup2LanguageDto>? list))
|
||||
{
|
||||
group.UserGroup2LanguageDtos = list.ToList(); // groups2apps is distinct
|
||||
}
|
||||
|
||||
@@ -197,7 +197,6 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
|
||||
/// Returns the "src" property from the json structure if the value is formatted correctly
|
||||
/// </summary>
|
||||
/// <param name="propVal"></param>
|
||||
/// <param name="deserializedValue">The deserialized <see cref="JObject" /> value</param>
|
||||
/// <param name="relative">Should the path returned be the application relative path</param>
|
||||
/// <returns></returns>
|
||||
private string? GetFileSrcFromPropertyValue(object? propVal, bool relative = true)
|
||||
|
||||
@@ -17,6 +17,7 @@ public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory<BackO
|
||||
/// </summary>
|
||||
/// <param name="userManager">The user manager</param>
|
||||
/// <param name="optionsAccessor">The <see cref="BackOfficeIdentityOptions" /></param>
|
||||
/// <param name="backOfficeAuthenticationTypeSettings"></param>
|
||||
public BackOfficeClaimsPrincipalFactory(
|
||||
UserManager<BackOfficeIdentityUser> userManager,
|
||||
IOptions<BackOfficeIdentityOptions> optionsAccessor,
|
||||
|
||||
@@ -63,10 +63,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
|
||||
get => _startContentIds;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
value = new int[0];
|
||||
}
|
||||
value ??= new int[0];
|
||||
|
||||
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds!, nameof(StartContentIds), _startIdsComparer);
|
||||
}
|
||||
@@ -80,10 +77,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
|
||||
get => _startMediaIds;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
value = new int[0];
|
||||
}
|
||||
value ??= Array.Empty<int>();
|
||||
|
||||
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds!, nameof(StartMediaIds), _startIdsComparer);
|
||||
}
|
||||
@@ -130,6 +124,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser
|
||||
/// <param name="email">This is allowed to be null (but would need to be filled in if trying to persist this instance)</param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="id"></param>
|
||||
public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null, UserKind kind = UserKind.Default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
@@ -354,8 +357,16 @@ public class PackagingService : IPackagingService
|
||||
|
||||
if (!string.IsNullOrEmpty(packageManifest.Version))
|
||||
{
|
||||
// Always use package version from manifest
|
||||
installedPackage.Version = packageManifest.Version;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(installedPackage.Version) &&
|
||||
string.IsNullOrEmpty(installedPackage.PackageId) is false &&
|
||||
TryGetAssemblyInformationalVersion(installedPackage.PackageId, out string? version))
|
||||
{
|
||||
// Use version of the assembly with the same name as the package ID
|
||||
installedPackage.Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
// Return all packages with an ID or name in the package manifest or package migrations
|
||||
@@ -414,4 +425,20 @@ public class PackagingService : IPackagingService
|
||||
|
||||
return packageFile.CreateReadStream();
|
||||
}
|
||||
|
||||
private static bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version)
|
||||
{
|
||||
foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies)
|
||||
{
|
||||
AssemblyName assemblyName = assembly.GetName();
|
||||
if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) &&
|
||||
assembly.TryGetInformationalVersion(out version))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
version = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<Description>Contains the infrastructure assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Infrastructure</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>$(DefineConstants);TRACE_SCOPES</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<Description>Contains the published cache assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Infrastructure.PublishedCache</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Umbraco.CSharpTest.Net.Collections" />
|
||||
<PackageReference Include="MessagePack" />
|
||||
|
||||
@@ -165,7 +165,8 @@ public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
|
||||
public override async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
|
||||
{
|
||||
// override to handle logging/events
|
||||
TUser? user = await UserManager.FindByNameAsync(userName);
|
||||
string strippedUsername = userName.Trim();
|
||||
TUser? user = await UserManager.FindByNameAsync(strippedUsername);
|
||||
if (user == null)
|
||||
{
|
||||
return await HandleSignIn(null, userName, SignInResult.Failed);
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<Description>Contains the web assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Web.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Dictionary;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common;
|
||||
|
||||
@@ -18,7 +19,7 @@ public class UmbracoHelper
|
||||
private readonly IUmbracoComponentRenderer _componentRenderer;
|
||||
private readonly ICultureDictionaryFactory _cultureDictionaryFactory;
|
||||
private readonly IPublishedContentQuery _publishedContentQuery;
|
||||
private ICultureDictionary? _cultureDictionary;
|
||||
private readonly Dictionary<CultureInfo, ICultureDictionary> _cultureDictionaries = [];
|
||||
|
||||
private IPublishedContent? _currentPage;
|
||||
|
||||
@@ -103,29 +104,29 @@ public class UmbracoHelper
|
||||
/// <summary>
|
||||
/// Returns the dictionary value for the key specified
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public string? GetDictionaryValue(string key) => CultureDictionary[key];
|
||||
/// <param name="key">Key of dictionary item.</param>
|
||||
/// <returns>The dictionary value, should one exist.</returns>
|
||||
public string? GetDictionaryValue(string key) => GetDictionaryValue(key, Thread.CurrentThread.CurrentUICulture);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
|
||||
/// </summary>
|
||||
/// <param name="key">key of dictionary item</param>
|
||||
/// <param name="key">Key of dictionary item.</param>
|
||||
/// <param name="specificCulture">the specific culture on which the result well be back upon</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>The dictionary value, should one exist.</returns>
|
||||
public string? GetDictionaryValue(string key, CultureInfo specificCulture)
|
||||
{
|
||||
_cultureDictionary = _cultureDictionaryFactory.CreateDictionary(specificCulture);
|
||||
return GetDictionaryValue(key);
|
||||
ICultureDictionary cultureDictionary = GetCultureDictionary(specificCulture);
|
||||
return cultureDictionary[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
|
||||
/// </summary>
|
||||
/// <param name="key">key of dictionary item</param>
|
||||
/// <param name="defaultValue">fall back text if dictionary item is empty - Name altText to match Umbraco.Field</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">key of dictionary item.</param>
|
||||
/// <param name="defaultValue">fall back text if dictionary item is empty - Name altText to match Umbraco.Field.</param>
|
||||
/// <returns>Returns the dictionary value, or a default value if none exists.</returns>
|
||||
public string GetDictionaryValueOrDefault(string key, string defaultValue)
|
||||
{
|
||||
var dictionaryValue = GetDictionaryValue(key);
|
||||
@@ -140,26 +141,51 @@ public class UmbracoHelper
|
||||
/// <summary>
|
||||
/// Returns the dictionary value for the key specified, and if empty returns the specified default fall back value
|
||||
/// </summary>
|
||||
/// <param name="key">key of dictionary item</param>
|
||||
/// <param name="specificCulture">the specific culture on which the result well be back upon</param>
|
||||
/// <param name="defaultValue">fall back text if dictionary item is empty - Name altText to match Umbraco.Field</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">Key of dictionary item.</param>
|
||||
/// <param name="specificCulture">The specific culture on which the result well be back upon.</param>
|
||||
/// <param name="defaultValue">Fall back text if dictionary item is empty - Name altText to match Umbraco.Field.</param>
|
||||
/// <returns>Returns the dictionary value, or a default value if none exists.</returns>
|
||||
public string GetDictionaryValueOrDefault(string key, CultureInfo specificCulture, string defaultValue)
|
||||
{
|
||||
_cultureDictionary = _cultureDictionaryFactory.CreateDictionary(specificCulture);
|
||||
var dictionaryValue = GetDictionaryValue(key);
|
||||
var dictionaryValue = GetDictionaryValue(key, specificCulture);
|
||||
if (string.IsNullOrWhiteSpace(dictionaryValue))
|
||||
{
|
||||
dictionaryValue = defaultValue;
|
||||
}
|
||||
|
||||
return dictionaryValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ICultureDictionary for the current UI Culture for access to dictionary items
|
||||
/// </summary>
|
||||
public ICultureDictionary CultureDictionary => GetCultureDictionary(Thread.CurrentThread.CurrentUICulture);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ICultureDictionary for access to dictionary items
|
||||
/// Gets the ICultureDictionary for access to dictionary items for a specific culture
|
||||
/// </summary>
|
||||
public ICultureDictionary CultureDictionary => _cultureDictionary ??= _cultureDictionaryFactory.CreateDictionary();
|
||||
/// <param name="specificCulture">The culture of the culture dictionary you want to retrieve.</param>
|
||||
/// <returns>Returns the culture dictionary for the specified culture.</returns>
|
||||
public ICultureDictionary GetCultureDictionary(CultureInfo specificCulture)
|
||||
{
|
||||
CreateCultureDictionary(specificCulture);
|
||||
return _cultureDictionaries.GetValue(specificCulture)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a culture dictionary for a specific culture if it doesn't already exist
|
||||
/// </summary>
|
||||
/// <param name="specificCulture">The culture to create a culture dictionary for.</param>
|
||||
internal void CreateCultureDictionary(CultureInfo specificCulture)
|
||||
{
|
||||
if (_cultureDictionaries.ContainsKey(specificCulture))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ICultureDictionary dictionary = _cultureDictionaryFactory.CreateDictionary(specificCulture);
|
||||
_cultureDictionaries.Add(specificCulture, dictionary);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@umbraco-cms/backoffice": "^14.2.0",
|
||||
"msw": "^2.3.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite": "^5.4.6",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"msw": {
|
||||
|
||||
@@ -13,7 +13,7 @@ WebApplication app = builder.Build();
|
||||
|
||||
await app.BootUmbracoAsync();
|
||||
|
||||
#if (UseHttpsRedirect)
|
||||
#if UseHttpsRedirect
|
||||
app.UseHttpsRedirection();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [SA1119] Correct unnecessary parenthesis and remove this override -->
|
||||
<WarningsNotAsErrors>SA1119</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\Umbraco.Cms.Targets\buildTransitive\Umbraco.Cms.Targets.props" />
|
||||
<Import Project="..\Umbraco.Cms.Targets\buildTransitive\Umbraco.Cms.Targets.targets" />
|
||||
<ItemGroup>
|
||||
|
||||
@@ -57,8 +57,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
x.GetRequiredService<IControllerActionSearcher>(),
|
||||
x.GetRequiredService<IPublicAccessRequestHandler>(),
|
||||
x.GetRequiredService<IUmbracoVirtualPageRoute>(),
|
||||
x.GetRequiredService<IOptionsMonitor<GlobalSettings>>()
|
||||
));
|
||||
x.GetRequiredService<IOptionsMonitor<GlobalSettings>>()));
|
||||
builder.Services.AddSingleton<IControllerActionSearcher, ControllerActionSearcher>();
|
||||
builder.Services.TryAddEnumerable(Singleton<MatcherPolicy, NotFoundSelectorPolicy>());
|
||||
builder.Services.AddSingleton<IUmbracoVirtualPageRoute, UmbracoVirtualPageRoute>();
|
||||
|
||||
@@ -148,9 +148,10 @@ public class PublicAccessRequestHandler : IPublicAccessRequestHandler
|
||||
_logger.LogDebug("EnsurePublishedContentAccess: Page is not protected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop until we have access or reached max loops
|
||||
} while (publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop);
|
||||
while (publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop);
|
||||
|
||||
if (i == maxLoop)
|
||||
{
|
||||
|
||||
@@ -45,13 +45,8 @@ public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
|
||||
ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find<IRenderController>(
|
||||
new DefaultHttpContext(), // this actually makes no difference for this method
|
||||
DefaultControllerName,
|
||||
UmbracoRouteValues.DefaultActionName);
|
||||
|
||||
if (descriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
UmbracoRouteValues.DefaultActionName) ?? throw new InvalidOperationException(
|
||||
$"No controller/action found by name {DefaultControllerName}.{UmbracoRouteValues.DefaultActionName}");
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
});
|
||||
@@ -128,11 +123,12 @@ public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
|
||||
IPublishedRequest request = def.PublishedRequest;
|
||||
|
||||
// Here we need to check if there is no hijacked route and no template assigned but there is a content item.
|
||||
// If this is the case we want to return a blank page.
|
||||
// If this is the case we want to return a blank page, the only exception being if the content item has a redirect field present.
|
||||
// We also check if templates have been disabled since if they are then we're allowed to render even though there's no template,
|
||||
// for example for json rendering in headless.
|
||||
if (request.HasPublishedContent()
|
||||
&& !request.HasTemplate()
|
||||
&& !request.IsRedirect()
|
||||
&& !_umbracoFeatures.Disabled.DisableTemplates
|
||||
&& !hasHijackedRoute)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
<Description>Contains the website assembly needed to run the frontend of Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Web.Website</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers,
|
||||
[CS0618] handle member obsolete appropriately, [SA1401] make fields private,
|
||||
[SA1649] update file name, and remove this override, [IDE1006] fix namin rule violation,
|
||||
and remove override -->
|
||||
<WarningsNotAsErrors>ASP0019,CS0618,SA1401,SA1649,IDE0270,IDE1006</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,6 +8,8 @@ public class ProfilingViewEngine : IViewEngine
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly IProfiler _profiler;
|
||||
|
||||
//TODO: can this be made private and with underscore?
|
||||
internal readonly IViewEngine Inner;
|
||||
|
||||
public ProfilingViewEngine(IViewEngine inner, IProfiler profiler)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
{
|
||||
"condition": "(UmbracoRelease == 'LTS')",
|
||||
"value": "13.4.1"
|
||||
"value": "13.5.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [CS0618] handle member obsolete appropriately, [IDE1006] fix naming rule violation,
|
||||
[IDE0057] simplify substring and remove this override -->
|
||||
<WarningsNotAsErrors>CS0618,IDE1006,IDE0057</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" />
|
||||
</ItemGroup>
|
||||
|
||||
2
tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/.gitignore
vendored
Normal file
2
tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ignore Visual Studio's generated launchSettings file.
|
||||
Properties/launchSettings.json
|
||||
BIN
tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Ogv.ogv
Normal file
BIN
tests/Umbraco.Tests.AcceptanceTest/fixtures/mediaLibrary/Ogv.ogv
Normal file
Binary file not shown.
Binary file not shown.
18
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
18
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
@@ -7,8 +7,8 @@
|
||||
"name": "acceptancetest",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.17",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.82",
|
||||
"@umbraco/json-models-builders": "^2.0.20",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.84",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
@@ -55,21 +55,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/json-models-builders": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.18.tgz",
|
||||
"integrity": "sha512-VC2KCuWVhae0HzVpo9RrOQt6zZSQqSpWqwCoKYYwmhRz/SYo6hARV6sH2ceEFsQwGqqJvakXuUWzlJK7bFqK1Q==",
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.20.tgz",
|
||||
"integrity": "sha512-LmTtklne1HlhMr1nALA+P5FrjIC9jL3A6Pcxj4dy+IPnTgnU2vMYaQIfE8wwz5Z5fZ5AAhWx/Zpdi8xCTbVSuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "2.0.0-beta.82",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.82.tgz",
|
||||
"integrity": "sha512-VkArVyvkKuTwJJH8eCHSvbho4H1Owx2ifidVuPyN8EVGDWbxOTb5i9jmtFjJnfDg9mg50JhRYKas4lUGvy1pBA==",
|
||||
"version": "2.0.0-beta.84",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.84.tgz",
|
||||
"integrity": "sha512-vH13Lg48knTkkLVTwhMXUKTOdjtmixFj0wF5Qhgb++13u4AVDb+oW+TbFwTjSYaLeNMraq5Uhwmto/XuJPs2Rw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.18",
|
||||
"@umbraco/json-models-builders": "2.0.20",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"typescript": "^4.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.17",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.82",
|
||||
"@umbraco/json-models-builders": "^2.0.20",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.84",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
|
||||
import {expect} from "@playwright/test";
|
||||
|
||||
const contentName = 'TestContent';
|
||||
const documentTypeName = 'TestDocumentTypeForContent';
|
||||
const dataTypeName = 'Upload Vector Graphics';
|
||||
const uploadVectorGraphicsPath = './fixtures/mediaLibrary/';
|
||||
|
||||
test.beforeEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
});
|
||||
|
||||
test.afterEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
});
|
||||
|
||||
test('can create content with the upload vector graphics data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const expectedState = 'Draft';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuAtRoot();
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.variants[0].state).toBe(expectedState);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
test('can publish content with the upload vector graphics data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const expectedState = 'Published';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickSaveAndPublishButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.variants[0].state).toBe(expectedState);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
test(`can upload a file with the svg extension in the content`, async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const vectorGraphicsName = 'VectorGraphics.svg';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.uploadFile(uploadVectorGraphicsPath + vectorGraphicsName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName));
|
||||
expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(vectorGraphicsName));
|
||||
});
|
||||
|
||||
// TODO: Remove skip when the front-end is ready. Currently the uploaded vector graphics file still displays after removing.
|
||||
test.skip('can remove an svg file in the content', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const uploadVectorGraphicsName = 'VectorGraphics.svg';
|
||||
const mineType = 'image/svg+xml';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadVectorGraphicsName, mineType);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickRemoveFilesButton();
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
|
||||
import {expect} from "@playwright/test";
|
||||
|
||||
const contentName = 'TestContent';
|
||||
const documentTypeName = 'TestDocumentTypeForContent';
|
||||
const dataTypeName = 'Upload Video';
|
||||
const uploadVideoPath = './fixtures/mediaLibrary/';
|
||||
|
||||
test.beforeEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
});
|
||||
|
||||
test.afterEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.document.ensureNameNotExists(contentName);
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
});
|
||||
|
||||
test('can create content with the upload video data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const expectedState = 'Draft';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuAtRoot();
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.variants[0].state).toBe(expectedState);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
test('can publish content with the upload video data type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const expectedState = 'Published';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickSaveAndPublishButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.variants[0].state).toBe(expectedState);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
|
||||
const uploadVideos = [
|
||||
{fileExtension: 'mp4', fileName: 'Video.mp4'},
|
||||
{fileExtension: 'webm', fileName: 'Webm.webm'},
|
||||
{fileExtension: 'ogv', fileName: 'Ogv.ogv'}
|
||||
];
|
||||
for (const uploadVideo of uploadVideos) {
|
||||
test(`can upload a video with the ${uploadVideo.fileExtension} extension in the content`, async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.uploadFile(uploadVideoPath + uploadVideo.fileName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName));
|
||||
expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(uploadVideo.fileName));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Remove skip when the front-end is ready. Currently the uploaded video still displays after removing.
|
||||
test.skip('can remove a mp4 file in the content', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const uploadFileName = 'Video.mp4';
|
||||
const mineType = 'video/mp4';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
await umbracoApi.document.createDocumentWithUploadFile(contentName, documentTypeId, dataTypeName, uploadFileName, mineType);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickRemoveFilesButton();
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
// Assert
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
|
||||
const contentData = await umbracoApi.document.getByName(contentName);
|
||||
expect(contentData.values).toEqual([]);
|
||||
});
|
||||
@@ -11,7 +11,7 @@ public class EnumeratorBenchmarks
|
||||
{
|
||||
foreach (var t in EnumerateOneWithArray(1))
|
||||
{
|
||||
;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class EnumeratorBenchmarks
|
||||
{
|
||||
foreach (var t in EnumerateOneWithYield(1))
|
||||
{
|
||||
;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" />
|
||||
|
||||
@@ -53,6 +53,7 @@ public class ContentBuilder
|
||||
private int? _sortOrder;
|
||||
private bool? _trashed;
|
||||
private DateTime? _updateDate;
|
||||
private bool? _blueprint;
|
||||
private int? _versionId;
|
||||
|
||||
DateTime? IWithCreateDateBuilder.CreateDate
|
||||
@@ -145,6 +146,13 @@ public class ContentBuilder
|
||||
set => _updateDate = value;
|
||||
}
|
||||
|
||||
public ContentBuilder WithBlueprint(bool blueprint)
|
||||
{
|
||||
_blueprint = blueprint;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentBuilder WithVersionId(int versionId)
|
||||
{
|
||||
_versionId = versionId;
|
||||
@@ -217,6 +225,7 @@ public class ContentBuilder
|
||||
{
|
||||
var id = _id ?? 0;
|
||||
var versionId = _versionId ?? 0;
|
||||
var blueprint = _blueprint ?? false;
|
||||
var key = _key ?? Guid.NewGuid();
|
||||
var parentId = _parentId ?? -1;
|
||||
var parent = _parent;
|
||||
@@ -253,6 +262,7 @@ public class ContentBuilder
|
||||
|
||||
content.Id = id;
|
||||
content.VersionId = versionId;
|
||||
content.Blueprint = blueprint;
|
||||
content.Key = key;
|
||||
content.CreateDate = createDate;
|
||||
content.UpdateDate = updateDate;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user