Merge remote-tracking branch 'origin/v12/dev' into v14/dev

# Conflicts:
#	src/Umbraco.Core/Services/ContentService.cs
#	src/Umbraco.Infrastructure/CompatibilitySuppressions.xml
#	src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
#	src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
This commit is contained in:
Bjarke Berg
2023-06-27 09:47:33 +02:00
86 changed files with 5114 additions and 285 deletions

9
.github/README.md vendored
View File

@@ -1,4 +1,11 @@
# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/contrib)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco) [![Discord](https://img.shields.io/discord/869656431308189746)](https://discord.gg/umbraco) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=v11%2Fcontrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
# [Umbraco CMS](https://umbraco.com)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Follow Umbraco on Twitter](https://img.shields.io/badge/Follow-blue?logo=twitter&logoColor=fff)](https://twitter.com/intent/follow?screen_name=umbraco)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
[![Open in GitHub Codespaces](https://img.shields.io/badge/Open%20in%20GitHub%20Codespaces-525252?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.

View File

@@ -17,6 +17,9 @@
<PackageReference Include="OpenIddict.AspNetCore" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Persistence.EFCore.Sqlite\Umbraco.Cms.Persistence.EFCore.Sqlite.csproj" />
<ProjectReference Include="..\Umbraco.Cms.Persistence.EFCore.SqlServer\Umbraco.Cms.Persistence.EFCore.SqlServer.csproj" />
<ProjectReference Include="..\Umbraco.Cms.Persistence.EFCore\Umbraco.Cms.Persistence.EFCore.csproj" />
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />

View File

@@ -58,7 +58,8 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
path = WebUtility.UrlDecode(path);
}
path = path.EnsureStartsWith("/");
path = path.TrimStart("/");
path = path.Length == 0 ? "/" : path;
IPublishedContent? contentItem = GetContent(path);
if (contentItem is not null)

View File

@@ -1,7 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
using Umbraco.Cms.Api.Delivery.Configuration;
@@ -33,6 +32,7 @@ public static class UmbracoBuilderExtensions
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
builder.AddUmbracoEFCoreDbContext();
builder
.Services
.AddControllers()
@@ -47,3 +47,4 @@ public static class UmbracoBuilderExtensions
return builder;
}
}

View File

@@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="JsonPatch.Net" Version="2.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.2" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.4.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
public class EFCoreSqlServerComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IMigrationProvider, SqlServerMigrationProvider>();
builder.Services.AddSingleton<IMigrationProviderSetup, SqlServerMigrationProviderSetup>();
}
}

View File

