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:
Bjarke Berg
2024-09-26 08:56:43 +02:00
116 changed files with 1433 additions and 603 deletions

View File

@@ -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>

View File

@@ -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:

View File

@@ -4,12 +4,12 @@ pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
variables:
nodeVersion: 20
@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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
{

View File

@@ -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)]

View File

@@ -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

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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)" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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 =>
{

View File

@@ -3,8 +3,12 @@
<Title>Umbraco CMS - Persistence - Entity Framework Core</Title>
<Description>Adds support for Entity Framework Core to Umbraco CMS.</Description>
</PropertyGroup>
<ItemGroup>
<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" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />

View File

@@ -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);

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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" />
@@ -71,4 +76,4 @@
</ItemGroup>
<RemoveDir Directories="@(LoginDirectories)" />
</Target>
</Project>
</Project>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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",
};
}
}

View File

@@ -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
{

View File

@@ -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>

View File

@@ -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.

View File

@@ -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();
}
}

View 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;
}

View File

@@ -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)
{
@@ -63,7 +63,7 @@ public class DisposableTimer : DisposableObjectSlim
var args = new object[startMessageArgs.Length + 1];
startMessageArgs.CopyTo(args, 0);
args[^1] = _timingId;
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
logger.LogDebug(startMessage + " [Timing {TimingId}]", args);

View File

@@ -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;
}

View File

@@ -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; }

View File

@@ -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(

View File

@@ -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">

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -330,7 +330,7 @@ namespace Umbraco.Cms.Core.Services.Implement
return Task.FromResult(dataTypes);
}
/// <inheritdoc />
public async Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias)
{
@@ -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();

View File

@@ -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)
{

View File

@@ -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,

View File

@@ -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);

View File

@@ -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)));
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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">

View File

@@ -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(

View File

@@ -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)
{

View File

@@ -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,

View File

@@ -1,7 +1,7 @@
namespace Umbraco.Cms.Core.Services.Querying.RecycleBin;
/// <summary>
/// < 10 = Success
/// &lt;10 = Success.
/// </summary>
public enum RecycleBinQueryResultType
{

View File

@@ -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,9 +142,9 @@ 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);
}
}
}
}

View File

@@ -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 });
}

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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;
}

View File

@@ -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 -->

View File

@@ -8,15 +8,17 @@ namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
public interface IRecurringBackgroundJob
{
static readonly TimeSpan DefaultDelay = System.TimeSpan.FromMinutes(3);
static readonly ServerRole[] DefaultServerRoles = new[] { ServerRole.Single, ServerRole.SchedulingPublisher };
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">
/// Timespan representing the initial delay after application start-up before the first run of the task
/// occurs.
/// </param>
/// <summary>
/// Timespan representing the initial delay after application start-up before the first run of the task
/// occurs.
/// </summary>
TimeSpan Delay { get => DefaultDelay; }
ServerRole[] ServerRoles { get => DefaultServerRoles; }

View File

@@ -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,

View File

@@ -30,7 +30,7 @@ public class LogScrubberJob : IRecurringBackgroundJob
private readonly IAuditService _auditService;
private readonly ILogger<LogScrubberJob> _logger;
private readonly IProfilingLogger _profilingLogger;
private readonly ICoreScopeProvider _scopeProvider;
private readonly ICoreScopeProvider _scopeProvider;
private LoggingSettings _settings;
/// <summary>
@@ -41,14 +41,14 @@ public class LogScrubberJob : IRecurringBackgroundJob
/// <param name="scopeProvider">Provides scopes for database operations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
public LogScrubberJob(
public LogScrubberJob(
IAuditService auditService,
IOptionsMonitor<LoggingSettings> settings,
ICoreScopeProvider scopeProvider,
ILogger<LogScrubberJob> logger,
IProfilingLogger profilingLogger)
{
_auditService = auditService;
_settings = settings.CurrentValue;
_scopeProvider = scopeProvider;
@@ -59,7 +59,7 @@ public class LogScrubberJob : IRecurringBackgroundJob
public Task RunJobAsync()
{
// Ensure we use an explicit scope since we are running on a background thread.
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
using (_profilingLogger.DebugDuration<LogScrubberJob>("Log scrubbing executing", "Log scrubbing complete"))

View File

@@ -55,5 +55,5 @@ public class InstructionProcessJob : IRecurringBackgroundJob
}
return Task.CompletedTask;
}
}
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -49,7 +49,5 @@ public class LogViewerConfig : ILogViewerConfig
IReadOnlyList<SavedLogSearch> result = GetSavedSearches()!;
scope.Complete();
return result;
scope.Complete();
return result;
}
}

View File

@@ -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}");

View File

@@ -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}");
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -55,6 +55,6 @@ internal class LogDto
/// </summary>
[Column("parameters")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(500)]
[Length(4000)]
public string? Parameters { get; set; }
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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))

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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" />

View File

@@ -126,7 +126,7 @@ public static class UmbracoBuilderDependencyInjectionExtensions
builder.AddNotificationHandler<UmbracoRequestEndNotification, AutoModelsNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, AutoModelsNotificationHandler>();
builder.AddNotificationHandler<DataTypeCacheRefresherNotification, AutoModelsNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, OutOfDateModelsStatus>();
builder.AddNotificationHandler<DataTypeCacheRefresherNotification, OutOfDateModelsStatus>();
}
@@ -135,7 +135,7 @@ public static class UmbracoBuilderDependencyInjectionExtensions
// Register required services for ModelsBuilderDashboardController
builder.Services.AddSingleton<IModelsGenerator, ModelsGenerator>();
// TODO: Remove in v13 - this is only here in case someone is already using this generator directly
builder.Services.AddSingleton<ModelsGenerator>();
builder.Services.AddSingleton<OutOfDateModelsStatus>();
@@ -165,7 +165,7 @@ public static class UmbracoBuilderDependencyInjectionExtensions
// This is what the community MB would replace, all of the above services are fine to be registered
builder.Services.AddSingleton<IPublishedModelFactory>(factory => factory.CreateDefaultPublishedModelFactory());
return builder;
}
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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": {

View File

@@ -13,7 +13,7 @@ WebApplication app = builder.Build();
await app.BootUmbracoAsync();
#if (UseHttpsRedirect)
#if UseHttpsRedirect
app.UseHttpsRedirection();
#endif

View File

@@ -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>

View File

@@ -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>();

View File

@@ -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);
// loop until we have access or reached max loops
while (publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop);
if (i == maxLoop)
{

View File

@@ -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)
{
@@ -150,8 +146,8 @@ public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
$"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null");
}
string? customActionName = GetTemplateName(request);
string? customActionName = GetTemplateName(request);
def = new UmbracoRouteValues(
request,
def.ControllerActionDescriptor,
@@ -166,7 +162,7 @@ public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
return def;
}
private string? GetTemplateName(IPublishedRequest request)
{
// check that a template is defined), if it doesn't and there is a hijacked route it will just route

View File

@@ -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>

View File

@@ -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)

View File

@@ -98,7 +98,7 @@
},
{
"condition": "(UmbracoRelease == 'LTS')",
"value": "13.4.1"
"value": "13.5.0"
}
]
}

View File

@@ -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>

View File

@@ -0,0 +1,2 @@
# Ignore Visual Studio's generated launchSettings file.
Properties/launchSettings.json

View File

@@ -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"
}
},

View File

@@ -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"

View File

@@ -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([]);
});

View File

@@ -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([]);
});

Some files were not shown because too many files have changed in this diff Show More