@@ -0,0 +1,266 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.SqlServer.Migrations
{
[DbContext(typeof(UmbracoDbContext))]
[Migration("20230622184303_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ClientSecret")
.HasColumnType("nvarchar(max)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Permissions")
.HasColumnType("nvarchar(max)");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("RedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Requirements")
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique()
.HasFilter("[ClientId] IS NOT NULL");
b.ToTable("umbracoOpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Scopes")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("Descriptions")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Resources")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("umbracoOpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("AuthorizationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime2");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("datetime2");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique()
.HasFilter("[ReferenceId] IS NOT NULL");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,166 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.SqlServer.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "umbracoOpenIddictApplications",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ClientId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
ClientSecret = table.Column<string>(type: "nvarchar(max)", nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
Permissions = table.Column<string>(type: "nvarchar(max)", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
RedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
Requirements = table.Column<string>(type: "nvarchar(max)", nullable: true),
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictApplications", x => x.Id);
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Descriptions = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
Resources = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
Scopes = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_umbracoOpenIddictAuthorizations_umbracoOpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "umbracoOpenIddictApplications",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictTokens",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
AuthorizationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ExpirationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
Payload = table.Column<string>(type: "nvarchar(max)", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
RedemptionDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ReferenceId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_umbracoOpenIddictTokens_umbracoOpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "umbracoOpenIddictApplications",
principalColumn: "Id");
table.ForeignKey(
name: "FK_umbracoOpenIddictTokens_umbracoOpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "umbracoOpenIddictAuthorizations",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictApplications_ClientId",
table: "umbracoOpenIddictApplications",
column: "ClientId",
unique: true,
filter: "[ClientId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
table: "umbracoOpenIddictAuthorizations",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictScopes_Name",
table: "umbracoOpenIddictScopes",
column: "Name",
unique: true,
filter: "[Name] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_ApplicationId_Status_Subject_Type",
table: "umbracoOpenIddictTokens",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_AuthorizationId",
table: "umbracoOpenIddictTokens",
column: "AuthorizationId");
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_ReferenceId",
table: "umbracoOpenIddictTokens",
column: "ReferenceId",
unique: true,
filter: "[ReferenceId] IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "umbracoOpenIddictScopes");
migrationBuilder.DropTable(
name: "umbracoOpenIddictTokens");
migrationBuilder.DropTable(
name: "umbracoOpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "umbracoOpenIddictApplications");
}
}
}

View File

@@ -0,0 +1,264 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Umbraco.Cms.Persistence.EFCore;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.SqlServer.Migrations
{
[DbContext(typeof(UmbracoDbContext))]
partial class UmbracoOpenIddictDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ClientSecret")
.HasColumnType("nvarchar(max)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Permissions")
.HasColumnType("nvarchar(max)");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("RedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Requirements")
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique()
.HasFilter("[ClientId] IS NOT NULL");
b.ToTable("umbracoOpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Scopes")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("Descriptions")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Resources")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("umbracoOpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("AuthorizationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime2");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("datetime2");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique()
.HasFilter("[ReferenceId] IS NOT NULL");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Persistence.EFCore.Migrations;
using Umbraco.Extensions;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
public class SqlServerMigrationProvider : IMigrationProvider
{
private readonly IDbContextFactory<UmbracoDbContext> _dbContextFactory;
public SqlServerMigrationProvider(IDbContextFactory<UmbracoDbContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public string ProviderName => "Microsoft.Data.SqlClient";
public async Task MigrateAsync(EFCoreMigration migration)
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
await context.MigrateDatabaseAsync(GetMigrationType(migration));
}
public async Task MigrateAllAsync()
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
await context.Database.MigrateAsync();
}
private static Type GetMigrationType(EFCoreMigration migration) =>
migration switch
{
EFCoreMigration.InitialCreate => typeof(Migrations.InitialCreate),
_ => throw new ArgumentOutOfRangeException(nameof(migration), $@"Not expected migration value: {migration}")
};
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
public class SqlServerMigrationProviderSetup : IMigrationProviderSetup
{
public string ProviderName => "Microsoft.Data.SqlClient";
public void Setup(DbContextOptionsBuilder builder, string? connectionString)
{
builder.UseSqlServer(connectionString, x => x.MigrationsAssembly(GetType().Assembly.FullName));
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>Umbraco CMS - EF Core - SqlServer migrations</Title>
<!-- TODO: Enable when final version is shipped (because there's currently no previous version) -->
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Persistence.EFCore\Umbraco.Cms.Persistence.EFCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.Sqlite;
public class EFCoreSqliteComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IMigrationProvider, SqliteMigrationProvider>();
builder.Services.AddSingleton<IMigrationProviderSetup, SqliteMigrationProviderSetup>();
}
}

View File

@@ -0,0 +1,258 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.Sqlite.Migrations
{
[DbContext(typeof(UmbracoDbContext))]
[Migration("20230622183638_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientSecret")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("DisplayNames")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("RedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Requirements")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("umbracoOpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Scopes")
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Descriptions")
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("DisplayNames")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Resources")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("umbracoOpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("AuthorizationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("CreationDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("TEXT");
b.Property<string>("Payload")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("TEXT");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.Sqlite.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "umbracoOpenIddictApplications",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ClientId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ClientSecret = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
DisplayNames = table.Column<string>(type: "TEXT", nullable: true),
Permissions = table.Column<string>(type: "TEXT", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "TEXT", nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
RedirectUris = table.Column<string>(type: "TEXT", nullable: true),
Requirements = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictApplications", x => x.Id);
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
Descriptions = table.Column<string>(type: "TEXT", nullable: true),
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
DisplayNames = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
Resources = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ApplicationId = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
Scopes = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "TEXT", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_umbracoOpenIddictAuthorizations_umbracoOpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "umbracoOpenIddictApplications",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "umbracoOpenIddictTokens",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
ApplicationId = table.Column<string>(type: "TEXT", nullable: true),
AuthorizationId = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: true),
ExpirationDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Payload = table.Column<string>(type: "TEXT", nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
RedemptionDate = table.Column<DateTime>(type: "TEXT", nullable: true),
ReferenceId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "TEXT", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_umbracoOpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_umbracoOpenIddictTokens_umbracoOpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "umbracoOpenIddictApplications",
principalColumn: "Id");
table.ForeignKey(
name: "FK_umbracoOpenIddictTokens_umbracoOpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "umbracoOpenIddictAuthorizations",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictApplications_ClientId",
table: "umbracoOpenIddictApplications",
column: "ClientId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
table: "umbracoOpenIddictAuthorizations",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictScopes_Name",
table: "umbracoOpenIddictScopes",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_ApplicationId_Status_Subject_Type",
table: "umbracoOpenIddictTokens",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_AuthorizationId",
table: "umbracoOpenIddictTokens",
column: "AuthorizationId");
migrationBuilder.CreateIndex(
name: "IX_umbracoOpenIddictTokens_ReferenceId",
table: "umbracoOpenIddictTokens",
column: "ReferenceId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "umbracoOpenIddictScopes");
migrationBuilder.DropTable(
name: "umbracoOpenIddictTokens");
migrationBuilder.DropTable(
name: "umbracoOpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "umbracoOpenIddictApplications");
}
}
}

View File

@@ -0,0 +1,256 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Umbraco.Cms.Persistence.EFCore;
#nullable disable
namespace Umbraco.Cms.Persistence.EFCore.Sqlite.Migrations
{
[DbContext(typeof(UmbracoDbContext))]
partial class UmbracoOpenIddictDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientSecret")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("DisplayNames")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("RedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Requirements")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("umbracoOpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("CreationDate")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Scopes")
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Descriptions")
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("DisplayNames")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Resources")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("umbracoOpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("AuthorizationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("CreationDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("TEXT");
b.Property<string>("Payload")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("TEXT");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("umbracoOpenIddictTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Persistence.EFCore.Migrations;
using Umbraco.Extensions;
namespace Umbraco.Cms.Persistence.EFCore.Sqlite;
public class SqliteMigrationProvider : IMigrationProvider
{
private readonly IDbContextFactory<UmbracoDbContext> _dbContextFactory;
public SqliteMigrationProvider(IDbContextFactory<UmbracoDbContext> dbContextFactory)
=> _dbContextFactory = dbContextFactory;
public string ProviderName => "Microsoft.Data.Sqlite";
public async Task MigrateAsync(EFCoreMigration migration)
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
await context.MigrateDatabaseAsync(GetMigrationType(migration));
}
public async Task MigrateAllAsync()
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
if (context.Database.CurrentTransaction is not null)
{
throw new InvalidOperationException("Cannot migrate all when a transaction is active.");
}
await context.Database.MigrateAsync();
}
private static Type GetMigrationType(EFCoreMigration migration) =>
migration switch
{
EFCoreMigration.InitialCreate => typeof(Migrations.InitialCreate),
_ => throw new ArgumentOutOfRangeException(nameof(migration), $@"Not expected migration value: {migration}")
};
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.Sqlite;
public class SqliteMigrationProviderSetup : IMigrationProviderSetup
{
public string ProviderName => "Microsoft.Data.Sqlite";
public void Setup(DbContextOptionsBuilder builder, string? connectionString)
{
builder.UseSqlite(connectionString, x => x.MigrationsAssembly(GetType().Assembly.FullName));
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>Umbraco CMS - EF Core - Sqlite migrations</Title>
<!-- TODO: Enable when final version is shipped (because there's currently no previous version) -->
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Persistence.EFCore\Umbraco.Cms.Persistence.EFCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Migrations.Notifications;
using Umbraco.Cms.Persistence.EFCore;
namespace Umbraco.Cms.Persistence.EFCore.Composition;
public class UmbracoEFCoreComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IEFCoreMigrationExecutor, EfCoreMigrationExecutor>();
builder.AddNotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification, EFCoreCreateTablesNotificationHandler>();
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>();
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options
.UseEntityFrameworkCore()
.UseDbContext<UmbracoDbContext>();
});
}
}
public class EFCoreCreateTablesNotificationHandler : INotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification>, INotificationAsyncHandler<UnattendedInstallNotification>
{
private readonly IEFCoreMigrationExecutor _iefCoreMigrationExecutor;
public EFCoreCreateTablesNotificationHandler(IEFCoreMigrationExecutor iefCoreMigrationExecutor)
{
_iefCoreMigrationExecutor = iefCoreMigrationExecutor;
}
public async Task HandleAsync(UnattendedInstallNotification notification, CancellationToken cancellationToken)
{
await HandleAsync();
}
public async Task HandleAsync(DatabaseSchemaAndDataCreatedNotification notification, CancellationToken cancellationToken)
{
await HandleAsync();
}
private async Task HandleAsync()
{
await _iefCoreMigrationExecutor.ExecuteAllMigrationsAsync();
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore;
public class EfCoreMigrationExecutor : IEFCoreMigrationExecutor
{
private readonly IEnumerable<IMigrationProvider> _migrationProviders;
private readonly IOptions<ConnectionStrings> _options;
// We need to do migrations out side of a scope due to sqlite
public EfCoreMigrationExecutor(
IEnumerable<IMigrationProvider> migrationProviders,
IOptions<ConnectionStrings> options)
{
_migrationProviders = migrationProviders;
_options = options;
}
public async Task ExecuteSingleMigrationAsync(EFCoreMigration migration)
{
IMigrationProvider? provider = _migrationProviders.FirstOrDefault(x => x.ProviderName == _options.Value.ProviderName);
if (provider is not null)
{
await provider.MigrateAsync(migration);
}
}
public async Task ExecuteAllMigrationsAsync()
{
IMigrationProvider? provider = _migrationProviders.FirstOrDefault(x => x.ProviderName == _options.Value.ProviderName);
if (provider is not null)
{
await provider.MigrateAllAsync();
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Persistence.EFCore;
namespace Umbraco.Extensions;
public static class BackOfficeAuthBuilderOpenIddictExtensions
{
public static IUmbracoBuilder AddUmbracoEFCoreDbContext(this IUmbracoBuilder builder)
{
builder.Services.AddUmbracoEFCoreContext<UmbracoDbContext>((options, connectionString, providerName) =>
{
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
return builder;
}
}

View File

@@ -2,6 +2,7 @@ using System.Data;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
namespace Umbraco.Extensions;
@@ -50,4 +51,21 @@ public static class DbContextExtensions
var result = await dbCommand.ExecuteScalarAsync();
return (T?)result;
}
public static async Task MigrateDatabaseAsync(this DbContext context, Type targetMigration)
{
MigrationAttribute? migrationAttribute = targetMigration.GetCustomAttribute<MigrationAttribute>(false);
if (migrationAttribute is null)
{
throw new ArgumentException("The type does not have a MigrationAttribute", nameof(targetMigration));
}
await context.MigrateDatabaseAsync(migrationAttribute.Id);
}
public static async Task MigrateDatabaseAsync(this DbContext context, string targetMigrationId)
{
await context.GetService<IMigrator>().MigrateAsync(targetMigrationId);
}
}

View File

@@ -1,8 +1,11 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Cms.Persistence.EFCore.Locking;
using Umbraco.Cms.Persistence.EFCore.Migrations;
using Umbraco.Cms.Persistence.EFCore.Scoping;
namespace Umbraco.Extensions;
@@ -11,6 +14,55 @@ public static class UmbracoEFCoreServiceCollectionExtensions
{
public delegate void DefaultEFCoreOptionsAction(DbContextOptionsBuilder options, string? providerName, string? connectionString);
public static IServiceCollection AddUmbracoEFCoreContext<T>(this IServiceCollection services, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null)
where T : DbContext
{
defaultEFCoreOptionsAction ??= DefaultOptionsAction;
services.AddDbContext<T>(
(provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder),
optionsLifetime: ServiceLifetime.Transient);
services.AddDbContextFactory<T>((provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder));
services.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>();
services.AddUnique<IEFCoreScopeAccessor<T>, EFCoreScopeAccessor<T>>();
services.AddUnique<IEFCoreScopeProvider<T>, EFCoreScopeProvider<T>>();
services.AddSingleton<IDistributedLockingMechanism, SqliteEFCoreDistributedLockingMechanism<T>>();
services.AddSingleton<IDistributedLockingMechanism, SqlServerEFCoreDistributedLockingMechanism<T>>();
return services;
}
private static void SetupDbContext(DefaultEFCoreOptionsAction defaultEFCoreOptionsAction, IServiceProvider provider, DbContextOptionsBuilder builder)
{
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
IEnumerable<IMigrationProviderSetup> migrationProviders = provider.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider =
migrationProviders.FirstOrDefault(x => x.ProviderName == connectionStrings.ProviderName);
migrationProvider?.Setup(builder, connectionStrings.ConnectionString);
defaultEFCoreOptionsAction(builder, connectionStrings.ConnectionString, connectionStrings.ProviderName);
}
private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider)
{
string? connectionString = null;
string? providerName = null;
ConnectionStrings connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>().CurrentValue;
// Replace data directory
string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString();
if (string.IsNullOrEmpty(dataDirectory) is false)
{
connectionStrings.ConnectionString = connectionStrings.ConnectionString?.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory);
}
return connectionStrings;
}
public static IServiceCollection AddUmbracoEFCoreContext<T>(this IServiceCollection services, string connectionString, string providerName, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null)
where T : DbContext
{

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Persistence.EFCore.Migrations;
public interface IMigrationProvider
{
string ProviderName { get; }
Task MigrateAsync(EFCoreMigration migration);
Task MigrateAllAsync();
}

View File

@@ -0,0 +1,10 @@
using Microsoft.EntityFrameworkCore;
namespace Umbraco.Cms.Persistence.EFCore.Migrations;
public interface IMigrationProviderSetup
{
string ProviderName { get; }
void Setup(DbContextOptionsBuilder builder, string? connectionString);
}

View File

@@ -8,7 +8,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.5.0" />
</ItemGroup>

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Persistence.EFCore;
/// <remarks>
/// To autogenerate migrations use the following commands
/// and insure the 'src/Umbraco.Web.UI/appsettings.json' have a connection string set with the right provider.
///
/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -- --provider SqlServer
/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -- --provider Sqlite
///
/// To find documentation about this way of working with the context see
/// https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-one-context-type
/// </remarks>
public class UmbracoDbContext : DbContext
{
public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes())
{
entity.SetTableName(Constants.DatabaseSchema.TableNamePrefix + entity.GetTableName());
}
}
}

View File

@@ -10,6 +10,6 @@ public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApi
{
}
protected override IApiContent Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties)
=> new ApiContent(id, name, contentType, route, properties);
protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties);
}

View File

@@ -17,7 +17,7 @@ public abstract class ApiContentBuilderBase<T>
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
}
protected abstract T Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties);
protected abstract T Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties);
public virtual T? Build(IPublishedContent content)
{
@@ -34,9 +34,7 @@ public abstract class ApiContentBuilderBase<T>
return Create(
content,
content.Key,
_apiContentNameProvider.GetName(content),
content.ContentType.Alias,
route,
properties);
}

View File

@@ -13,7 +13,7 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase<IApiConten
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _apiContentRouteBuilder = apiContentRouteBuilder;
protected override IApiContentResponse Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties)
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
{
var routesByCulture = new Dictionary<string, IApiContentRoute>();
@@ -35,6 +35,6 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase<IApiConten
routesByCulture[publishedCultureInfo.Culture] = cultureRoute;
}
return new ApiContentResponse(id, name, contentType, route, properties, routesByCulture);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, routesByCulture);
}
}

View File

@@ -54,8 +54,8 @@
<key alias="setPermissions">Sæt rettigheder</key>
<key alias="unlock">Lås op</key>
<key alias="createblueprint">Opret indholdsskabelon</key>
<key alias="resendInvite">Gensend Invitation</key>
<key alias="defaultValue">Standard værdi</key>
<key alias="resendInvite">Gensend invitation</key>
<key alias="defaultValue">Standardværdi</key>
</area>
<area alias="actionCategories">
<key alias="content">Indhold</key>

View File

@@ -2059,7 +2059,7 @@
<key alias="usergroup">Benutzergruppe</key>
<key alias="userInvited">wurde eingeladen</key>
<key alias="userInvitedSuccessHelp">Eine Einladung mit Anweisungen zur Anmeldung im Umbraco-Back-Office wurde dem neuen Benutzer zugeschickt.</key>
<key alias="userinviteWelcomeMessage">Hallo und Willkommen bei Umbraco! In nur einer Minute sind Sie bereit loszulegen, Sie müssen nur ein Kennwort festlegen und optinal Ihrem Avatar ein Bild hinzufügen.</key>
<key alias="userinviteWelcomeMessage">Hallo und Willkommen bei Umbraco! In nur einer Minute sind Sie bereit loszulegen, Sie müssen nur ein Kennwort festlegen und optional Ihrem Avatar ein Bild hinzufügen.</key>
<key alias="userinviteExpiredMessage">Willkommen bei Umbraco! Bedauerlicherweise ist Ihre Einladung verfallen. Bitte kontaktieren Sie Ihren Administrator und bitten Sie ihn, diese erneut zu schicken.</key>
<key alias="userinviteAvatarMessage">Laden Sie ein Foto von sich hoch, um es anderen Benutzern zu erleichtern, sie zu erkennen. Klicken Sie auf den Kreis oben, um Ihr Foto hochzuladen.</key>
<key alias="writer">Autor</key>

View File

@@ -488,7 +488,7 @@
<key alias="insertlink">Insert link</key>
<key alias="insertMacro">Click to add a Macro</key>
<key alias="inserttable">Insert table</key>
<key alias="languagedeletewarning">This will delete the language</key>
<key alias="languagedeletewarning">This will delete the language and all content related to the language</key>
<key alias="languageChangeWarning">Changing the culture for a language may be an expensive operation and will result
in the content cache and indexes being rebuilt
</key>
@@ -2275,7 +2275,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="items">items</key>
<key alias="urls">URL(s)</key>
<key alias="urlsSelected">URL(s) selected</key>
<key alias="itemsSelected">items selected</key>
<key alias="itemsSelected">item(s) selected</key>
<key alias="invalidDate">Invalid date</key>
<key alias="invalidNumber">Not a number</key>
<key alias="invalidNumberStepSize">Not a valid numeric step size</key>

View File

@@ -503,7 +503,7 @@
<key alias="insertlink">Insert link</key>
<key alias="insertMacro">Click to add a Macro</key>
<key alias="inserttable">Insert table</key>
<key alias="languagedeletewarning">This will delete the language</key>
<key alias="languagedeletewarning">This will delete the language and all content related to the language</key>
<key alias="languageChangeWarning">Changing the culture for a language may be an expensive operation and will result
in the content cache and indexes being rebuilt
</key>
@@ -2371,7 +2371,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="items">items</key>
<key alias="urls">URL(s)</key>
<key alias="urlsSelected">URL(s) selected</key>
<key alias="itemsSelected">items selected</key>
<key alias="itemsSelected">item(s) selected</key>
<key alias="invalidDate">Invalid date</key>
<key alias="invalidNumber">Not a number</key>
<key alias="invalidNumberStepSize">Not a valid numeric step size</key>

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,20 @@ namespace Umbraco.Cms.Core.Models.DeliveryApi;
public class ApiContent : ApiElement, IApiContent
{
public ApiContent(Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties)
public ApiContent(Guid id, string name, string contentType, DateTime createDate, DateTime updateDate, IApiContentRoute route, IDictionary<string, object?> properties)
: base(id, contentType, properties)
{
Name = name;
CreateDate = createDate;
UpdateDate = updateDate;
Route = route;
}
public string Name { get; }
public DateTime CreateDate { get; }
public DateTime UpdateDate { get; }
public IApiContentRoute Route { get; }
}

View File

@@ -4,8 +4,8 @@ namespace Umbraco.Cms.Core.Models.DeliveryApi;
public class ApiContentResponse : ApiContent, IApiContentResponse
{
public ApiContentResponse(Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties, IDictionary<string, IApiContentRoute> cultures)
: base(id, name, contentType, route, properties)
public ApiContentResponse(Guid id, string name, string contentType, DateTime createDate, DateTime updateDate, IApiContentRoute route, IDictionary<string, object?> properties, IDictionary<string, IApiContentRoute> cultures)
: base(id, name, contentType, createDate, updateDate, route, properties)
=> Cultures = cultures;
// a little DX; by default this dictionary will be serialized as the first part of the response due to the inner workings of the serializer.

View File

@@ -4,5 +4,9 @@ public interface IApiContent : IApiElement
{
string? Name { get; }
public DateTime CreateDate { get; }
public DateTime UpdateDate { get; }
IApiContentRoute Route { get; }
}

View File

@@ -2436,7 +2436,7 @@ public class ContentService : RepositoryService, IContentService
{
EventMessages eventMessages = EventMessagesFactory.Get();
if(content.ParentId == parentId)
if (content.ParentId == parentId)
{
return OperationResult.Succeed(eventMessages);
}
@@ -2587,7 +2587,8 @@ public class ContentService : RepositoryService, IContentService
IContent[] contents = _documentRepository.Get(query).ToArray();
var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages);
if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
var deletingContentNotification = new ContentDeletingNotification(contents, eventMessages);
if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification) || scope.Notifications.PublishCancelable(deletingContentNotification))
{
scope.Complete();
return OperationResult.Cancel(eventMessages);

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Text.RegularExpressions;
@@ -16,7 +16,8 @@ public static class ContentServiceExtensions
{
#region RTE Anchor values
private static readonly Regex AnchorRegex = new("<a id=\"(.*?)\">", RegexOptions.Compiled);
private static readonly Regex AnchorRegex = new(@"<a id=\\*""(.*?)\\*"">", RegexOptions.Compiled);
private static readonly string[] _propertyTypesWithRte = new[] { Constants.PropertyEditors.Aliases.TinyMce, Constants.PropertyEditors.Aliases.BlockList, Constants.PropertyEditors.Aliases.BlockGrid };
public static IEnumerable<IContent>? GetByIds(this IContentService contentService, IEnumerable<Udi> ids)
{
@@ -67,14 +68,17 @@ public static class ContentServiceExtensions
public static IEnumerable<string> GetAnchorValuesFromRTEs(this IContentService contentService, int id, string? culture = "*")
{
var result = new List<string>();
culture = culture is not "*" ? culture : null;
IContent? content = contentService.GetById(id);
if (content is not null)
if (content is null)
{
foreach (IProperty contentProperty in content.Properties)
{
if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases
.TinyMce))
return result;
}
foreach (IProperty contentProperty in content.Properties.Where(s => _propertyTypesWithRte.Contains(s.PropertyType.PropertyEditorAlias)))
{
var value = contentProperty.GetValue(culture)?.ToString();
if (!string.IsNullOrEmpty(value))
@@ -82,8 +86,6 @@ public static class ContentServiceExtensions
result.AddRange(contentService.GetAnchorValuesFromRTEContent(value));
}
}
}
}
return result;
}
@@ -96,7 +98,7 @@ public static class ContentServiceExtensions
MatchCollection matches = AnchorRegex.Matches(rteContent);
foreach (Match match in matches)
{
result.Add(match.Value.Split(Constants.CharArrays.DoubleQuote)[1]);
result.Add(match.Groups[1].Value);
}
return result;

View File

@@ -41,6 +41,7 @@ public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex
if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator)
{
PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly;
SupportProtectedContent = contentValueSetValidator.SupportProtectedContent;
}
}

View File

@@ -47,6 +47,8 @@ public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDi
public bool PublishedValuesOnly { get; protected set; } = false;
public bool SupportProtectedContent { get; protected set; } = true;
/// <summary>
/// override to check if we can actually initialize.
/// </summary>

View File

@@ -337,4 +337,18 @@
<Right>lib/net7.0/Umbraco.Infrastructure.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Infrastructure.Search.IUmbracoIndexingHandler.RemoveProtectedContent</Target>
<Left>lib/net7.0/Umbraco.Infrastructure.dll</Left>
<Right>lib/net7.0/Umbraco.Infrastructure.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Umbraco.Cms.Infrastructure.Examine.IUmbracoIndex.SupportProtectedContent</Target>
<Left>lib/net7.0/Umbraco.Infrastructure.dll</Left>
<Right>lib/net7.0/Umbraco.Infrastructure.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>

View File

@@ -59,6 +59,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<ExamineIndexRebuilder>();
builder.AddNotificationHandler<ContentCacheRefresherNotification, ContentIndexingNotificationHandler>();
builder.AddNotificationHandler<PublicAccessCacheRefresherNotification, ContentIndexingNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, ContentTypeIndexingNotificationHandler>();
builder.AddNotificationHandler<ContentCacheRefresherNotification, DeliveryApiContentIndexingNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, DeliveryApiContentIndexingNotificationHandler>();

View File

@@ -41,6 +41,7 @@ public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValid
_scopeProvider = scopeProvider;
}
[Obsolete("This constructor is obsolete, the IScopeProvider will change to Infrastructure.Scoping.ScopeProvider instead, this will be removed in Umbraco 14.")]
public ContentValueSetValidator(
bool publishedValuesOnly,
bool supportProtectedContent,

View File

@@ -4,6 +4,7 @@ using Examine.Search;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HostedServices;
using Umbraco.Cms.Infrastructure.Search;
using Umbraco.Extensions;
@@ -25,6 +26,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler
private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder;
private readonly ICoreScopeProvider _scopeProvider;
private readonly ExamineIndexingMainDomHandler _mainDomHandler;
private readonly IPublicAccessService _publicAccessService;
public ExamineUmbracoIndexingHandler(
ILogger<ExamineUmbracoIndexingHandler> logger,
@@ -35,7 +37,8 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler
IPublishedContentValueSetBuilder publishedContentValueSetBuilder,
IValueSetBuilder<IMedia> mediaValueSetBuilder,
IValueSetBuilder<IMember> memberValueSetBuilder,
ExamineIndexingMainDomHandler mainDomHandler)
ExamineIndexingMainDomHandler mainDomHandler,
IPublicAccessService publicAccessService)
{
_logger = logger;
_scopeProvider = scopeProvider;
@@ -46,6 +49,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler
_mediaValueSetBuilder = mediaValueSetBuilder;
_memberValueSetBuilder = memberValueSetBuilder;
_mainDomHandler = mainDomHandler;
_publicAccessService = publicAccessService;
_enabled = new Lazy<bool>(IsEnabled);
}
@@ -122,6 +126,20 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler
}
}
/// <inheritdoc />
public void RemoveProtectedContent()
{
var actions = DeferredActions.Get(_scopeProvider);
if (actions != null)
{
actions.Add(new DeferredRemoveProtectedContent(_backgroundTaskQueue, this, _publicAccessService));
}
else
{
DeferredRemoveProtectedContent.Execute(_backgroundTaskQueue, this, _publicAccessService);
}
}
/// <inheritdoc />
public void DeleteDocumentsForContentTypes(IReadOnlyCollection<int> removedContentTypes)
{
@@ -391,5 +409,50 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler
}
}
/// <summary>
/// Removes all protected content from applicable indexes on a background thread
/// </summary>
private class DeferredRemoveProtectedContent : IDeferredAction
{
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler;
private readonly IPublicAccessService _publicAccessService;
public DeferredRemoveProtectedContent(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IPublicAccessService publicAccessService)
{
_backgroundTaskQueue = backgroundTaskQueue;
_examineUmbracoIndexingHandler = examineUmbracoIndexingHandler;
_publicAccessService = publicAccessService;
}
public void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _publicAccessService);
public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IPublicAccessService publicAccessService)
=> backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken =>
{
using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true);
var protectedContentIds = publicAccessService.GetAll().Select(entry => entry.ProtectedNodeId).ToArray();
if (protectedContentIds.Any() is false)
{
return Task.CompletedTask;
}
foreach (IUmbracoContentIndex index in examineUmbracoIndexingHandler._examineManager.Indexes
.OfType<IUmbracoContentIndex>()
.Where(x => x is { EnableDefaultEventHandler: true, SupportProtectedContent: false }))
{
if (cancellationToken.IsCancellationRequested)
{
return Task.CompletedTask;
}
index.DeleteFromIndex(protectedContentIds.Select(id => id.ToString()));
}
return Task.CompletedTask;
});
}
#endregion
}

View File

@@ -21,4 +21,12 @@ public interface IUmbracoIndex : IIndex, IIndexStats
/// * non-published Variants
/// </remarks>
bool PublishedValuesOnly { get; }
/// <summary>
/// Whether the index can contain protected content
/// </summary>
/// <remarks>
/// To retain backwards compatability, the default value is true
/// </remarks>
bool SupportProtectedContent { get; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Persistence.EFCore.Migrations;
public enum EFCoreMigration
{
InitialCreate = 0
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Infrastructure.Migrations;
public interface IEFCoreMigrationExecutor
{
Task ExecuteSingleMigrationAsync(EFCoreMigration efCoreMigration);
Task ExecuteAllMigrationsAsync();
}

View File

@@ -291,6 +291,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
using (var scope = _scopeProvider.CreateCoreScope())
{
var result = CreateSchemaAndData(scope);
scope.Notifications.Publish(new DatabaseSchemaAndDataCreatedNotification());
scope.Complete();
return result;
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Infrastructure.Migrations.Notifications;
public class DatabaseSchemaAndDataCreatedNotification : INotification
{
}

View File

@@ -80,6 +80,7 @@ public class UmbracoPlan : MigrationPlan
// To 12.1.0
To<V_12_1_0.TablesIndexesImprovement>("{1187192D-EDB5-4619-955D-91D48D738871}");
To<V_12_1_0.AddOpenIddict>("{47DE85CE-1E16-42A0-8AF6-3EC3BCEF5471}");
// To 14.0.0
To<V_13_0_0.AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");

View File

@@ -0,0 +1,20 @@
using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_1_0;
public class AddOpenIddict : UnscopedMigrationBase
{
private readonly IEFCoreMigrationExecutor _iefCoreMigrationExecutor;
public AddOpenIddict(IMigrationContext context, IEFCoreMigrationExecutor iefCoreMigrationExecutor)
: base(context)
{
_iefCoreMigrationExecutor = iefCoreMigrationExecutor;
}
protected override void Migrate()
{
_iefCoreMigrationExecutor.ExecuteSingleMigrationAsync(EFCoreMigration.InitialCreate).GetAwaiter().GetResult();
}
}

View File

@@ -72,7 +72,6 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver
return string.Empty;
}
var mark = new Markdown();
return mark.Transform(markdownString);
return markdownString;
}
}

View File

@@ -19,6 +19,11 @@ public interface IUmbracoIndexingHandler
void ReIndexForMedia(IMedia sender, bool isPublished);
/// <summary>
/// Removes any content that is flagged as protected
/// </summary>
void RemoveProtectedContent();
/// <summary>
/// Deletes all documents for the content type Ids
/// </summary>

View File

@@ -9,7 +9,9 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Search;
public sealed class ContentIndexingNotificationHandler : INotificationHandler<ContentCacheRefresherNotification>
public sealed class ContentIndexingNotificationHandler :
INotificationHandler<ContentCacheRefresherNotification>,
INotificationHandler<PublicAccessCacheRefresherNotification>
{
private readonly IContentService _contentService;
private readonly IUmbracoIndexingHandler _umbracoIndexingHandler;
@@ -158,4 +160,7 @@ public sealed class ContentIndexingNotificationHandler : INotificationHandler<Co
_umbracoIndexingHandler.DeleteIndexForEntities(deleteBatch, false);
}
}
public void Handle(PublicAccessCacheRefresherNotification notification)
=> _umbracoIndexingHandler.RemoveProtectedContent();
}

View File

@@ -109,9 +109,9 @@ public class NuCacheContentRepository : RepositoryBase, INuCacheContentRepositor
| ContentCacheDataSerializerEntityType.Member);
// If contentTypeIds, mediaTypeIds and memberTypeIds are null, truncate table as all records will be deleted (as these 3 are the only types in the table).
if ((contentTypeIds == null || !contentTypeIds.Any())
&& (mediaTypeIds == null || !mediaTypeIds.Any())
&& (memberTypeIds == null || !memberTypeIds.Any()))
if (contentTypeIds != null && !contentTypeIds.Any()
&& mediaTypeIds != null && !mediaTypeIds.Any()
&& memberTypeIds != null && !memberTypeIds.Any())
{
if (Database.DatabaseType == DatabaseType.SqlServer2012)
{

View File

@@ -146,7 +146,7 @@
{
"element": "#dialog [data-element='action-documentType']",
"title": "Create Document Type",
"content": "<p>Click <b>Document Type</b> to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type.</p><p>You will use the template in a later tour to render content.</p>",
"content": "<p>Click <b>Document Type with Template</b> to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type.</p><p>You will use the template in a later tour to render content.</p>",
"event": "click"
},
{
@@ -197,8 +197,8 @@
},
{
"element": "[data-element~='editor-property-settings'] [data-element='editor-add']",
"title": "Add editor",
"content": "When you add an editor you choose what the input method for this property will be. Click <b>Add editor</b> to open the editor picker dialog.",
"title": "Select editor",
"content": "When you select an editor you choose what the input method for this property will be. Click <b>Select editor</b> to open the editor picker dialog.",
"event": "click"
},
{

View File

@@ -1,4 +1,6 @@
using System.Globalization;
using System.Text.RegularExpressions;
using HeyRed.MarkdownSharp;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
@@ -303,6 +305,51 @@ internal class ContentMapDefinition : IMapDefinition
{
Properties = context.MapEnumerable<IProperty, ContentPropertyDto>(source.Properties).WhereNotNull()
};
var markdown = new Markdown();
var linkCheck = new Regex("<a[^>]+>", RegexOptions.IgnoreCase);
var evaluator = new MatchEvaluator(AddRelNoReferrer);
foreach (TVariant variant in target.Variants)
{
foreach (Tab<ContentPropertyDisplay> tab in variant.Tabs)
{
if (tab.Properties == null)
{
continue;
}
foreach (ContentPropertyDisplay property in tab.Properties)
{
if (string.IsNullOrEmpty(property.Description))
{
continue;
}
var description = markdown.Transform(property.Description);
property.Description = linkCheck.Replace(description, evaluator);
}
}
}
}
private string AddRelNoReferrer(Match m)
{
string result = m.Value;
if (!result.Contains("rel=", StringComparison.Ordinal))
{
result = result.Replace(">", " rel=\"noreferrer\">");
}
if (!result.Contains("class=", StringComparison.Ordinal))
{
result = result.Replace(">", " class=\"underline\">");
}
if (!result.Contains("target=", StringComparison.Ordinal))
{
result = result.Replace(">", " target=\"_blank\">");
}
return result;
}
// Umbraco.Code.MapAll -Segment -Language -DisplayName
@@ -359,7 +406,7 @@ internal class ContentMapDefinition : IMapDefinition
{
currentUser = currentIUserBackofficeUser;
}
else if(_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser is not null)
else if (_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser is not null)
{
currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
}

View File

@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Web.BackOffice.Security;
/// <summary>
/// Antiforgery implementation for the Umbraco back office
/// Anti-forgery implementation for the Umbraco back office
/// </summary>
/// <remarks>
/// This is a wrapper around the global/default <see cref="IAntiforgery" /> .net service. Because this service is a
@@ -33,14 +33,12 @@ public class BackOfficeAntiforgery : IBackOfficeAntiforgery
{
x.HeaderName = Constants.Web.AngularHeadername;
x.Cookie.Name = Constants.Web.CsrfValidationCookieName;
x.Cookie.SecurePolicy = globalSettings.CurrentValue.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
});
ServiceProvider container = services.BuildServiceProvider();
_internalAntiForgery = container.GetRequiredService<IAntiforgery>();
_globalSettings = globalSettings.CurrentValue;
globalSettings.OnChange(x =>
{
_globalSettings = x;
});
globalSettings.OnChange(x => _globalSettings = x);
}
/// <inheritdoc />

View File

@@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using System.Buffers;
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
@@ -42,7 +43,7 @@ public sealed class JsonDateTimeFormatAttribute : TypeFilterAttribute
{
var serializerSettings = new JsonSerializerSettings();
serializerSettings.Converters.Add(
new IsoDateTimeConverter { DateTimeFormat = _format });
new IsoDateTimeConverter { DateTimeFormat = _format, Culture = CultureInfo.InvariantCulture });
objectResult.Formatters.Clear();
objectResult.Formatters.Add(
new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options));

View File

@@ -1,4 +1,4 @@
// needs Markdown.Converter.js at the moment
// needs Markdown.Converter.js at the moment
(function () {
@@ -1590,7 +1590,7 @@
};
// takes the line as entered into the add link/as image dialog and makes
// sure the URL and the optinal title are "nice".
// sure the URL and the optional title are "nice".
function properlyEncoded(linkdef) {
return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
link = link.replace(/\?.*$/, function (querypart) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,20 +0,0 @@
/**
* @ngdoc filter
* @name umbraco.filters.simpleMarkdown
* @description
* Used when rendering a string as Markdown as HTML (i.e. with ng-bind-html). Allows use of **bold**, *italics*, ![images](url) and [links](url)
**/
angular.module("umbraco.filters").filter('simpleMarkdown', function () {
return function (text) {
if (!text) {
return '';
}
return text
.replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
.replace(/\*(.*)\*/gim, '<i>$1</i>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank' rel='noopener' class='underline'>$1</a>")
.replace(/\n/g, '<br />').trim();
};
});

View File

@@ -1,20 +0,0 @@
/**
* @ngdoc filter
* @name umbraco.filters.simpleMarkdown
* @description
* Used when rendering a string as Markdown as HTML (i.e. with ng-bind-html). Allows use of **bold**, *italics*, ![images](url) and [links](url)
**/
angular.module("umbraco.filters").filter('simpleMarkdown', function () {
return function (text) {
if (!text) {
return '';
}
return text
.replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
.replace(/\*(.*)\*/gim, '<i>$1</i>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank' class='underline'>$1</a>")
.replace(/\n/g, '<br />').trim();
};
});

View File

@@ -243,7 +243,7 @@ a:visited {
}
section {
background-image: url(../img/nonodesbg.jpg);
background-image: url(../img/nonodesbg.webp);
background-position: center center;
background-size: cover;
height: 100%;

View File

@@ -18,7 +18,8 @@
ng-class="{'trashed': vm.media.trashed}"
ng-src="{{vm.thumbnail}}"
title="{{vm.media.name}}"
alt="{{vm.media.name}}" />
alt="{{vm.media.name}}"
draggable="false" />
<umb-file-icon ng-if="vm.loading === false && vm.icon"
icon="{{vm.icon}}"

View File

@@ -13,12 +13,12 @@
<umb-property-actions actions="vm.propertyActions"></umb-property-actions>
<small class="control-description" ng-if="vm.property.description" ng-bind-html="vm.property.description | simpleMarkdown"></small>
<small class="control-description" ng-if="vm.property.description" ng-bind-html="vm.property.description"></small>
<div ng-if="vm.property.extendedDescription">
<div ng-if="vm.property.extendedDescriptionVisible">
<small class="control-description" ng-bind-html="vm.property.extendedDescription | simpleMarkdown"></small>
<small class="control-description" ng-bind-html="vm.property.extendedDescription"></small>
</div>
<button type="button" class="btn btn-mini btn-link btn-link-reverse p0" ng-click="vm.property.extendedDescriptionVisible = !vm.property.extendedDescriptionVisible">

View File

@@ -43,7 +43,7 @@
<localize key="actions_chooseWhereToImport">Chose where to import</localize>
<localize key="dictionaryListCaption">dictionary items</localize>.
</strong>
(optinal)
(optional)
</p>
<umb-tree section="translation"

View File

@@ -1,7 +1,7 @@
<div>
<div ng-if="model.language" class="umb-alert umb-alert--warning mb2">
<localize key="defaultdialogs_languagedeletewarning">This will delete the language</localize> <strong>{{model.language.name}} [{{model.language.culture}}]</strong>.
<localize key="defaultdialogs_languagedeletewarning">This will delete the language and all content related to the language</localize> <strong>{{model.language.name}} [{{model.language.culture}}]</strong>.
</div>
<localize key="defaultdialogs_confirmdelete">Are you sure you want to delete</localize>?

View File

@@ -49,7 +49,7 @@
$attrs.$observe('readonly', (value) => {
vm.readonly = value !== undefined;
vm.sortableOptions.disabled = vm.readonly;
vm.sortableOptions.disabled = vm.readonly || vm.singleBlockMode;
vm.blockEditorApi.readonly = vm.readonly;
if (deleteAllBlocksAction) {
@@ -107,11 +107,13 @@
inlineEditing = vm.model.config.useInlineEditingAsDefault;
liveEditing = vm.model.config.useLiveEditing;
vm.singleBlockMode =
vm.model.config.validationLimit.min == 1 &&
vm.model.config.validationLimit.max == 1 &&
vm.model.config.blocks.length == 1 &&
vm.model.config.useSingleBlockMode;
vm.blockEditorApi.singleBlockMode = vm.singleBlockMode;
vm.validationLimit = vm.model.config.validationLimit;
@@ -123,17 +125,33 @@
}
// We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated.
if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object.
if (typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object.
vm.model.value = {};
}
var scopeOfExistence = $scope;
if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
if (vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
scopeOfExistence = vm.umbVariantContentEditors.getScope();
} else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) {
scopeOfExistence = vm.umbElementEditorContent.getScope();
}
vm.sortableOptions = {
axis: "y",
containment: "parent",
cursor: "grabbing",
handle: ".blockelement__draggable-element",
cancel: "input,textarea,select,option",
classes: ".blockelement--dragging",
distance: 5,
tolerance: "pointer",
scroll: true,
disabled: vm.readonly || vm.singleBlockMode,
update: function (ev, ui) {
setDirty();
}
};
copyAllBlocksAction = {
labelKey: "clipboard_labelForCopyAllEntries",
labelTokens: [vm.model.label],
@@ -519,6 +537,7 @@
}
vm.requestShowCreate = requestShowCreate;
function requestShowCreate(createIndex, mouseEvent) {
if (vm.blockTypePicker) {
@@ -539,12 +558,15 @@
}
}
vm.requestShowClipboard = requestShowClipboard;
function requestShowClipboard(createIndex) {
showCreateDialog(createIndex, true);
}
vm.showCreateDialog = showCreateDialog;
function showCreateDialog(createIndex, openClipboard) {
if (vm.blockTypePicker) {
@@ -616,6 +638,7 @@
editorService.open(blockPickerModel);
};
function userFlowWhenBlockWasCreated(createIndex) {
if (vm.layout.length > createIndex) {
var blockObject = vm.layout[createIndex].$block;
@@ -676,7 +699,7 @@
return b.date - a.date
});
if(firstTime !== true && vm.clipboardItems.length > oldAmount) {
if (firstTime !== true && vm.clipboardItems.length > oldAmount) {
jumpClipboard();
}
@@ -686,7 +709,7 @@
var jumpClipboardTimeout;
function jumpClipboard() {
if(jumpClipboardTimeout) {
if (jumpClipboardTimeout) {
return;
}
@@ -835,28 +858,13 @@
singleBlockMode: vm.singleBlockMode
};
vm.sortableOptions = {
axis: "y",
containment: "parent",
cursor: "grabbing",
handle: ".blockelement__draggable-element",
cancel: "input,textarea,select,option",
classes: ".blockelement--dragging",
distance: 5,
tolerance: "pointer",
scroll: true,
disabled: vm.readonly,
update: function (ev, ui) {
setDirty();
}
};
function onAmountOfBlocksChanged() {
// enable/disable property actions
if (copyAllBlocksAction) {
copyAllBlocksAction.isDisabled = vm.layout.length === 0;
}
if (deleteAllBlocksAction) {
deleteAllBlocksAction.isDisabled = vm.layout.length === 0 || vm.readonly;
}

View File

@@ -103,20 +103,6 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso
}
};
// sortable options
$scope.sortableOptions = {
axis: "y",
containment: "parent",
distance: 10,
opacity: 0.7,
tolerance: "pointer",
scroll: true,
zIndex: 6000,
update: function (e, ui) {
setDirty();
}
};
let removeAllEntriesAction = {
labelKey: "clipboard_labelForRemoveAllEntries",
labelTokens: [],
@@ -153,6 +139,21 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso
$scope.maxNumberOfItems = $scope.model.config.maxNumber ? parseInt($scope.model.config.maxNumber) : 0;
}
// sortable options
$scope.sortableOptions = {
axis: "y",
containment: "parent",
distance: 10,
opacity: 0.7,
tolerance: "pointer",
scroll: true,
zIndex: 6000,
disabled: $scope.readonly || $scope.maxNumberOfItems === 1,
update: function (e, ui) {
setDirty();
}
};
//Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that!
$scope.model.config.multiPicker = Object.toBoolean($scope.model.config.multiPicker);
$scope.model.config.showOpenButton = Object.toBoolean($scope.model.config.showOpenButton);

View File

@@ -34,9 +34,9 @@
<!-- Both min and max items -->
<span ng-if="minNumberOfItems && maxNumberOfItems && minNumberOfItems !== maxNumberOfItems">
<span ng-if="renderModel.length < maxNumberOfItems">Add between {{minNumberOfItems}} and {{maxNumberOfItems}} items</span>
<span ng-if="renderModel.length < maxNumberOfItems">Add between {{minNumberOfItems}} and {{maxNumberOfItems}} item(s)</span>
<span ng-if="renderModel.length > maxNumberOfItems">
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected"> items selected</localize>
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected"> item(s) selected</localize>
</span>
</span>
@@ -44,7 +44,7 @@
<span ng-if="minNumberOfItems && maxNumberOfItems && minNumberOfItems === maxNumberOfItems">
<span ng-if="renderModel.length < maxNumberOfItems">Add {{minNumberOfItems - renderModel.length}} item(s)</span>
<span ng-if="renderModel.length > maxNumberOfItems">
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected"> items selected</localize>
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected"> item(s) selected</localize>
</span>
</span>
@@ -52,7 +52,7 @@
<span ng-if="!minNumberOfItems && maxNumberOfItems">
<span ng-if="renderModel.length < maxNumberOfItems">Add up to {{maxNumberOfItems}} items</span>
<span ng-if="renderModel.length > maxNumberOfItems">
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected">items selected</localize>
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected">item(s) selected</localize>
</span>
</span>
@@ -71,12 +71,12 @@
<div ng-messages="contentPickerForm.minCount.$error" show-validation-on-submit>
<div class="help-inline" ng-message="minCount">
<localize key="validation_minCount">You need to add at least</localize> {{minNumberOfItems}} <localize key="validation_items">items</localize>
<localize key="validation_minCount">You need to add at least</localize> {{minNumberOfItems}} <localize key="validation_items">item(s)</localize>
</div>
</div>
<div ng-messages="contentPickerForm.maxCount.$error" show-validation-on-submit>
<div class="help-inline" ng-message="maxCount">
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected">items selected</localize>
<localize key="validation_maxCount">You can only have</localize> {{maxNumberOfItems}} <localize key="validation_itemsSelected">item(s) selected</localize>
</div>
</div>

View File

@@ -49,7 +49,8 @@
ng-if="media.$dataURL"
ng-src="{{media.$dataURL}}"
title="{{media.name}}"
alt="{{media.name}}" />
alt="{{media.name}}"
draggable="false" />
<umb-file-icon ng-if="!media.$dataURL && media.$icon"
icon="{{media.$icon}}"

View File

@@ -77,7 +77,7 @@
vm.allowEditMedia = !vm.readonly;
vm.allowDropMedia = !vm.readonly;
vm.sortableOptions.disabled = vm.readonly;
vm.sortableOptions.disabled = vm.readonly || vm.singleMode;
});
vm.$onInit = function() {
@@ -91,7 +91,7 @@
vm.validationLimit = vm.model.config.validationLimit || {};
// If single-mode we only allow 1 item as the maximum:
if(vm.model.config.multiple === false) {
if (vm.model.config.multiple === false) {
vm.validationLimit.max = 1;
}
vm.model.config.crops = vm.model.config.crops || [];
@@ -109,6 +109,20 @@
unsubscribe.push(mediaUploader.on('uploadSuccess', _handleMediaUploadSuccess));
unsubscribe.push(mediaUploader.on('queueCompleted', _handleMediaQueueCompleted));
vm.sortableOptions = {
cursor: "grabbing",
handle: "umb-media-card, .umb-media-card",
cancel: "input,textarea,select,option",
classes: ".umb-media-card--dragging",
distance: 5,
tolerance: "pointer",
scroll: true,
disabled: vm.readonly || vm.singleMode,
update: function (ev, ui) {
setDirty();
}
};
copyAllMediasAction = {
labelKey: "clipboard_labelForCopyAllEntries",
labelTokens: [vm.model.label],
@@ -137,7 +151,7 @@
vm.umbProperty.setPropertyActions(propertyActions);
}
if(vm.model.value === null || !Array.isArray(vm.model.value)) {
if (vm.model.value === null || !Array.isArray(vm.model.value)) {
vm.model.value = [];
}
@@ -231,7 +245,7 @@
function addMediaAt(createIndex, $event) {
if (!vm.allowAddMedia) return;
var mediaPicker = {
const mediaPicker = {
startNodeId: vm.model.config.startNodeId,
startNodeIsVirtual: vm.model.config.startNodeIsVirtual,
dataTypeKey: vm.model.dataTypeKey,
@@ -248,7 +262,7 @@
} else {
requestPasteFromClipboard(createIndex, item.data, item.type);
}
if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
if (!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
mediaPicker.close();
}
},
@@ -281,7 +295,7 @@
};
mediaPicker.clipboardItems = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null);
mediaPicker.clipboardItems.sort( (a, b) => {
mediaPicker.clipboardItems.sort((a, b) => {
return b.date - a.date
});
@@ -392,7 +406,7 @@
// make a clone to avoid editing model directly.
var mediaEntryClone = Utilities.copy(mediaEntry);
var mediaEditorModel = {
const mediaEditorModel = {
$parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing
$parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form)
createFlow: options.createFlow === true,
@@ -502,20 +516,6 @@
});
}
vm.sortableOptions = {
cursor: "grabbing",
handle: "umb-media-card, .umb-media-card",
cancel: "input,textarea,select,option",
classes: ".umb-media-card--dragging",
distance: 5,
tolerance: "pointer",
scroll: true,
disabled: vm.readonly,
update: function (ev, ui) {
setDirty();
}
};
function onAmountOfMediaChanged() {
// enable/disable property actions

View File

@@ -21,13 +21,19 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
$scope.renderModel = [];
if ($scope.model.config && parseInt($scope.model.config.maxNumber) !== 1 && $scope.umbProperty) {
var propertyActions = [
if ($scope.model.config) {
$scope.minNumberOfItems = $scope.model.config.minNumber ? parseInt($scope.model.config.minNumber) : 0;
$scope.maxNumberOfItems = $scope.model.config.maxNumber ? parseInt($scope.model.config.maxNumber) : 0;
if ($scope.umbProperty && $scope.maxNumberOfItems !== 1) {
let propertyActions = [
removeAllEntriesAction
];
$scope.umbProperty.setPropertyActions(propertyActions);
}
}
if (!Array.isArray($scope.model.value)) {
$scope.model.value = [];
@@ -41,7 +47,7 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
tolerance: "pointer",
scroll: true,
zIndex: 6000,
disabled: $scope.readonly,
disabled: $scope.readonly || $scope.maxNumberOfItems === 1,
update: function () {
setDirty();
}

View File

@@ -1,4 +1,4 @@
<div ng-controller="Umbraco.PropertyEditors.MultiUrlPickerController" class="umb-property-editor umb-contentpicker">
<div ng-controller="Umbraco.PropertyEditors.MultiUrlPickerController" class="umb-property-editor umb-contentpicker">
<p ng-if="(renderModel|filter:{trashed:true}).length == 1"><localize key="contentPicker_pickedTrashedItem">You have picked a content item currently deleted or in the recycle bin</localize></p>
<p ng-if="(renderModel|filter:{trashed:true}).length > 1"><localize key="contentPicker_pickedTrashedItems">You have picked content items currently deleted or in the recycle bin</localize></p>
@@ -29,12 +29,11 @@
<div class="umb-contentpicker__min-max-help">
<!-- Both min and max items -->
<span ng-if="model.config.minNumber && model.config.maxNumber && model.config.minNumber !== model.config.maxNumber">
<span ng-if="renderModel.length < model.config.maxNumber">Add between {{model.config.minNumber}} and {{model.config.maxNumber}} items</span>
<span ng-if="renderModel.length > model.config.maxNumber">
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected"> items selected</localize>
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected"> item(s) selected</localize>
</span>
</span>

View File

@@ -24,6 +24,9 @@
<AdditionalFiles Include="umbraco\UmbracoWebsite\Maintenance.cshtml" />
<AdditionalFiles Include="umbraco\UmbracoWebsite\NoNodes.cshtml" />
<AdditionalFiles Include="umbraco\UmbracoWebsite\NotFound.cshtml" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>

View File

@@ -36,6 +36,7 @@ public class PublishedContentQueryTests : ExamineBaseTest
public bool EnableDefaultEventHandler => throw new NotImplementedException();
public bool PublishedValuesOnly => throw new NotImplementedException();
public bool SupportProtectedContent => throw new NotImplementedException();
public IEnumerable<string> GetFields() => _fieldNames;
}

View File

@@ -28,6 +28,8 @@ public class ContentBuilderTests : DeliveryApiTests
var urlSegment = "url-segment";
var name = "The page";
ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, new[] { prop1, prop2 });
content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 06, 01));
content.SetupGet(c => c.UpdateDate).Returns(new DateTime(2023, 07, 12));
var publishedUrlProvider = new Mock<IPublishedUrlProvider>();
publishedUrlProvider
@@ -47,6 +49,8 @@ public class ContentBuilderTests : DeliveryApiTests
Assert.AreEqual(2, result.Properties.Count);
Assert.AreEqual("Delivery API value", result.Properties["deliveryApi"]);
Assert.AreEqual("Default value", result.Properties["default"]);
Assert.AreEqual(new DateTime(2023, 06, 01), result.CreateDate);
Assert.AreEqual(new DateTime(2023, 07, 12), result.UpdateDate);
}
[Test]

View File

@@ -17,8 +17,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
[TestFixture]
public class MarkdownEditorValueConverterTests : PropertyValueConverterTests
{
[TestCase("hello world", "<p>hello world</p>")]
[TestCase("hello *world*", "<p>hello <em>world</em></p>")]
[TestCase("hello world", "hello world")]
[TestCase("hello *world*", "hello *world*")]
[TestCase("", "")]
[TestCase(null, "")]
[TestCase(123, "")]

View File

@@ -183,6 +183,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Imaging.ImageSh
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Persistence.EFCore", "src\Umbraco.Cms.Persistence.EFCore\Umbraco.Cms.Persistence.EFCore.csproj", "{E8324C85-2854-4E07-8789-2D83DB978339}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Persistence.EFCore.Sqlite", "src\Umbraco.Cms.Persistence.EFCore.Sqlite\Umbraco.Cms.Persistence.EFCore.Sqlite.csproj", "{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Persistence.EFCore.SqlServer", "src\Umbraco.Cms.Persistence.EFCore.SqlServer\Umbraco.Cms.Persistence.EFCore.SqlServer.csproj", "{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -356,6 +360,30 @@ Global
{E8324C85-2854-4E07-8789-2D83DB978339}.Release|Any CPU.Build.0 = Release|Any CPU
{E8324C85-2854-4E07-8789-2D83DB978339}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{E8324C85-2854-4E07-8789-2D83DB978339}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.Release|Any CPU.Build.0 = Release|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{9046F56E-4AC3-4603-A6A3-3ACCF632997E}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.Release|Any CPU.Build.0 = Release|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{35E3DA10-5549-41DE-B7ED-CC29355BA9FD}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.Release|Any CPU.Build.0 = Release|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{8B4771F0-8EC2-4761-BBCE-DDE073DB3B7A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.Release|Any CPU.Build.0 = Release|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE