diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index d67ad0fe28..4cb593a39b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -72,10 +72,10 @@ Great question! The short version goes like this:

- * **Switch to the correct branch** - switch to the `v8/contrib` branch
+ * **Switch to the correct branch** - switch to the `v9/contrib` branch
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
- * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first.
+ * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v9/contrib`, create a new branch first.
* **Push** - great, now you can push the changes up to your fork on GitHub
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
@@ -173,7 +173,7 @@ To find the general areas for something you're looking to fix or improve, have a
### Which branch should I target for my contributions?
-We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
+We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v9/contrib`. If you are working on v9, this is the branch you should be targetting. For v8 contributions, please target 'v8/contrib'
Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.
@@ -199,10 +199,10 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
-git rebase upstream/v8/contrib
+git rebase upstream/v9/contrib
```
-In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed.
+In this command we're syncing with the `v9/contrib` branch, but you can of course choose another one if needed.
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
diff --git a/.github/workflows/codeql-config.yml b/.github/config/codeql-config.yml
similarity index 100%
rename from .github/workflows/codeql-config.yml
rename to .github/config/codeql-config.yml
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3d08bf3a4b..ee912262d7 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -56,7 +56,7 @@ jobs:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
with:
- config-file: ./.github/workflows/codeql-config.yml
+ config-file: ./.github/config/codeql-config.yml
# This job is to prevent the workflow status from showing as failed when all other jobs are skipped - See https://github.community/t/workflow-is-failing-if-no-job-can-be-ran-due-to-condition/16873
always_job:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 1c7f8b11d7..ab97269d1f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,6 +14,7 @@
"args": [],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
+ "requireExactSource": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
diff --git a/NuGet.Config b/NuGet.Config
deleted file mode 100644
index a0d5bc0481..0000000000
--- a/NuGet.Config
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets
index d271653e44..70081e0677 100644
--- a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets
+++ b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets
@@ -6,6 +6,16 @@
umbraco
+
+ $(DefaultItemExcludes);App_Plugins\**;
+
+ $(DefaultItemExcludes);umbraco\Data\**;
+ $(DefaultItemExcludes);umbraco\Logs\**;
+ $(DefaultItemExcludes);umbraco\mediacache\**;
+
+ $(DefaultItemExcludes);wwwroot\media\**;
+
+
@@ -21,9 +31,47 @@
SourceFiles="@(ContentWwwrootFiles)"
DestinationFiles="@(ContentWwwrootFiles->'$(MSBuildProjectDirectory)\wwwroot\$(UmbracoWwwrootName)\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
-
+
+
+ <_AppPluginsFiles Include="App_Plugins\**" />
+
+
+
+
+
+
+
+
+ <_UmbracoFolderFiles Include="umbraco\config\**" />
+ <_UmbracoFolderFiles Include="umbraco\PartialViewMacros\**" />
+ <_UmbracoFolderFiles Include="umbraco\UmbracoBackOffice\**" />
+ <_UmbracoFolderFiles Include="umbraco\UmbracoInstall\**" />
+ <_UmbracoFolderFiles Include="umbraco\UmbracoWebsite\**" />
+ <_UmbracoFolderFiles Include="umbraco\UmbracoWebsite\**" />
+ <_UmbracoFolderFiles Include="umbraco\Licenses\**" />
+
+
+ <_UmbracoFolderFiles Include="umbraco\Deploy\**" />
+
+
+
+
+
+
diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml
index f7d94f1b8b..a699d05148 100644
--- a/build/azure-pipelines.yml
+++ b/build/azure-pipelines.yml
@@ -226,14 +226,14 @@ stages:
inputs:
command: ci
workingDir: 'tests\Umbraco.Tests.AcceptanceTest'
- - task: Npm@1
+ - task: PowerShell@2
displayName: Run Cypress (Desktop)
condition: always()
continueOnError: true
inputs:
- workingDir: tests\Umbraco.Tests.AcceptanceTest
- command: 'custom'
- customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"'
+ targetType: inline
+ workingDirectory: tests\Umbraco.Tests.AcceptanceTest
+ script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"'
- task: PublishTestResults@2
condition: always()
@@ -259,7 +259,6 @@ stages:
# customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"'
- task: PublishPipelineArtifact@1
displayName: "Publish test artifacts"
- condition: failed()
inputs:
targetPath: '$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts'
artifact: 'Test artifacts - Windows'
@@ -329,14 +328,14 @@ stages:
inputs:
command: ci
workingDir: 'tests/Umbraco.Tests.AcceptanceTest'
- - task: Npm@1
+ - task: Bash@3
displayName: Run Cypress (Desktop)
condition: always()
continueOnError: true
inputs:
- workingDir: tests/Umbraco.Tests.AcceptanceTest
- command: 'custom'
- customCommand: 'run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"'
+ targetType: inline
+ workingDirectory: tests/Umbraco.Tests.AcceptanceTest
+ script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"'
- task: PublishTestResults@2
condition: always()
inputs:
@@ -361,13 +360,11 @@ stages:
# customCommand: 'run test -- --config="viewportHeight=812,viewportWidth=375,screenshotsFolder=cypress/artifacts/mobile/screenshots,videosFolder=cypress/artifacts/mobile/videos,videoUploadOnPasses=false"'
- task: PublishPipelineArtifact@1
displayName: "Publish test artifacts"
- condition: failed()
inputs:
targetPath: '$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts'
artifact: 'Test artifacts - Linux'
- task: PublishPipelineArtifact@1
displayName: "Publish run log"
- condition: failed()
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/dotnet_run_log_linux.txt'
artifact: Test Run logs - Linux
@@ -541,7 +538,7 @@ stages:
inputs:
targetType: inline
script: |
- choco install docfx -y
+ choco install docfx --version=2.58.5 -y
if ($lastexitcode -ne 0){
throw ("Error installing DocFX")
}
diff --git a/build/build.ps1 b/build/build.ps1
index 8a0b9dfaaf..da4733d432 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -449,12 +449,6 @@
if ($this.OnError()) { return }
})
- $ubuild.DefineMethod("PrepareAzureGallery",
- {
- Write-Host "Prepare Azure Gallery"
- $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput)
- })
-
$ubuild.DefineMethod("PrepareCSharpDocs",
{
Write-Host "Prepare C# Documentation"
@@ -528,8 +522,6 @@
if ($this.OnError()) { return }
$this.PackageNuGet()
if ($this.OnError()) { return }
- $this.PrepareAzureGallery()
- if ($this.OnError()) { return }
$this.PostPackageHook()
if ($this.OnError()) { return }
diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json
index 247dda7d72..4fa6e23e21 100644
--- a/build/templates/UmbracoPackage/.template.config/template.json
+++ b/build/templates/UmbracoPackage/.template.config/template.json
@@ -24,7 +24,7 @@
"version": {
"type": "parameter",
"datatype": "string",
- "defaultValue": "9.1.0",
+ "defaultValue": "9.2.0-rc",
"description": "The version of Umbraco to load using NuGet",
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
},
diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json
index 397f751676..2236e809a1 100644
--- a/build/templates/UmbracoProject/.template.config/template.json
+++ b/build/templates/UmbracoProject/.template.config/template.json
@@ -57,7 +57,7 @@
"version": {
"type": "parameter",
"datatype": "string",
- "defaultValue": "9.1.0",
+ "defaultValue": "9.2.0-rc",
"description": "The version of Umbraco to load using NuGet",
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
},
diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj
index 088cfab554..ebbaa1bc11 100644
--- a/build/templates/UmbracoProject/UmbracoProject.csproj
+++ b/build/templates/UmbracoProject/UmbracoProject.csproj
@@ -1,41 +1,28 @@
-
+
net6.0
Umbraco.Cms.Web.UI
- $(DefaultItemExcludes);App_Plugins/**;
- $(DefaultItemExcludes);umbraco/**;
- $(DefaultItemExcludes);wwwroot/media/**;
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
true
diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json
index feb6b07d95..99e877812c 100644
--- a/build/templates/UmbracoProject/appsettings.json
+++ b/build/templates/UmbracoProject/appsettings.json
@@ -17,6 +17,7 @@
"CMS": {
//#if (HasNoNodesViewPath || UseHttpsRedirect)
"Global": {
+ "SanitizeTinyMce": true,
//#if (!HasNoNodesViewPath && UseHttpsRedirect)
"UseHttps": true
//#elseif (UseHttpsRedirect)
@@ -25,10 +26,16 @@
//#if (HasNoNodesViewPath)
"NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE"
//#endif
+
},
//#endif
"Hosting": {
"Debug": false
+ },
+ "Content": {
+ "ContentVersionCleanupPolicy": {
+ "EnableCleanup": true
+ }
}
}
}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 5fb6242c02..fed70283e7 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,10 +3,10 @@
- 9.1.0
- 9.1.0
- 9.1.0
- 9.1.0
+ 9.2.0
+ 9.2.0
+ 9.2.0-rc
+ 9.2.0
9.0
en-US
Umbraco CMS
diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs
index 1b7c6d46fc..4af6685d8a 100644
--- a/src/JsonSchema/AppSettings.cs
+++ b/src/JsonSchema/AppSettings.cs
@@ -1,82 +1,120 @@
-using Umbraco.Cms.Core.Configuration.Models;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Forms.Core.Configuration;
using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings;
namespace JsonSchema
{
- public class AppSettings
+ internal class AppSettings
{
+ ///
+ /// Gets or sets the Umbraco
+ ///
public UmbracoDefinition Umbraco { get; set; }
///
- /// Configuration of Umbraco CMS and packages
+ /// Configuration of Umbraco CMS and packages
///
- public class UmbracoDefinition
+ internal class UmbracoDefinition
{
+ // ReSharper disable once InconsistentNaming
public CmsDefinition CMS { get; set; }
+
public FormsDefinition Forms { get; set; }
+
public DeployDefinition Deploy { get; set; }
///
- /// Configurations for the Umbraco CMS
+ /// Configurations for the Umbraco CMS
///
public class CmsDefinition
{
public ActiveDirectorySettings ActiveDirectory { get; set; }
+
public ContentSettings Content { get; set; }
+
public ExceptionFilterSettings ExceptionFilter { get; set; }
+
public ModelsBuilderSettings ModelsBuilder { get; set; }
+
public GlobalSettings Global { get; set; }
+
public HealthChecksSettings HealthChecks { get; set; }
+
public HostingSettings Hosting { get; set; }
+
public ImagingSettings Imaging { get; set; }
+
public IndexCreatorSettings Examine { get; set; }
+
public KeepAliveSettings KeepAlive { get; set; }
+
public LoggingSettings Logging { get; set; }
+
public MemberPasswordConfigurationSettings MemberPassword { get; set; }
+
public NuCacheSettings NuCache { get; set; }
+
public RequestHandlerSettings RequestHandler { get; set; }
+
public RuntimeSettings Runtime { get; set; }
+
public SecuritySettings Security { get; set; }
+
public TourSettings Tours { get; set; }
+
public TypeFinderSettings TypeFinder { get; set; }
+
public UserPasswordConfigurationSettings UserPassword { get; set; }
+
public WebRoutingSettings WebRouting { get; set; }
+
public UmbracoPluginSettings Plugins { get; set; }
+
public UnattendedSettings Unattended { get; set; }
+
public RichTextEditorSettings RichTextEditor { get; set; }
+
public RuntimeMinificationSettings RuntimeMinification { get; set; }
+
public BasicAuthSettings BasicAuth { get; set; }
+
public PackageMigrationSettings PackageMigration { get; set; }
}
///
- /// Configurations for the Umbraco Forms package to Umbraco CMS
+ /// Configurations for the Umbraco Forms package to Umbraco CMS
///
public class FormsDefinition
{
public FormDesignSettings FormDesign { get; set; }
+
public PackageOptionSettings Options { get; set; }
+
public Umbraco.Forms.Core.Configuration.SecuritySettings Security { get; set; }
+
public FieldTypesDefinition FieldTypes { get; set; }
///
- /// Configurations for the Umbraco Forms Field Types
+ /// Configurations for the Umbraco Forms Field Types
///
public class FieldTypesDefinition
{
public DatePickerSettings DatePicker { get; set; }
+
public Recaptcha2Settings Recaptcha2 { get; set; }
+
public Recaptcha3Settings Recaptcha3 { get; set; }
}
}
///
- /// Configurations for the Umbraco Deploy package to Umbraco CMS
+ /// Configurations for the Umbraco Deploy package to Umbraco CMS
///
public class DeployDefinition
{
-
}
}
}
diff --git a/src/JsonSchema/NamespacePrefixedSchemaNameGenerator.cs b/src/JsonSchema/NamespacePrefixedSchemaNameGenerator.cs
index 77b7b8c474..54ce0fdedf 100644
--- a/src/JsonSchema/NamespacePrefixedSchemaNameGenerator.cs
+++ b/src/JsonSchema/NamespacePrefixedSchemaNameGenerator.cs
@@ -1,13 +1,13 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using NJsonSchema.Generation;
namespace JsonSchema
{
- public class NamespacePrefixedSchemaNameGenerator : DefaultSchemaNameGenerator
+ internal class NamespacePrefixedSchemaNameGenerator : DefaultSchemaNameGenerator
{
- public override string Generate(Type type)
- {
- return type.Namespace.Replace(".", String.Empty) + base.Generate(type);
- }
+ public override string Generate(Type type) => type.Namespace.Replace(".", string.Empty) + base.Generate(type);
}
}
diff --git a/src/JsonSchema/Options.cs b/src/JsonSchema/Options.cs
new file mode 100644
index 0000000000..9930210cd8
--- /dev/null
+++ b/src/JsonSchema/Options.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using CommandLine;
+
+namespace JsonSchema
+{
+ internal class Options
+ {
+ [Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "../../../../Umbraco.Web.UI/umbraco/config/appsettings-schema.json")]
+ public string OutputFile { get; set; }
+ }
+}
diff --git a/src/JsonSchema/Program.cs b/src/JsonSchema/Program.cs
index cd09093020..8b02068c46 100644
--- a/src/JsonSchema/Program.cs
+++ b/src/JsonSchema/Program.cs
@@ -1,3 +1,6 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
using System;
using System.IO;
using System.Threading.Tasks;
@@ -5,14 +8,8 @@ using CommandLine;
namespace JsonSchema
{
- class Program
+ internal class Program
{
- private class Options
- {
- [Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "../../../../Umbraco.Web.UI/umbraco/config/appsettings-schema.json")]
- public string OutputFile { get; set; }
- }
-
public static async Task Main(string[] args)
{
try
@@ -25,7 +22,6 @@ namespace JsonSchema
Console.WriteLine(e);
throw;
}
-
}
private static async Task Execute(Options options)
@@ -34,7 +30,7 @@ namespace JsonSchema
var schema = await generator.Generate();
var path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, options.OutputFile));
- Console.WriteLine("Path to use {0}",path);
+ Console.WriteLine("Path to use {0}", path);
Directory.CreateDirectory(Path.GetDirectoryName(path));
Console.WriteLine("Ensured directory exists");
await File.WriteAllTextAsync(path, schema);
diff --git a/src/JsonSchema/UmbracoJsonSchemaGenerator.cs b/src/JsonSchema/UmbracoJsonSchemaGenerator.cs
index b6e516d0c5..e06189d3b4 100644
--- a/src/JsonSchema/UmbracoJsonSchemaGenerator.cs
+++ b/src/JsonSchema/UmbracoJsonSchemaGenerator.cs
@@ -1,4 +1,7 @@
-using System.Net.Http;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -7,28 +10,26 @@ using NJsonSchema.Generation;
namespace JsonSchema
{
///
- /// Generator of the JsonSchema for AppSettings.json including A specific Umbraco version.
+ /// Generator of the JsonSchema for AppSettings.json including A specific Umbraco version.
///
public class UmbracoJsonSchemaGenerator
{
+ private static readonly HttpClient s_client = new ();
private readonly JsonSchemaGenerator _innerGenerator;
- private static readonly HttpClient s_client = new HttpClient();
///
- /// Creates a new instance of .
+ /// Initializes a new instance of the class.
///
- /// The prefix to use for definitions generated.
public UmbracoJsonSchemaGenerator()
=> _innerGenerator = new JsonSchemaGenerator(new UmbracoJsonSchemaGeneratorSettings());
-
///
- /// Generates a json representing the JsonSchema for AppSettings.json including A specific Umbraco version..
+ /// Generates a json representing the JsonSchema for AppSettings.json including A specific Umbraco version..
///
public async Task Generate()
{
- var umbracoSchema = GenerateUmbracoSchema();
- var officialSchema = await GetOfficialAppSettingsSchema();
+ JObject umbracoSchema = GenerateUmbracoSchema();
+ JObject officialSchema = await GetOfficialAppSettingsSchema();
officialSchema.Merge(umbracoSchema);
@@ -37,19 +38,17 @@ namespace JsonSchema
private async Task GetOfficialAppSettingsSchema()
{
+ HttpResponseMessage response = await s_client.GetAsync("https://json.schemastore.org/appsettings.json")
+ .ConfigureAwait(false);
- var response = await s_client.GetAsync("https://json.schemastore.org/appsettings.json");
-
-
- var result = await response.Content.ReadAsStringAsync();
+ var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject(result);
-
}
private JObject GenerateUmbracoSchema()
{
- var schema = _innerGenerator.Generate(typeof(AppSettings));
+ NJsonSchema.JsonSchema schema = _innerGenerator.Generate(typeof(AppSettings));
return JsonConvert.DeserializeObject(schema.ToJson());
}
diff --git a/src/JsonSchema/UmbracoJsonSchemaGeneratorSettings.cs b/src/JsonSchema/UmbracoJsonSchemaGeneratorSettings.cs
index 26af0faae2..46625aeb2c 100644
--- a/src/JsonSchema/UmbracoJsonSchemaGeneratorSettings.cs
+++ b/src/JsonSchema/UmbracoJsonSchemaGeneratorSettings.cs
@@ -1,4 +1,7 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
@@ -12,15 +15,14 @@ namespace JsonSchema
public class UmbracoJsonSchemaGeneratorSettings : JsonSchemaGeneratorSettings
{
///
- /// Creates a new instance of .
+ /// Initializes a new instance of the class.
///
- /// The prefix to use for definitions generated.
public UmbracoJsonSchemaGeneratorSettings()
{
AlwaysAllowAdditionalObjectProperties = true;
SerializerSettings = new JsonSerializerSettings()
{
- ContractResolver = new WritablePropertiesOnlyResolver()
+ ContractResolver = new WritablePropertiesOnlyResolver()
};
DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull;
SchemaNameGenerator = new NamespacePrefixedSchemaNameGenerator();
diff --git a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs
index ddd9f96c73..670574896e 100644
--- a/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs
+++ b/src/Umbraco.Core/Cache/FastDictionaryAppCache.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -18,6 +18,8 @@ namespace Umbraco.Cms.Core.Cache
///
private readonly ConcurrentDictionary> _items = new ConcurrentDictionary>();
+ public IEnumerable Keys => _items.Keys;
+
public int Count => _items.Count;
///
diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs
index 9bf9c365f0..b760ffe646 100644
--- a/src/Umbraco.Core/Collections/StackQueue.cs
+++ b/src/Umbraco.Core/Collections/StackQueue.cs
@@ -3,58 +3,37 @@
namespace Umbraco.Core.Collections
{
///
- /// Collection that can be both a queue and a stack.
+ /// Collection that can be both a queue and a stack.
///
///
public class StackQueue
{
- private readonly LinkedList _linkedList = new LinkedList();
+ private readonly LinkedList _linkedList = new();
- public void Clear()
- {
- _linkedList.Clear();
- }
+ public int Count => _linkedList.Count;
- public void Push(T obj)
- {
- _linkedList.AddFirst(obj);
- }
+ public void Clear() => _linkedList.Clear();
- public void Enqueue(T obj)
- {
- _linkedList.AddFirst(obj);
- }
+ public void Push(T obj) => _linkedList.AddFirst(obj);
+
+ public void Enqueue(T obj) => _linkedList.AddFirst(obj);
public T Pop()
{
- var obj = _linkedList.First.Value;
+ T obj = _linkedList.First.Value;
_linkedList.RemoveFirst();
return obj;
}
public T Dequeue()
{
- var obj = _linkedList.Last.Value;
+ T obj = _linkedList.Last.Value;
_linkedList.RemoveLast();
return obj;
}
- public T PeekStack()
- {
- return _linkedList.First.Value;
- }
+ public T PeekStack() => _linkedList.First.Value;
- public T PeekQueue()
- {
- return _linkedList.Last.Value;
- }
-
- public int Count
- {
- get
- {
- return _linkedList.Count;
- }
- }
+ public T PeekQueue() => _linkedList.Last.Value;
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index dcca8c2adb..1caa81d80a 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -219,6 +219,9 @@ namespace Umbraco.Cms.Core.Configuration.Models
[DefaultValue(StaticLoginLogoImage)]
public string LoginLogoImage { get; set; } = StaticLoginLogoImage;
-
+ ///
+ /// Get or sets the model representing the global content version cleanup policy
+ ///
+ public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new ContentVersionCleanupPolicySettings();
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs
new file mode 100644
index 0000000000..bd460058eb
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs
@@ -0,0 +1,33 @@
+using System.ComponentModel;
+
+namespace Umbraco.Cms.Core.Configuration.Models
+{
+ ///
+ /// Model representing the global content version cleanup policy
+ ///
+ public class ContentVersionCleanupPolicySettings
+ {
+ private const bool StaticEnableCleanup = false;
+ private const int StaticKeepAllVersionsNewerThanDays = 7;
+ private const int StaticKeepLatestVersionPerDayForDays = 90;
+
+ ///
+ /// Gets or sets a value indicating whether or not the cleanup job should be executed.
+ ///
+ [DefaultValue(StaticEnableCleanup)]
+ public bool EnableCleanup { get; set; } = StaticEnableCleanup;
+
+ ///
+ /// Gets or sets the number of days where all historical content versions are kept.
+ ///
+ [DefaultValue(StaticKeepAllVersionsNewerThanDays)]
+ public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays;
+
+ ///
+ /// Gets or sets the number of days where the latest historical content version for that day are kept.
+ ///
+ [DefaultValue(StaticKeepLatestVersionPerDayForDays)]
+ public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays;
+
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index 7799fec5ea..c880830274 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -28,6 +28,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const bool StaticDisableElectionForSingleServer = false;
internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml";
internal const string StaticSqlWriteLockTimeOut = "00:00:05";
+ internal const bool StaticSanitizeTinyMce = false;
///
/// Gets or sets a value for the reserved URLs.
@@ -157,6 +158,12 @@ namespace Umbraco.Cms.Core.Configuration.Models
///
public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
+ ///
+ /// Gets a value indicating whether TinyMCE scripting sanitization should be applied
+ ///
+ [DefaultValue(StaticSanitizeTinyMce)]
+ public bool SanitizeTinyMce => StaticSanitizeTinyMce;
+
///
/// An int value representing the time in milliseconds to lock the database for a write operation
///
diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
index d5b74b38dc..d016e3547b 100644
--- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
@@ -11,8 +11,6 @@ namespace Umbraco.Cms.Core.Configuration.Models
[UmbracoOptions(Constants.Configuration.ConfigPlugins)]
public class UmbracoPluginSettings
{
-
-
///
/// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser.
///
@@ -25,7 +23,8 @@ namespace Umbraco.Cms.Core.Configuration.Models
".jpg", ".jpeg", ".gif", ".png", ".svg", // images
".eot", ".ttf", ".woff", // fonts
".xml", ".json", ".config", // configurations
- ".lic" // license
+ ".lic", // license
+ ".map" // js map files
});
}
}
diff --git a/src/Umbraco.Core/Constants-Sql.cs b/src/Umbraco.Core/Constants-Sql.cs
new file mode 100644
index 0000000000..b57861c92a
--- /dev/null
+++ b/src/Umbraco.Core/Constants-Sql.cs
@@ -0,0 +1,18 @@
+namespace Umbraco.Cms.Core
+{
+ public static partial class Constants
+ {
+ public static class Sql
+ {
+ ///
+ /// The maximum amount of parameters that can be used in a query.
+ ///
+ ///
+ /// The actual limit is 2100
+ /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server),
+ /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches.
+ ///
+ public const int MaxParameterCount = 2000;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs
new file mode 100644
index 0000000000..3b5bf7aebb
--- /dev/null
+++ b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Umbraco.Cms.Core.DependencyInjection
+{
+ ///
+ /// Provides access to a request scoped service provider when available for cases where
+ /// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core.
+ ///
+ public interface IScopedServiceProvider
+ {
+ ///
+ /// Gets a request scoped service provider when available.
+ ///
+ ///
+ /// Can be null.
+ ///
+ IServiceProvider ServiceProvider { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
index 4fb335be84..21e5cc03ed 100644
--- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
+++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
@@ -1,6 +1,7 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Extensions;
+using System.Text;
namespace Umbraco.Cms.Core.Exceptions
{
@@ -86,18 +87,28 @@ namespace Umbraco.Cms.Core.Exceptions
private static string FormatMessage(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases)
{
- // TODO Add property group aliases to message
- return addedCompositionAlias.IsNullOrWhiteSpace()
- ? string.Format(
- "ContentType with alias '{0}' has an invalid composition " +
- "and there was a conflict on the following PropertyTypes: '{1}'. " +
- "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.",
- contentTypeAlias, string.Join(", ", propertyTypeAliases))
- : string.Format(
- "ContentType with alias '{0}' was added as a Composition to ContentType with alias '{1}', " +
- "but there was a conflict on the following PropertyTypes: '{2}'. " +
- "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.",
- addedCompositionAlias, contentTypeAlias, string.Join(", ", propertyTypeAliases));
+ var sb = new StringBuilder();
+
+ if (addedCompositionAlias.IsNullOrWhiteSpace())
+ {
+ sb.AppendFormat("Content type with alias '{0}' has an invalid composition.", contentTypeAlias);
+ }
+ else
+ {
+ sb.AppendFormat("Content type with alias '{0}' was added as a composition to content type with alias '{1}', but there was a conflict.", addedCompositionAlias, contentTypeAlias);
+ }
+
+ if (propertyTypeAliases.Length > 0)
+ {
+ sb.AppendFormat(" Property types must have a unique alias across all compositions, these aliases are duplicate: {0}.", string.Join(", ", propertyTypeAliases));
+ }
+
+ if (propertyGroupAliases.Length > 0)
+ {
+ sb.AppendFormat(" Property groups with the same alias must also have the same type across all compositions, these aliases have different types: {0}.", string.Join(", ", propertyGroupAliases));
+ }
+
+ return sb.ToString();
}
///
diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
index 8c4e572442..197f6f6d63 100644
--- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
+++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs
@@ -1328,8 +1328,8 @@ namespace Umbraco.Extensions
{"NodeTypeAlias", "NodeTypeAlias"},
{"CreateDate", "CreateDate"},
{"UpdateDate", "UpdateDate"},
- {"CreatorName", "CreatorName"},
- {"WriterName", "WriterName"},
+ {"CreatorId", "CreatorId"},
+ {"WriterId", "WriterId"},
{"Url", "Url"}
};
diff --git a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs
index b714caf744..b4ffc40130 100644
--- a/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs
+++ b/src/Umbraco.Core/Extensions/PublishedModelFactoryExtensions.cs
@@ -12,7 +12,7 @@ namespace Umbraco.Extensions
public static class PublishedModelFactoryExtensions
{
///
- /// Returns true if the current is an implementation of and is enabled
+ /// Returns true if the current is an implementation of and is enabled
///
public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory)
{
@@ -21,8 +21,8 @@ namespace Umbraco.Extensions
return liveFactory.Enabled;
}
- // if it's not ILivePublishedModelFactory we can't determine if it's enabled or not so return true
- return true;
+ // if it's not ILivePublishedModelFactory we know we're not using a live factory
+ return false;
}
///
diff --git a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs
index e22bf86a59..502268ead0 100644
--- a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs
+++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs
@@ -226,7 +226,7 @@ namespace Umbraco.Cms.Core.Handlers
foreach (var perm in perms)
{
var group = _userService.GetUserGroupById(perm.UserGroupId);
- var assigned = string.Join(", ", perm.AssignedPermissions);
+ var assigned = string.Join(", ", perm?.AssignedPermissions ?? Array.Empty());
var entity = _entityService.Get(perm.EntityId);
_auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
index eeb291c41f..b7e5e867c4 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -20,12 +20,26 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
public abstract class BaseHttpHeaderCheck : HealthCheck
{
private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly ILocalizedTextService _textService;
private readonly string _header;
- private readonly string _value;
private readonly string _localizedTextPrefix;
private readonly bool _metaTagOptionAvailable;
private static HttpClient s_httpClient;
+ [Obsolete("Use ctor without value.")]
+ protected BaseHttpHeaderCheck(
+ IHostingEnvironment hostingEnvironment,
+ ILocalizedTextService textService,
+ string header,
+ string value,
+ string localizedTextPrefix,
+ bool metaTagOptionAvailable) :this(hostingEnvironment, textService, header, localizedTextPrefix, metaTagOptionAvailable)
+ {
+
+ }
+
+ [Obsolete("Save ILocalizedTextService in a field on the super class instead of using this")]
+ protected ILocalizedTextService LocalizedTextService => _textService;
///
/// Initializes a new instance of the class.
///
@@ -33,26 +47,18 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
IHostingEnvironment hostingEnvironment,
ILocalizedTextService textService,
string header,
- string value,
string localizedTextPrefix,
bool metaTagOptionAvailable)
{
- LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService));
+ _textService = textService ?? throw new ArgumentNullException(nameof(textService));
_hostingEnvironment = hostingEnvironment;
_header = header;
- _value = value;
_localizedTextPrefix = localizedTextPrefix;
_metaTagOptionAvailable = metaTagOptionAvailable;
}
private static HttpClient HttpClient => s_httpClient ??= new HttpClient();
-
- ///
- /// Gets the localized text service.
- ///
- protected ILocalizedTextService LocalizedTextService { get; }
-
///
/// Gets a link to an external read more page.
///
@@ -79,7 +85,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
var success = false;
// Access the site home page and check for the click-jack protection header or meta tag
- Uri url = _hostingEnvironment.ApplicationMainUrl;
+ var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority);
try
{
@@ -95,12 +101,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
}
message = success
- ? LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound")
- : LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound");
+ ? _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound")
+ : _textService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound");
}
catch (Exception ex)
{
- message = LocalizedTextService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
+ message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
}
return
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs
index 957ee0b715..8586989f32 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Hosting;
@@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
/// Initializes a new instance of the class.
///
public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService)
- : base(hostingEnvironment, textService, "X-Frame-Options", "sameorigin", "clickJacking", true)
+ : base(hostingEnvironment, textService, "X-Frame-Options", "clickJacking", true)
{
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
index cdd8a0493f..d5eac03038 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -53,7 +53,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
{
string message;
var success = false;
- var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority);;
+ var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority);
// Access the site home page and check for the headers
var request = new HttpRequestMessage(HttpMethod.Head, url);
@@ -65,7 +65,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
var headersToCheckFor = new List {"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" };
// Ignore if server header is present and it's set to cloudflare
- if (allHeaders.InvariantContains("Server") && response.Headers.TryGetValues("Server", out var serverHeaders) && serverHeaders.ToString().InvariantEquals("cloudflare"))
+ if (allHeaders.InvariantContains("Server") && response.Headers.TryGetValues("Server", out var serverHeaders) && serverHeaders.FirstOrDefault().InvariantEquals("cloudflare"))
{
headersToCheckFor.Remove("Server");
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs
index b2166b88bd..7902f4e3f8 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Hosting;
@@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
/// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites.
///
public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService)
- : base(hostingEnvironment, textService, "Strict-Transport-Security", "max-age=10886400", "hSTS", true)
+ : base(hostingEnvironment, textService, "Strict-Transport-Security", "hSTS", true)
{
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs
index 035733e4ee..78ee2c0e12 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Hosting;
@@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
/// Initializes a new instance of the class.
///
public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService)
- : base(hostingEnvironment, textService, "X-Content-Type-Options", "nosniff", "noSniff", false)
+ : base(hostingEnvironment, textService, "X-Content-Type-Options", "noSniff", false)
{
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs
index 6c05c39f46..570ca8002d 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Hosting;
@@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
/// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites.
///
public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService)
- : base(hostingEnvironment, textService, "X-XSS-Protection", "1; mode=block", "xssProtection", true)
+ : base(hostingEnvironment, textService, "X-XSS-Protection", "xssProtection", true)
{
}
diff --git a/src/Umbraco.Core/IO/DefaultViewContentProvider.cs b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs
new file mode 100644
index 0000000000..7c42255971
--- /dev/null
+++ b/src/Umbraco.Core/IO/DefaultViewContentProvider.cs
@@ -0,0 +1,62 @@
+using System.Text;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.IO
+{
+ public class DefaultViewContentProvider : IDefaultViewContentProvider
+ {
+ public string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null, string modelNamespace = null, string modelNamespaceAlias = null)
+ {
+ var content = new StringBuilder();
+
+ if (string.IsNullOrWhiteSpace(modelNamespaceAlias))
+ modelNamespaceAlias = "ContentModels";
+
+ // either
+ // @inherits Umbraco.Web.Mvc.UmbracoViewPage
+ // @inherits Umbraco.Web.Mvc.UmbracoViewPage
+ content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;");
+ content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage");
+ if (modelClassName.IsNullOrWhiteSpace() == false)
+ {
+ content.Append("<");
+ if (modelNamespace.IsNullOrWhiteSpace() == false)
+ {
+ content.Append(modelNamespaceAlias);
+ content.Append(".");
+ }
+ content.Append(modelClassName);
+ content.Append(">");
+ }
+ content.Append("\r\n");
+
+ // if required, add
+ // @using ContentModels = ModelNamespace;
+ if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false)
+ {
+ content.Append("@using ");
+ content.Append(modelNamespaceAlias);
+ content.Append(" = ");
+ content.Append(modelNamespace);
+ content.Append(";\r\n");
+ }
+
+ // either
+ // Layout = null;
+ // Layout = "layoutPage.cshtml";
+ content.Append("@{\r\n\tLayout = ");
+ if (layoutPageAlias.IsNullOrWhiteSpace())
+ {
+ content.Append("null");
+ }
+ else
+ {
+ content.Append("\"");
+ content.Append(layoutPageAlias);
+ content.Append(".cshtml\"");
+ }
+ content.Append(";\r\n}");
+ return content.ToString();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs
new file mode 100644
index 0000000000..8c1a775d7c
--- /dev/null
+++ b/src/Umbraco.Core/IO/IDefaultViewContentProvider.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Cms.Core.IO
+{
+ public interface IDefaultViewContentProvider
+ {
+ string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null,
+ string modelNamespace = null, string modelNamespaceAlias = null);
+ }
+}
diff --git a/src/Umbraco.Core/IO/IViewHelper.cs b/src/Umbraco.Core/IO/IViewHelper.cs
new file mode 100644
index 0000000000..d53dcbf2b9
--- /dev/null
+++ b/src/Umbraco.Core/IO/IViewHelper.cs
@@ -0,0 +1,13 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.IO
+{
+ public interface IViewHelper
+ {
+ bool ViewExists(ITemplate t);
+ string GetFileContents(ITemplate t);
+ string CreateView(ITemplate t, bool overWrite = false);
+ string UpdateViewFile(ITemplate t, string currentAlias = null);
+ string ViewPath(string alias);
+ }
+}
diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs
index 258c4a7f64..56a2760802 100644
--- a/src/Umbraco.Core/IO/ViewHelper.cs
+++ b/src/Umbraco.Core/IO/ViewHelper.cs
@@ -2,26 +2,34 @@ using System;
using System.IO;
using System.Linq;
using System.Text;
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
{
- public class ViewHelper
+ public class ViewHelper : IViewHelper
{
private readonly IFileSystem _viewFileSystem;
+ private readonly IDefaultViewContentProvider _defaultViewContentProvider;
+ [Obsolete("Use ctor with all params")]
public ViewHelper(IFileSystem viewFileSystem)
{
- if (viewFileSystem == null) throw new ArgumentNullException(nameof(viewFileSystem));
- _viewFileSystem = viewFileSystem;
+ _viewFileSystem = viewFileSystem ?? throw new ArgumentNullException(nameof(viewFileSystem));
+ _defaultViewContentProvider = StaticServiceProvider.Instance.GetRequiredService();
}
- internal bool ViewExists(ITemplate t)
+ public ViewHelper(FileSystems fileSystems, IDefaultViewContentProvider defaultViewContentProvider)
{
- return _viewFileSystem.FileExists(ViewPath(t.Alias));
+ _viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems));
+ _defaultViewContentProvider = defaultViewContentProvider ?? throw new ArgumentNullException(nameof(defaultViewContentProvider));
}
+ public bool ViewExists(ITemplate t) => _viewFileSystem.FileExists(ViewPath(t.Alias));
+
+
public string GetFileContents(ITemplate t)
{
var viewContent = "";
@@ -60,58 +68,13 @@ namespace Umbraco.Cms.Core.IO
return viewContent;
}
- public static string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null, string modelNamespace = null, string modelNamespaceAlias = null)
+ [Obsolete("Inject IDefaultViewContentProvider instead")]
+ public static string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null,
+ string modelNamespace = null, string modelNamespaceAlias = null)
{
- var content = new StringBuilder();
-
- if (string.IsNullOrWhiteSpace(modelNamespaceAlias))
- modelNamespaceAlias = "ContentModels";
-
- // either
- // @inherits Umbraco.Web.Mvc.UmbracoViewPage
- // @inherits Umbraco.Web.Mvc.UmbracoViewPage
- content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;");
- content.Append("@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage");
- if (modelClassName.IsNullOrWhiteSpace() == false)
- {
- content.Append("<");
- if (modelNamespace.IsNullOrWhiteSpace() == false)
- {
- content.Append(modelNamespaceAlias);
- content.Append(".");
- }
- content.Append(modelClassName);
- content.Append(">");
- }
- content.Append("\r\n");
-
- // if required, add
- // @using ContentModels = ModelNamespace;
- if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false)
- {
- content.Append("@using ");
- content.Append(modelNamespaceAlias);
- content.Append(" = ");
- content.Append(modelNamespace);
- content.Append(";\r\n");
- }
-
- // either
- // Layout = null;
- // Layout = "layoutPage.cshtml";
- content.Append("@{\r\n\tLayout = ");
- if (layoutPageAlias.IsNullOrWhiteSpace())
- {
- content.Append("null");
- }
- else
- {
- content.Append("\"");
- content.Append(layoutPageAlias);
- content.Append(".cshtml\"");
- }
- content.Append(";\r\n}");
- return content.ToString();
+ var viewContentProvider = StaticServiceProvider.Instance.GetRequiredService();
+ return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace,
+ modelNamespaceAlias);
}
private string SaveTemplateToFile(ITemplate template)
@@ -157,12 +120,12 @@ namespace Umbraco.Cms.Core.IO
return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml");
}
- private static string EnsureInheritedLayout(ITemplate template)
+ private string EnsureInheritedLayout(ITemplate template)
{
var design = template.Content;
if (string.IsNullOrEmpty(design))
- design = GetDefaultFileContent(template.MasterTemplateAlias);
+ design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias);
return design;
}
diff --git a/src/Umbraco.Core/Models/AuditType.cs b/src/Umbraco.Core/Models/AuditType.cs
index cea6c7ccf2..b6a36be5ff 100644
--- a/src/Umbraco.Core/Models/AuditType.cs
+++ b/src/Umbraco.Core/Models/AuditType.cs
@@ -113,6 +113,16 @@
///
/// Custom audit message.
///
- Custom
+ Custom,
+
+ ///
+ /// Content version preventCleanup set to true
+ ///
+ ContentVersionPreventCleanup,
+
+ ///
+ /// Content version preventCleanup set to false
+ ///
+ ContentVersionEnableCleanup
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
index 79f3fba1fd..6198f0c664 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
@@ -29,6 +29,9 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "allowedContentTypes")]
public IEnumerable AllowedContentTypes { get; set; }
+ [DataMember(Name = "historyCleanup")]
+ public HistoryCleanupViewModel HistoryCleanup { get; set; }
+
///
/// Custom validation
///
diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs
index 7d45c46600..3bb52c39b7 100644
--- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models.ContentEditing
@@ -6,11 +6,9 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataContract(Name = "contentType", Namespace = "")]
public class DocumentTypeDisplay : ContentTypeCompositionDisplay
{
- public DocumentTypeDisplay()
- {
+ public DocumentTypeDisplay() =>
//initialize collections so at least their never null
AllowedTemplates = new List();
- }
//name, alias, icon, thumb, desc, inherited from the content type
@@ -29,5 +27,8 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "apps")]
public IEnumerable ContentApps { get; set; }
+
+ [DataMember(Name = "historyCleanup")]
+ public HistoryCleanupViewModel HistoryCleanup { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
new file mode 100644
index 0000000000..b7bfb32808
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
@@ -0,0 +1,17 @@
+using System.Runtime.Serialization;
+
+namespace Umbraco.Cms.Core.Models.ContentEditing
+{
+ [DataContract(Name = "historyCleanup", Namespace = "")]
+ public class HistoryCleanup
+ {
+ [DataMember(Name = "preventCleanup")]
+ public bool PreventCleanup { get; set; }
+
+ [DataMember(Name = "keepAllVersionsNewerThanDays")]
+ public int? KeepAllVersionsNewerThanDays { get; set; }
+
+ [DataMember(Name = "keepLatestVersionPerDayForDays")]
+ public int? KeepLatestVersionPerDayForDays { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs
new file mode 100644
index 0000000000..303ff4eda3
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs
@@ -0,0 +1,18 @@
+using System.Runtime.Serialization;
+
+namespace Umbraco.Cms.Core.Models.ContentEditing
+{
+ [DataContract(Name = "historyCleanup", Namespace = "")]
+ public class HistoryCleanupViewModel : HistoryCleanup
+ {
+
+ [DataMember(Name = "globalEnableCleanup")]
+ public bool GlobalEnableCleanup { get; set; }
+
+ [DataMember(Name = "globalKeepAllVersionsNewerThanDays")]
+ public int? GlobalKeepAllVersionsNewerThanDays { get; set; }
+
+ [DataMember(Name = "globalKeepLatestVersionPerDayForDays")]
+ public int? GlobalKeepLatestVersionPerDayForDays { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs
index ae5b4805a7..551cbbc0ee 100644
--- a/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupPermissionsSave.cs
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
/// Used to assign user group permissions to a content node
///
[DataContract(Name = "contentPermission", Namespace = "")]
- public class UserGroupPermissionsSave : IValidatableObject
+ public class UserGroupPermissionsSave
{
public UserGroupPermissionsSave()
{
@@ -28,13 +28,5 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "permissions")]
public IDictionary> AssignedPermissions { get; set; }
-
- public IEnumerable Validate(ValidationContext validationContext)
- {
- if (AssignedPermissions.SelectMany(x => x.Value).Any(x => x.IsNullOrWhiteSpace()))
- {
- yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" });
- }
- }
}
}
diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs
index 9a0e1a6854..6ff94f57f3 100644
--- a/src/Umbraco.Core/Models/ContentType.cs
+++ b/src/Umbraco.Core/Models/ContentType.cs
@@ -1,37 +1,45 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models
{
///
- /// Represents the content type that a object is based on
+ /// Represents the content type that a object is based on
///
[Serializable]
[DataContract(IsReference = true)]
- public class ContentType : ContentTypeCompositionBase, IContentType
+ public class ContentType : ContentTypeCompositionBase, IContentTypeWithHistoryCleanup
{
public const bool SupportsPublishingConst = true;
- private int _defaultTemplate;
+ // Custom comparer for enumerable
+ private static readonly DelegateEqualityComparer> TemplateComparer = new (
+ (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
+ templates => templates.GetHashCode());
+
private IEnumerable _allowedTemplates;
+ private int _defaultTemplate;
+
///
- /// Constuctor for creating a ContentType with the parent's id.
+ /// Constuctor for creating a ContentType with the parent's id.
///
/// Only use this for creating ContentTypes at the root (with ParentId -1).
///
public ContentType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId)
{
_allowedTemplates = new List();
+ HistoryCleanup = new HistoryCleanup();
}
///
- /// Constuctor for creating a ContentType with the parent as an inherited type.
+ /// Constuctor for creating a ContentType with the parent as an inherited type.
///
/// Use this to ensure inheritance from parent.
///
@@ -40,30 +48,24 @@ namespace Umbraco.Cms.Core.Models
: base(shortStringHelper, parent, alias)
{
_allowedTemplates = new List();
+ HistoryCleanup = new HistoryCleanup();
}
- ///
- public override ISimpleContentType ToSimple() => new SimpleContentType(this);
-
///
public override bool SupportsPublishing => SupportsPublishingConst;
- //Custom comparer for enumerable
- private static readonly DelegateEqualityComparer> TemplateComparer = new DelegateEqualityComparer>(
- (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
- templates => templates.GetHashCode());
+ ///
+ public override ISimpleContentType ToSimple() => new SimpleContentType(this);
///
- /// Gets or sets the alias of the default Template.
- /// TODO: This should be ignored from cloning!!!!!!!!!!!!!!
- /// - but to do that we have to implement callback hacks, this needs to be fixed in v8,
+ /// Gets or sets the alias of the default Template.
+ /// TODO: This should be ignored from cloning!!!!!!!!!!!!!!
+ /// - but to do that we have to implement callback hacks, this needs to be fixed in v8,
/// we should not store direct entity
///
[IgnoreDataMember]
- public ITemplate DefaultTemplate
- {
- get { return AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); }
- }
+ public ITemplate DefaultTemplate =>
+ AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId);
[DataMember]
@@ -74,9 +76,9 @@ namespace Umbraco.Cms.Core.Models
}
///
- /// Gets or Sets a list of Templates which are allowed for the ContentType
- /// TODO: This should be ignored from cloning!!!!!!!!!!!!!!
- /// - but to do that we have to implement callback hacks, this needs to be fixed in v8,
+ /// Gets or Sets a list of Templates which are allowed for the ContentType
+ /// TODO: This should be ignored from cloning!!!!!!!!!!!!!!
+ /// - but to do that we have to implement callback hacks, this needs to be fixed in v8,
/// we should not store direct entity
///
[DataMember]
@@ -88,38 +90,38 @@ namespace Umbraco.Cms.Core.Models
SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer);
if (_allowedTemplates.Any(x => x.Id == _defaultTemplate) == false)
+ {
DefaultTemplateId = 0;
+ }
}
}
+ public HistoryCleanup HistoryCleanup { get; set; }
+
///
- /// Determines if AllowedTemplates contains templateId
+ /// Determines if AllowedTemplates contains templateId
///
/// The template id to check
/// True if AllowedTemplates contains the templateId else False
- public bool IsAllowedTemplate(int templateId)
- {
- return AllowedTemplates == null
+ public bool IsAllowedTemplate(int templateId) =>
+ AllowedTemplates == null
? false
: AllowedTemplates.Any(t => t.Id == templateId);
- }
///
- /// Determines if AllowedTemplates contains templateId
+ /// Determines if AllowedTemplates contains templateId
///
/// The template alias to check
/// True if AllowedTemplates contains the templateAlias else False
- public bool IsAllowedTemplate(string templateAlias)
- {
- return AllowedTemplates == null
+ public bool IsAllowedTemplate(string templateAlias) =>
+ AllowedTemplates == null
? false
: AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase));
- }
///
- /// Sets the default template for the ContentType
+ /// Sets the default template for the ContentType
///
- /// Default
+ /// Default
public void SetDefaultTemplate(ITemplate template)
{
if (template == null)
@@ -138,17 +140,19 @@ namespace Umbraco.Cms.Core.Models
}
///
- /// Removes a template from the list of allowed templates
+ /// Removes a template from the list of allowed templates
///
- /// to remove
+ /// to remove
/// True if template was removed, otherwise False
public bool RemoveTemplate(ITemplate template)
{
if (DefaultTemplateId == template.Id)
- DefaultTemplateId = default(int);
+ {
+ DefaultTemplateId = default;
+ }
var templates = AllowedTemplates.ToList();
- var remove = templates.FirstOrDefault(x => x.Id == template.Id);
+ ITemplate remove = templates.FirstOrDefault(x => x.Id == template.Id);
var result = templates.Remove(remove);
AllowedTemplates = templates;
@@ -156,6 +160,7 @@ namespace Umbraco.Cms.Core.Models
}
///
- IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias);
+ IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) =>
+ (IContentType)DeepCloneWithResetIdentities(newAlias);
}
}
diff --git a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs
new file mode 100644
index 0000000000..5fa0e98958
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Umbraco.Cms.Core.Models
+{
+ public class ContentVersionCleanupPolicySettings
+ {
+ public int ContentTypeId { get; set; }
+
+ public bool PreventCleanup { get; set; }
+
+ public int? KeepAllVersionsNewerThanDays { get; set; }
+
+ public int? KeepLatestVersionPerDayForDays { get; set; }
+
+ public DateTime Updated { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs
new file mode 100644
index 0000000000..d2e53e0985
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentVersionMeta.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace Umbraco.Cms.Core.Models
+{
+ public class ContentVersionMeta
+ {
+ public int ContentId { get; }
+ public int ContentTypeId { get; }
+ public int VersionId { get; }
+ public int UserId { get; }
+
+ public DateTime VersionDate { get; }
+ public bool CurrentPublishedVersion { get; }
+ public bool CurrentDraftVersion { get; }
+ public bool PreventCleanup { get; }
+ public string Username { get; }
+
+ public ContentVersionMeta() { }
+
+ public ContentVersionMeta(
+ int versionId,
+ int contentId,
+ int contentTypeId,
+ int userId,
+ DateTime versionDate,
+ bool currentPublishedVersion,
+ bool currentDraftVersion,
+ bool preventCleanup,
+ string username)
+ {
+ VersionId = versionId;
+ ContentId = contentId;
+ ContentTypeId = contentTypeId;
+
+ UserId = userId;
+ VersionDate = versionDate;
+ CurrentPublishedVersion = currentPublishedVersion;
+ CurrentDraftVersion = currentDraftVersion;
+ PreventCleanup = preventCleanup;
+ Username = username;
+ }
+
+ public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
+ }
+}
diff --git a/src/Umbraco.Core/Models/IContentType.cs b/src/Umbraco.Core/Models/IContentType.cs
index f04a73d5e0..a01e612887 100644
--- a/src/Umbraco.Core/Models/IContentType.cs
+++ b/src/Umbraco.Core/Models/IContentType.cs
@@ -1,56 +1,67 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Core.Models
{
+ [Obsolete("This will be merged into IContentType in Umbraco 10")]
+ public interface IContentTypeWithHistoryCleanup : IContentType
+ {
+ ///
+ /// Gets or Sets the history cleanup configuration
+ ///
+ HistoryCleanup HistoryCleanup { get; set; }
+ }
+
///
- /// Defines a ContentType, which Content is based on
+ /// Defines a ContentType, which Content is based on
///
public interface IContentType : IContentTypeComposition
{
///
- /// Internal property to store the Id of the default template
+ /// Internal property to store the Id of the default template
///
int DefaultTemplateId { get; set; }
///
- /// Gets the default Template of the ContentType
+ /// Gets the default Template of the ContentType
///
ITemplate DefaultTemplate { get; }
///
- /// Gets or Sets a list of Templates which are allowed for the ContentType
+ /// Gets or Sets a list of Templates which are allowed for the ContentType
///
IEnumerable AllowedTemplates { get; set; }
///
- /// Determines if AllowedTemplates contains templateId
+ /// Determines if AllowedTemplates contains templateId
///
/// The template id to check
/// True if AllowedTemplates contains the templateId else False
bool IsAllowedTemplate(int templateId);
///
- /// Determines if AllowedTemplates contains templateId
+ /// Determines if AllowedTemplates contains templateId
///
/// The template alias to check
/// True if AllowedTemplates contains the templateAlias else False
bool IsAllowedTemplate(string templateAlias);
///
- /// Sets the default template for the ContentType
+ /// Sets the default template for the ContentType
///
- /// Default
+ /// Default
void SetDefaultTemplate(ITemplate template);
///
- /// Removes a template from the list of allowed templates
+ /// Removes a template from the list of allowed templates
///
- /// to remove
+ /// to remove
/// True if template was removed, otherwise False
bool RemoveTemplate(ITemplate template);
///
- /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
+ /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
///
///
///
diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
index d3269c2c0b..debaa976c5 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
@@ -2,9 +2,9 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Hosting;
@@ -13,31 +13,50 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models.Mapping
{
///
- /// Defines mappings for content/media/members type mappings
+ /// Defines mappings for content/media/members type mappings
///
public class ContentTypeMapDefinition : IMapDefinition
{
private readonly CommonMapper _commonMapper;
- private readonly PropertyEditorCollection _propertyEditors;
+ private readonly IContentTypeService _contentTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly IFileService _fileService;
- private readonly IContentTypeService _contentTypeService;
- private readonly IMediaTypeService _mediaTypeService;
- private readonly IMemberTypeService _memberTypeService;
- private readonly ILoggerFactory _loggerFactory;
- private readonly ILogger _logger;
- private readonly IShortStringHelper _shortStringHelper;
private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
+ private readonly IMediaTypeService _mediaTypeService;
+ private readonly IMemberTypeService _memberTypeService;
+ private readonly PropertyEditorCollection _propertyEditors;
+ private readonly IShortStringHelper _shortStringHelper;
+ private ContentSettings _contentSettings;
- public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IFileService fileService,
- IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService,
- ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, IHostingEnvironment hostingEnvironment)
+
+ [Obsolete("Use ctor with all params injected")]
+ public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors,
+ IDataTypeService dataTypeService, IFileService fileService,
+ IContentTypeService contentTypeService, IMediaTypeService mediaTypeService,
+ IMemberTypeService memberTypeService,
+ ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings,
+ IHostingEnvironment hostingEnvironment)
+ : this(commonMapper, propertyEditors, dataTypeService, fileService, contentTypeService, mediaTypeService,
+ memberTypeService, loggerFactory, shortStringHelper, globalSettings, hostingEnvironment,
+ StaticServiceProvider.Instance.GetRequiredService>())
+ {
+ }
+
+ public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors,
+ IDataTypeService dataTypeService, IFileService fileService,
+ IContentTypeService contentTypeService, IMediaTypeService mediaTypeService,
+ IMemberTypeService memberTypeService,
+ ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings,
+ IHostingEnvironment hostingEnvironment, IOptionsMonitor contentSettings)
{
_commonMapper = commonMapper;
_propertyEditors = propertyEditors;
@@ -51,13 +70,19 @@ namespace Umbraco.Cms.Core.Models.Mapping
_shortStringHelper = shortStringHelper;
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
+
+ _contentSettings = contentSettings.CurrentValue;
+ contentSettings.OnChange(x => _contentSettings = x);
}
public void DefineMaps(IUmbracoMapper mapper)
{
- mapper.Define((source, context) => new ContentType(_shortStringHelper, source.ParentId), Map);
- mapper.Define((source, context) => new MediaType(_shortStringHelper, source.ParentId), Map);
- mapper.Define((source, context) => new MemberType(_shortStringHelper, source.ParentId), Map);
+ mapper.Define(
+ (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map);
+ mapper.Define(
+ (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map);
+ mapper.Define(
+ (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map);
mapper.Define((source, context) => new DocumentTypeDisplay(), Map);
mapper.Define((source, context) => new MediaTypeDisplay(), Map);
@@ -66,14 +91,20 @@ namespace Umbraco.Cms.Core.Models.Mapping
mapper.Define(
(source, context) =>
{
- var dataType = _dataTypeService.GetDataType(source.DataTypeId);
- if (dataType == null) throw new NullReferenceException("No data type found with id " + source.DataTypeId);
+ IDataType dataType = _dataTypeService.GetDataType(source.DataTypeId);
+ if (dataType == null)
+ {
+ throw new NullReferenceException("No data type found with id " + source.DataTypeId);
+ }
+
return new PropertyType(_shortStringHelper, dataType, source.Alias);
}, Map);
// TODO: isPublishing in ctor?
- mapper.Define, PropertyGroup>((source, context) => new PropertyGroup(false), Map);
- mapper.Define, PropertyGroup>((source, context) => new PropertyGroup(false), Map);
+ mapper.Define, PropertyGroup>(
+ (source, context) => new PropertyGroup(false), Map);
+ mapper.Define, PropertyGroup>(
+ (source, context) => new PropertyGroup(false), Map);
mapper.Define((source, context) => new ContentTypeBasic(), Map);
mapper.Define((source, context) => new ContentTypeBasic(), Map);
@@ -84,11 +115,14 @@ namespace Umbraco.Cms.Core.Models.Mapping
mapper.Define((source, context) => new MediaTypeDisplay(), Map);
mapper.Define((source, context) => new MemberTypeDisplay(), Map);
- mapper.Define, PropertyGroupDisplay>((source, context) => new PropertyGroupDisplay(), Map);
- mapper.Define, PropertyGroupDisplay>((source, context) => new PropertyGroupDisplay(), Map);
+ mapper.Define, PropertyGroupDisplay>(
+ (source, context) => new PropertyGroupDisplay(), Map);
+ mapper.Define, PropertyGroupDisplay>(
+ (source, context) => new PropertyGroupDisplay(), Map);
mapper.Define((source, context) => new PropertyTypeDisplay(), Map);
- mapper.Define((source, context) => new MemberPropertyTypeDisplay(), Map);
+ mapper.Define(
+ (source, context) => new MemberPropertyTypeDisplay(), Map);
}
// no MapAll - take care
@@ -97,13 +131,20 @@ namespace Umbraco.Cms.Core.Models.Mapping
MapSaveToTypeBase(source, target, context);
MapComposition(source, target, alias => _contentTypeService.Get(alias));
+ if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup)
+ {
+ targetWithHistoryCleanup.HistoryCleanup = source.HistoryCleanup;
+ }
+
target.AllowedTemplates = source.AllowedTemplates
.Where(x => x != null)
.Select(_fileService.GetTemplate)
.Where(x => x != null)
.ToArray();
- target.SetDefaultTemplate(source.DefaultTemplate == null ? null : _fileService.GetTemplate(source.DefaultTemplate));
+ target.SetDefaultTemplate(source.DefaultTemplate == null
+ ? null
+ : _fileService.GetTemplate(source.DefaultTemplate));
}
// no MapAll - take care
@@ -119,11 +160,16 @@ namespace Umbraco.Cms.Core.Models.Mapping
MapSaveToTypeBase(source, target, context);
MapComposition(source, target, alias => _memberTypeService.Get(alias));
- foreach (var propertyType in source.Groups.SelectMany(x => x.Properties))
+ foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties))
{
- var localCopy = propertyType;
- var destProp = target.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias));
- if (destProp == null) continue;
+ MemberPropertyTypeBasic localCopy = propertyType;
+ IPropertyType destProp =
+ target.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias));
+ if (destProp == null)
+ {
+ continue;
+ }
+
target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty);
target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty);
target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData);
@@ -135,6 +181,24 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
MapTypeToDisplayBase(source, target);
+ if (source is IContentTypeWithHistoryCleanup sourceWithHistoryCleanup)
+ {
+ target.HistoryCleanup = new HistoryCleanupViewModel
+ {
+ PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup.PreventCleanup,
+ KeepAllVersionsNewerThanDays =
+ sourceWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays,
+ KeepLatestVersionPerDayForDays =
+ sourceWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays,
+ GlobalKeepAllVersionsNewerThanDays =
+ _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays,
+ GlobalKeepLatestVersionPerDayForDays =
+ _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays,
+ GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup
+ };
+ }
+
+
target.AllowCultureVariant = source.VariesByCulture();
target.AllowSegmentVariant = source.VariesBySegment();
target.ContentApps = _commonMapper.GetContentApps(source);
@@ -143,16 +207,23 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates);
if (source.DefaultTemplate != null)
+ {
target.DefaultTemplate = context.Map(source.DefaultTemplate);
+ }
//default listview
target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content";
- if (string.IsNullOrEmpty(source.Alias)) return;
+ if (string.IsNullOrEmpty(source.Alias))
+ {
+ return;
+ }
var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias;
if (_dataTypeService.GetDataType(name) != null)
+ {
target.ListViewEditorName = name;
+ }
}
// no MapAll - take care
@@ -164,11 +235,16 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media";
target.IsSystemMediaType = source.IsSystemMediaType();
- if (string.IsNullOrEmpty(source.Name)) return;
+ if (string.IsNullOrEmpty(source.Name))
+ {
+ return;
+ }
var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name;
if (_dataTypeService.GetDataType(name) != null)
+ {
target.ListViewEditorName = name;
+ }
}
// no MapAll - take care
@@ -177,11 +253,16 @@ namespace Umbraco.Cms.Core.Models.Mapping
MapTypeToDisplayBase(source, target);
//map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData
- foreach (var propertyType in source.PropertyTypes)
+ foreach (IPropertyType propertyType in source.PropertyTypes)
{
- var localCopy = propertyType;
- var displayProp = target.Groups.SelectMany(dest => dest.Properties).SingleOrDefault(dest => dest.Alias.InvariantEquals(localCopy.Alias));
- if (displayProp == null) continue;
+ IPropertyType localCopy = propertyType;
+ MemberPropertyTypeDisplay displayProp = target.Groups.SelectMany(dest => dest.Properties)
+ .SingleOrDefault(dest => dest.Alias.InvariantEquals(localCopy.Alias));
+ if (displayProp == null)
+ {
+ continue;
+ }
+
displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias);
displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias);
displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias);
@@ -216,28 +297,20 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// no MapAll - uses the IContentTypeBase map method, which has MapAll
- private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context)
- {
+ private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) =>
Map(source, target, Constants.UdiEntityType.MemberType);
- }
// no MapAll - uses the IContentTypeBase map method, which has MapAll
- private void Map(IContentType source, ContentTypeBasic target, MapperContext context)
- {
+ private void Map(IContentType source, ContentTypeBasic target, MapperContext context) =>
Map(source, target, Constants.UdiEntityType.DocumentType);
- }
// no MapAll - uses the IContentTypeBase map method, which has MapAll
- private void Map(IMediaType source, ContentTypeBasic target, MapperContext context)
- {
+ private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) =>
Map(source, target, Constants.UdiEntityType.MediaType);
- }
// no MapAll - uses the IContentTypeBase map method, which has MapAll
- private void Map(IMemberType source, ContentTypeBasic target, MapperContext context)
- {
+ private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) =>
Map(source, target, Constants.UdiEntityType.MemberType);
- }
// Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate
// Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations
@@ -254,10 +327,14 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant);
if (source.Id > 0)
+ {
target.Id = source.Id;
+ }
if (source.GroupId > 0)
+ {
target.PropertyGroupId = new Lazy(() => source.GroupId, false);
+ }
target.Alias = source.Alias;
target.Description = source.Description;
@@ -268,18 +345,19 @@ namespace Umbraco.Cms.Core.Models.Mapping
// no MapAll - take care
private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context)
{
- MapTypeToDisplayBase(source, target, context);
+ MapTypeToDisplayBase(source,
+ target, context);
//sync templates
- var destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias);
+ IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias);
//if the dest is set and it's the same as the source, then don't change
if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false)
{
- var templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray());
+ IEnumerable templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray());
target.AllowedTemplates = source.AllowedTemplates
.Select(x =>
{
- var template = templates.SingleOrDefault(t => t.Alias == x);
+ ITemplate template = templates.SingleOrDefault(t => t.Alias == x);
return template != null
? context.Map(template)
: null;
@@ -293,7 +371,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
//if the dest is set and it's the same as the source, then don't change
if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias)
{
- var template = _fileService.GetTemplate(source.DefaultTemplate);
+ ITemplate template = _fileService.GetTemplate(source.DefaultTemplate);
target.DefaultTemplate = template == null ? null : context.Map(template);
}
}
@@ -304,22 +382,24 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// no MapAll - take care
- private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context)
- {
- MapTypeToDisplayBase(source, target, context);
- }
+ private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) =>
+ MapTypeToDisplayBase(source,
+ target, context);
// no MapAll - take care
- private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context)
- {
- MapTypeToDisplayBase(source, target, context);
- }
+ private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) =>
+ MapTypeToDisplayBase(
+ source, target, context);
// Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes
- private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context)
+ private static void Map(PropertyGroupBasic source, PropertyGroup target,
+ MapperContext context)
{
if (source.Id > 0)
+ {
target.Id = source.Id;
+ }
+
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
@@ -328,10 +408,14 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes
- private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context)
+ private static void Map(PropertyGroupBasic source, PropertyGroup target,
+ MapperContext context)
{
if (source.Id > 0)
+ {
target.Id = source.Id;
+ }
+
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
@@ -340,11 +424,15 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames
- private static void Map(PropertyGroupBasic source, PropertyGroupDisplay target, MapperContext context)
+ private static void Map(PropertyGroupBasic source,
+ PropertyGroupDisplay target, MapperContext context)
{
target.Inherited = source.Inherited;
if (source.Id > 0)
+ {
target.Id = source.Id;
+ }
+
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
@@ -354,17 +442,22 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames
- private static void Map(PropertyGroupBasic source, PropertyGroupDisplay target, MapperContext context)
+ private static void Map(PropertyGroupBasic source,
+ PropertyGroupDisplay target, MapperContext context)
{
target.Inherited = source.Inherited;
if (source.Id > 0)
+ {
target.Id = source.Id;
+ }
+
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
target.Alias = source.Alias;
target.SortOrder = source.SortOrder;
- target.Properties = context.MapEnumerable(source.Properties);
+ target.Properties =
+ context.MapEnumerable(source.Properties);
}
// Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName
@@ -409,7 +502,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
// Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations
// Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate
// Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType)
- private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context)
+ private static void MapSaveToTypeBase(TSource source,
+ IContentTypeComposition target, MapperContext context)
where TSource : ContentTypeSave
where TSourcePropertyType : PropertyTypeBasic
{
@@ -418,7 +512,9 @@ namespace Umbraco.Cms.Core.Models.Mapping
var id = Convert.ToInt32(source.Id);
if (id > 0)
+ {
target.Id = id;
+ }
target.Alias = source.Alias;
target.Description = source.Description;
@@ -457,18 +553,19 @@ namespace Umbraco.Cms.Core.Models.Mapping
// - managing the content type's PropertyTypes collection (for generic properties)
// handle actual groups (non-generic-properties)
- var destOrigGroups = target.PropertyGroups.ToArray(); // local groups
- var destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not
+ PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups
+ IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not
var destGroups = new List();
- var sourceGroups = source.Groups.Where(x => x.IsGenericProperties == false).ToArray();
+ PropertyGroupBasic[] sourceGroups =
+ source.Groups.Where(x => x.IsGenericProperties == false).ToArray();
var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray();
- foreach (var sourceGroup in sourceGroups)
+ foreach (PropertyGroupBasic sourceGroup in sourceGroups)
{
// get the dest group
- var destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context);
+ PropertyGroup destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context);
// handle local properties
- var destProperties = sourceGroup.Properties
+ IPropertyType[] destProperties = sourceGroup.Properties
.Where(x => x.Inherited == false)
.Select(x => MapSaveProperty(x, destOrigProperties, context))
.ToArray();
@@ -476,7 +573,9 @@ namespace Umbraco.Cms.Core.Models.Mapping
// if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect
// local groups which would not have local properties anymore
if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias))
+ {
continue;
+ }
// ensure no duplicate alias, then assign the group properties collection
EnsureUniqueAliases(destProperties);
@@ -492,11 +591,12 @@ namespace Umbraco.Cms.Core.Models.Mapping
// the old groups - they are just gone and will be cleared by the repository
// handle non-grouped (ie generic) properties
- var genericPropertiesGroup = source.Groups.FirstOrDefault(x => x.IsGenericProperties);
+ PropertyGroupBasic genericPropertiesGroup =
+ source.Groups.FirstOrDefault(x => x.IsGenericProperties);
if (genericPropertiesGroup != null)
{
// handle local properties
- var destProperties = genericPropertiesGroup.Properties
+ IPropertyType[] destProperties = genericPropertiesGroup.Properties
.Where(x => x.Inherited == false)
.Select(x => MapSaveProperty(x, destOrigProperties, context))
.ToArray();
@@ -547,7 +647,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
MapTypeToDisplayBase(source, target);
- var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService, _shortStringHelper, _loggerFactory.CreateLogger>());
+ var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService,
+ _shortStringHelper, _loggerFactory.CreateLogger>());
target.Groups = groupsMapper.Map(source);
}
@@ -580,7 +681,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// no MapAll - relies on the non-generic method
- private void MapTypeToDisplayBase(TSource source, TTarget target, MapperContext context)
+ private void MapTypeToDisplayBase(TSource source,
+ TTarget target, MapperContext context)
where TSource : ContentTypeSave
where TSourcePropertyType : PropertyTypeBasic
where TTarget : ContentTypeCompositionDisplay
@@ -588,35 +690,49 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
MapTypeToDisplayBase(source, target);
- target.Groups = context.MapEnumerable, PropertyGroupDisplay>(source.Groups);
+ target.Groups =
+ context
+ .MapEnumerable, PropertyGroupDisplay>(
+ source.Groups);
}
private IEnumerable MapLockedCompositions(IContentTypeComposition source)
{
// get ancestor ids from path of parent if not root
if (source.ParentId == Constants.System.Root)
+ {
return Enumerable.Empty();
+ }
- var parent = _contentTypeService.Get(source.ParentId);
+ IContentType parent = _contentTypeService.Get(source.ParentId);
if (parent == null)
+ {
return Enumerable.Empty();
+ }
var aliases = new List();
- var ancestorIds = parent.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture));
+ IEnumerable ancestorIds = parent.Path.Split(Constants.CharArrays.Comma)
+ .Select(s => int.Parse(s, CultureInfo.InvariantCulture));
// loop through all content types and return ordered aliases of ancestors
- var allContentTypes = _contentTypeService.GetAll().ToArray();
+ IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray();
foreach (var ancestorId in ancestorIds)
{
- var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId);
+ IContentType ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId);
if (ancestor != null)
+ {
aliases.Add(ancestor.Alias);
+ }
}
+
return aliases.OrderBy(x => x);
}
public static Udi MapContentTypeUdi(IContentTypeComposition source)
{
- if (source == null) return null;
+ if (source == null)
+ {
+ return null;
+ }
string udiType;
switch (source)
@@ -637,7 +753,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
return Udi.Create(udiType, source.Key);
}
- private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups, MapperContext context)
+ private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup,
+ IEnumerable destOrigGroups, MapperContext context)
where TPropertyType : PropertyTypeBasic
{
PropertyGroup destGroup;
@@ -663,7 +780,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
return destGroup;
}
- private static IPropertyType MapSaveProperty(PropertyTypeBasic sourceProperty, IEnumerable destOrigProperties, MapperContext context)
+ private static IPropertyType MapSaveProperty(PropertyTypeBasic sourceProperty,
+ IEnumerable destOrigProperties, MapperContext context)
{
IPropertyType destProperty;
if (sourceProperty.Id > 0)
@@ -690,43 +808,52 @@ namespace Umbraco.Cms.Core.Models.Mapping
private static void EnsureUniqueAliases(IEnumerable properties)
{
- var propertiesA = properties.ToArray();
+ IPropertyType[] propertiesA = properties.ToArray();
var distinctProperties = propertiesA
.Select(x => x.Alias?.ToUpperInvariant())
.Distinct()
.Count();
if (distinctProperties != propertiesA.Length)
+ {
throw new InvalidOperationException("Cannot map properties due to alias conflict.");
+ }
}
private static void EnsureUniqueAliases(IEnumerable groups)
{
- var groupsA = groups.ToArray();
+ PropertyGroup[] groupsA = groups.ToArray();
var distinctProperties = groupsA
.Select(x => x.Alias)
.Distinct()
.Count();
if (distinctProperties != groupsA.Length)
+ {
throw new InvalidOperationException("Cannot map groups due to alias conflict.");
+ }
}
- private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, Func getContentType)
+ private static void MapComposition(ContentTypeSave source, IContentTypeComposition target,
+ Func getContentType)
{
var current = target.CompositionAliases().ToArray();
- var proposed = source.CompositeContentTypes;
+ IEnumerable proposed = source.CompositeContentTypes;
- var remove = current.Where(x => !proposed.Contains(x));
- var add = proposed.Where(x => !current.Contains(x));
+ IEnumerable remove = current.Where(x => !proposed.Contains(x));
+ IEnumerable add = proposed.Where(x => !current.Contains(x));
foreach (var alias in remove)
+ {
target.RemoveContentType(alias);
+ }
foreach (var alias in add)
{
// TODO: Remove N+1 lookup
- var contentType = getContentType(alias);
+ IContentTypeComposition contentType = getContentType(alias);
if (contentType != null)
+ {
target.AddContentType(contentType);
+ }
}
}
}
diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
index 2380657180..e4d101ff06 100644
--- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
@@ -251,7 +251,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
// the entity service due to too many Sql parameters.
var list = new List();
- foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000))
+ foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount))
list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray()));
contentEntities = list.ToArray();
}
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs
index c29df4e85f..7f8d852115 100644
--- a/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationComponentsInstallingNotification.cs
@@ -1,15 +1,22 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
+
namespace Umbraco.Cms.Core.Notifications
{
+ // TODO (V10): Remove this class.
+
///
/// Notification that occurs during the Umbraco boot process, before instances of initialize.
///
+ [Obsolete("This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " +
+ "Following re-work they are no longer used (from Deploy 9.2.0)." +
+ "Given they are non-documented and no other use is expected, they can be removed in the next major release")]
public class UmbracoApplicationComponentsInstallingNotification : INotification
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The runtime level
public UmbracoApplicationComponentsInstallingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel;
diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs
index 6a66e2413f..66593ab086 100644
--- a/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs
+++ b/src/Umbraco.Core/Notifications/UmbracoApplicationMainDomAcquiredNotification.cs
@@ -1,11 +1,18 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
+
namespace Umbraco.Cms.Core.Notifications
{
+ // TODO (V10): Remove this class.
+
///
/// Notification that occurs during Umbraco boot after the MainDom has been acquired.
///
+ [Obsolete("This notification was added to the core runtime start-up as a hook for Umbraco Cloud local connection string and database setup. " +
+ "Following re-work they are no longer used (from Deploy 9.2.0)." +
+ "Given they are non-documented and no other use is expected, they can be removed in the next major release")]
public class UmbracoApplicationMainDomAcquiredNotification : INotification
{
///
diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
index f0f891d48e..680eee5ba2 100644
--- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
+++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
@@ -26,6 +26,8 @@ namespace Umbraco.Cms.Core
public const string Content = TableNamePrefix + "Content";
public const string ContentVersion = TableNamePrefix + "ContentVersion";
public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation";
+ public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy";
+
public const string Document = TableNamePrefix + "Document";
public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation";
public const string DocumentVersion = TableNamePrefix + "DocumentVersion";
@@ -50,6 +52,7 @@ namespace Umbraco.Cms.Core
public const string User2UserGroup = TableNamePrefix + "User2UserGroup";
public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify";
public const string UserGroup2App = TableNamePrefix + "UserGroup2App";
+ public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node";
public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission";
public const string ExternalLogin = TableNamePrefix + "ExternalLogin";
public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken";
diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs
new file mode 100644
index 0000000000..38f425d5a1
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+ public interface IDocumentVersionRepository : IRepository
+ {
+ ///
+ /// Gets a list of all historic content versions.
+ ///
+ public IReadOnlyCollection GetDocumentVersionsEligibleForCleanup();
+
+ ///
+ /// Gets cleanup policy override settings per content type.
+ ///
+ public IReadOnlyCollection GetCleanupPolicies();
+
+ ///
+ /// Gets paginated content versions for given content id paginated.
+ ///
+ public IEnumerable GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null);
+
+ ///
+ /// Deletes multiple content versions by ID.
+ ///
+ void DeleteVersions(IEnumerable versionIds);
+
+ ///
+ /// Updates the prevent cleanup flag on a content version.
+ ///
+ void SetPreventCleanup(int versionId, bool preventCleanup);
+
+ ///
+ /// Gets the content version metadata for a specific version.
+ ///
+ ContentVersionMeta Get(int versionId);
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs
index 716e722be1..8510c982ab 100644
--- a/src/Umbraco.Core/PropertyEditors/VoidEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/VoidEditor.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Serialization;
@@ -16,8 +16,6 @@ namespace Umbraco.Cms.Core.PropertyEditors
[HideFromTypeFinder]
public class VoidEditor : DataEditor
{
- private readonly IJsonSerializer _jsonSerializer;
-
///
/// Initializes a new instance of the class.
///
diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs
index 34152ebc0f..2c79e68e1d 100644
--- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs
+++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
-
+ // TODO: Only used in unit tests, needs to be moved to test project
+ [EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedContent : IPublishedContent
{
public InternalPublishedContent(IPublishedContentType contentType)
diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs
index 2a197affff..c2f8f27419 100644
--- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs
+++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Xml;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
+ // TODO: Only used in unit tests, needs to be moved to test project
+ [EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache
{
private readonly Dictionary _content = new Dictionary();
diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs
index a98eff6484..7fe46d8d75 100644
--- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs
+++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedProperty.cs
@@ -1,7 +1,10 @@
-using Umbraco.Cms.Core.Models.PublishedContent;
+using System.ComponentModel;
+using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
+ // TODO: Only used in unit tests, needs to be moved to test project
+ [EditorBrowsable(EditorBrowsableState.Never)]
public class InternalPublishedProperty : IPublishedProperty
{
public IPublishedPropertyType PropertyType { get; set; }
diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs
index c73b6cef76..1ff8d99139 100644
--- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs
+++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshot.cs
@@ -1,8 +1,12 @@
using System;
+using System.ComponentModel;
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
+
+ // TODO: Only used in unit tests, needs to be moved to test project
+ [EditorBrowsable(EditorBrowsableState.Never)]
public sealed class InternalPublishedSnapshot : IPublishedSnapshot
{
public InternalPublishedContentCache InnerContentCache { get; } = new InternalPublishedContentCache();
diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs
index 5dcdc5189f..61e2a9c2a9 100644
--- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs
+++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedSnapshotService.cs
@@ -1,10 +1,13 @@
using System.Collections.Generic;
+using System.ComponentModel;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Cache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache.Internal
{
+ // TODO: Only used in unit tests, needs to be moved to test project
+ [EditorBrowsable(EditorBrowsableState.Never)]
public class InternalPublishedSnapshotService : IPublishedSnapshotService
{
private InternalPublishedSnapshot _snapshot;
diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
index 969e5bf98b..2a7bc97650 100644
--- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
@@ -1,117 +1,111 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Routing
{
///
- /// Provides urls.
+ /// Provides urls.
///
public class DefaultUrlProvider : IUrlProvider
{
- private RequestHandlerSettings _requestSettings;
+ private readonly ILocalizationService _localizationService;
+ private readonly ILocalizedTextService _localizedTextService;
private readonly ILogger _logger;
+ private RequestHandlerSettings _requestSettings;
private readonly ISiteDomainMapper _siteDomainMapper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly UriUtility _uriUtility;
- public DefaultUrlProvider(IOptionsMonitor requestSettings, ILogger logger, ISiteDomainMapper siteDomainMapper, IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility)
+ [Obsolete("Use ctor with all parameters")]
+ public DefaultUrlProvider(IOptionsMonitor requestSettings, ILogger logger,
+ ISiteDomainMapper siteDomainMapper, IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility)
+ : this(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ public DefaultUrlProvider(
+ IOptionsMonitor requestSettings,
+ ILogger logger,
+ ISiteDomainMapper siteDomainMapper,
+ IUmbracoContextAccessor umbracoContextAccessor,
+ UriUtility uriUtility,
+ ILocalizationService localizationService)
{
_requestSettings = requestSettings.CurrentValue;
_logger = logger;
_siteDomainMapper = siteDomainMapper;
- _uriUtility = uriUtility;
_umbracoContextAccessor = umbracoContextAccessor;
+ _uriUtility = uriUtility;
+ _localizationService = localizationService;
+
requestSettings.OnChange(x => _requestSettings = x);
}
- #region GetUrl
-
- ///
- public virtual UrlInfo GetUrl(IPublishedContent content, UrlMode mode, string culture, Uri current)
- {
- if (!current.IsAbsoluteUri) throw new ArgumentException("Current URL must be absolute.", nameof(current));
- var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
- // will not use cache if previewing
- var route = umbracoContext.Content.GetRouteById(content.Id, culture);
-
- return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture);
- }
-
- internal UrlInfo GetUrlFromRoute(string route, IUmbracoContext umbracoContext, int id, Uri current, UrlMode mode, string culture)
- {
- if (string.IsNullOrWhiteSpace(route))
- {
- _logger.LogDebug("Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.", id);
- return null;
- }
-
- // extract domainUri and path
- // route is / or /
- var pos = route.IndexOf('/');
- var path = pos == 0 ? route : route.Substring(pos);
- var domainUri = pos == 0
- ? null
- : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
-
- // assemble the URL from domainUri (maybe null) and path
- var url = AssembleUrl(domainUri, path, current, mode).ToString();
-
- return UrlInfo.Url(url, culture);
- }
-
- #endregion
-
#region GetOtherUrls
///
- /// Gets the other URLs of a published content.
+ /// Gets the other URLs of a published content.
///
/// The Umbraco context.
/// The published content id.
/// The current absolute URL.
/// The other URLs for the published content.
///
- /// Other URLs are those that GetUrl would not return in the current context, but would be valid
- /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...).
+ ///
+ /// Other URLs are those that GetUrl would not return in the current context, but would be valid
+ /// URLs for the node in other contexts (different domain for current request, umbracoUrlAlias...).
+ ///
///
public virtual IEnumerable GetOtherUrls(int id, Uri current)
{
- var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
- var node = umbracoContext.Content.GetById(id);
+ IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
+ IPublishedContent node = umbracoContext.Content.GetById(id);
if (node == null)
{
yield break;
}
// look for domains, walking up the tree
- var n = node;
- var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, false);
+ IPublishedContent n = node;
+ IEnumerable domainUris =
+ DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id,
+ current, false);
while (domainUris == null && n != null) // n is null at root
{
n = n.Parent; // move to parent node
- domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id, current, excludeDefault: true);
+ domainUris = n == null
+ ? null
+ : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, n.Id,
+ current);
}
// no domains = exit
- if (domainUris ==null)
+ if (domainUris == null)
{
yield break;
}
- foreach (var d in domainUris)
+ foreach (DomainAndUri d in domainUris)
{
var culture = d?.Culture;
// although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok
var route = umbracoContext.Content.GetRouteById(id, culture);
- if (route == null) continue;
+ if (route == null)
+ {
+ continue;
+ }
// need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned)
var pos = route.IndexOf('/');
@@ -125,9 +119,57 @@ namespace Umbraco.Cms.Core.Routing
#endregion
+ #region GetUrl
+
+ ///
+ public virtual UrlInfo GetUrl(IPublishedContent content, UrlMode mode, string culture, Uri current)
+ {
+ if (!current.IsAbsoluteUri)
+ {
+ throw new ArgumentException("Current URL must be absolute.", nameof(current));
+ }
+
+ IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
+ // will not use cache if previewing
+ var route = umbracoContext.Content.GetRouteById(content.Id, culture);
+
+ return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture);
+ }
+
+ internal UrlInfo GetUrlFromRoute(string route, IUmbracoContext umbracoContext, int id, Uri current,
+ UrlMode mode, string culture)
+ {
+ if (string.IsNullOrWhiteSpace(route))
+ {
+ _logger.LogDebug(
+ "Couldn't find any page with nodeId={NodeId}. This is most likely caused by the page not being published.",
+ id);
+ return null;
+ }
+
+ // extract domainUri and path
+ // route is / or /
+ var pos = route.IndexOf('/');
+ var path = pos == 0 ? route : route.Substring(pos);
+ var domainUri = pos == 0
+ ? null
+ : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
+
+ var defaultCulture = _localizationService.GetDefaultLanguageIsoCode();
+ if (domainUri is not null || culture == defaultCulture || culture is null)
+ {
+ var url = AssembleUrl(domainUri, path, current, mode).ToString();
+ return UrlInfo.Url(url, culture);
+ }
+
+ return null;
+ }
+
+ #endregion
+
#region Utilities
- Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode)
+ private Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode)
{
Uri uri;
@@ -136,7 +178,9 @@ namespace Umbraco.Cms.Core.Routing
if (domainUri == null) // no domain was found
{
if (current == null)
+ {
mode = UrlMode.Relative; // best we can do
+ }
switch (mode)
{
@@ -156,10 +200,15 @@ namespace Umbraco.Cms.Core.Routing
if (mode == UrlMode.Auto)
{
//this check is a little tricky, we can't just compare domains
- if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority))
+ if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) ==
+ current.GetLeftPart(UriPartial.Authority))
+ {
mode = UrlMode.Relative;
+ }
else
+ {
mode = UrlMode.Absolute;
+ }
}
switch (mode)
@@ -180,9 +229,9 @@ namespace Umbraco.Cms.Core.Routing
return _uriUtility.UriFromUmbraco(uri, _requestSettings);
}
- string CombinePaths(string path1, string path2)
+ private string CombinePaths(string path1, string path2)
{
- string path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2;
+ var path = path1.TrimEnd(Constants.CharArrays.ForwardSlash) + path2;
return path == "/" ? path : path.TrimEnd(Constants.CharArrays.ForwardSlash);
}
diff --git a/src/Umbraco.Core/Routing/SiteDomainMapper.cs b/src/Umbraco.Core/Routing/SiteDomainMapper.cs
index d877385ef1..455889d015 100644
--- a/src/Umbraco.Core/Routing/SiteDomainMapper.cs
+++ b/src/Umbraco.Core/Routing/SiteDomainMapper.cs
@@ -4,18 +4,36 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using Umbraco.Extensions;
-using System.ComponentModel;
namespace Umbraco.Cms.Core.Routing
{
///
- /// Provides utilities to handle site domains.
+ /// Provides utilities to handle site domains.
///
public class SiteDomainMapper : ISiteDomainMapper, IDisposable
{
+ public void Dispose() =>
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(true);
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ // This is pretty nasty disposing a static on an instance but it's because this whole class
+ // is pretty fubar. I'm sure we've fixed this all up in netcore now? We need to remove all statics.
+ _configLock.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
#region Configure
- private readonly ReaderWriterLockSlim _configLock = new ReaderWriterLockSlim();
+ private readonly ReaderWriterLockSlim _configLock = new();
private Dictionary> _qualifiedSites;
private bool _disposedValue;
@@ -25,10 +43,12 @@ namespace Umbraco.Cms.Core.Routing
// these are for validation
//private const string DomainValidationSource = @"^(\*|((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/[-\w]*)?))$";
private const string DomainValidationSource = @"^(((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/)?))$";
- private static readonly Regex s_domainValidation = new Regex(DomainValidationSource, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ private static readonly Regex s_domainValidation =
+ new(DomainValidationSource, RegexOptions.IgnoreCase | RegexOptions.Compiled);
///
- /// Clears the entire configuration.
+ /// Clears the entire configuration.
///
public void Clear()
{
@@ -49,20 +69,21 @@ namespace Umbraco.Cms.Core.Routing
}
}
- private IEnumerable ValidateDomains(IEnumerable domains)
- {
+ private IEnumerable ValidateDomains(IEnumerable domains) =>
// must use authority format w/optional scheme and port, but no path
// any domain should appear only once
- return domains.Select(domain =>
+ domains.Select(domain =>
+ {
+ if (!s_domainValidation.IsMatch(domain))
{
- if (!s_domainValidation.IsMatch(domain))
- throw new ArgumentOutOfRangeException(nameof(domains), $"Invalid domain: \"{domain}\".");
- return domain;
- });
- }
+ throw new ArgumentOutOfRangeException(nameof(domains), $"Invalid domain: \"{domain}\".");
+ }
+
+ return domain;
+ });
///
- /// Adds a site.
+ /// Adds a site.
///
/// A key uniquely identifying the site.
/// The site domains.
@@ -87,7 +108,7 @@ namespace Umbraco.Cms.Core.Routing
}
///
- /// Adds a site.
+ /// Adds a site.
///
/// A key uniquely identifying the site.
/// The site domains.
@@ -112,7 +133,7 @@ namespace Umbraco.Cms.Core.Routing
}
///
- /// Removes a site.
+ /// Removes a site.
///
/// A key uniquely identifying the site.
internal void RemoveSite(string key)
@@ -122,11 +143,15 @@ namespace Umbraco.Cms.Core.Routing
_configLock.EnterWriteLock();
if (Sites == null || !Sites.ContainsKey(key))
+ {
return;
+ }
Sites.Remove(key);
if (Sites.Count == 0)
+ {
Sites = null;
+ }
if (Bindings != null && Bindings.ContainsKey(key))
{
@@ -134,11 +159,16 @@ namespace Umbraco.Cms.Core.Routing
{
Bindings[b].Remove(key);
if (Bindings[b].Count == 0)
+ {
Bindings.Remove(b);
+ }
}
+
Bindings.Remove(key);
if (Bindings.Count > 0)
+ {
Bindings = null;
+ }
}
_qualifiedSites = null;
@@ -153,12 +183,12 @@ namespace Umbraco.Cms.Core.Routing
}
///
- /// Binds some sites.
+ /// Binds some sites.
///
/// The keys uniquely identifying the sites to bind.
///
- /// At the moment there is no public way to unbind sites. Clear and reconfigure.
- /// If site1 is bound to site2 and site2 is bound to site3 then site1 is bound to site3.
+ /// At the moment there is no public way to unbind sites. Clear and reconfigure.
+ /// If site1 is bound to site2 and site2 is bound to site3 then site1 is bound to site3.
///
public void BindSites(params string[] keys)
{
@@ -167,7 +197,9 @@ namespace Umbraco.Cms.Core.Routing
_configLock.EnterWriteLock();
foreach (var key in keys.Where(key => !Sites.ContainsKey(key)))
+ {
throw new ArgumentException($"Not an existing site key: {key}.", nameof(keys));
+ }
Bindings = Bindings ?? new Dictionary>();
@@ -180,9 +212,12 @@ namespace Umbraco.Cms.Core.Routing
foreach (var key in allkeys)
{
if (!Bindings.ContainsKey(key))
+ {
Bindings[key] = new List();
+ }
+
var xkey = key;
- var addKeys = allkeys.Where(k => k != xkey).Except(Bindings[key]);
+ IEnumerable addKeys = allkeys.Where(k => k != xkey).Except(Bindings[key]);
Bindings[key].AddRange(addKeys);
}
}
@@ -200,16 +235,18 @@ namespace Umbraco.Cms.Core.Routing
#region Map domains
///
- public virtual DomainAndUri MapDomain(IReadOnlyCollection domainAndUris, Uri current, string culture, string defaultCulture)
+ public virtual DomainAndUri MapDomain(IReadOnlyCollection domainAndUris, Uri current,
+ string culture, string defaultCulture)
{
var currentAuthority = current.GetLeftPart(UriPartial.Authority);
- var qualifiedSites = GetQualifiedSites(current);
+ Dictionary qualifiedSites = GetQualifiedSites(current);
return MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture);
}
///
- public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris, Uri current, bool excludeDefault, string culture, string defaultCulture)
+ public virtual IEnumerable MapDomains(IReadOnlyCollection domainAndUris,
+ Uri current, bool excludeDefault, string culture, string defaultCulture)
{
// TODO: ignoring cultures entirely?
@@ -221,15 +258,18 @@ namespace Umbraco.Cms.Core.Routing
{
_configLock.EnterReadLock();
- var qualifiedSites = GetQualifiedSitesInsideLock(current);
+ Dictionary qualifiedSites = GetQualifiedSitesInsideLock(current);
if (excludeDefault)
{
// exclude the current one (avoid producing the absolute equivalent of what GetUrl returns)
- var hintWithSlash = current.EndPathWithSlash();
- var hinted = domainAndUris.FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash));
+ Uri hintWithSlash = current.EndPathWithSlash();
+ DomainAndUri hinted =
+ domainAndUris.FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash));
if (hinted != null)
+ {
ret = ret.Where(d => d != hinted);
+ }
// exclude the default one (avoid producing a possible duplicate of what GetUrl returns)
// only if the default one cannot be the current one ie if hinted is not null
@@ -237,17 +277,21 @@ namespace Umbraco.Cms.Core.Routing
{
// it is illegal to call MapDomain if domainAndUris is empty
// also, domainAndUris should NOT contain current, hence the test on hinted
- var mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture, defaultCulture); // what GetUrl would get
+ DomainAndUri mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority, culture,
+ defaultCulture); // what GetUrl would get
ret = ret.Where(d => d != mainDomain);
}
}
// we do our best, but can't do the impossible
if (qualifiedSites == null)
+ {
return ret;
+ }
// find a site that contains the current authority
- var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
+ KeyValuePair currentSite =
+ qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
// if current belongs to a site, pick every element from domainAndUris that also belong
// to that site -- or to any site bound to that site
@@ -257,7 +301,8 @@ namespace Umbraco.Cms.Core.Routing
candidateSites = new[] { currentSite };
if (Bindings != null && Bindings.ContainsKey(currentSite.Key))
{
- var boundSites = qualifiedSites.Where(site => Bindings[currentSite.Key].Contains(site.Key));
+ IEnumerable> boundSites =
+ qualifiedSites.Where(site => Bindings[currentSite.Key].Contains(site.Key));
candidateSites = candidateSites.Union(boundSites).ToArray();
// .ToArray ensures it is evaluated before the configuration lock is exited
@@ -273,7 +318,9 @@ namespace Umbraco.Cms.Core.Routing
}
// if we are able to filter, then filter, else return the whole lot
- return candidateSites == null ? ret : ret.Where(d =>
+ return candidateSites == null
+ ? ret
+ : ret.Where(d =>
{
var authority = d.Uri.GetLeftPart(UriPartial.Authority);
return candidateSites.Any(site => site.Value.Contains(authority));
@@ -301,11 +348,15 @@ namespace Umbraco.Cms.Core.Routing
{
// we do our best, but can't do the impossible
if (Sites == null)
+ {
return null;
+ }
// cached?
if (_qualifiedSites != null && _qualifiedSites.ContainsKey(current.Scheme))
+ {
return _qualifiedSites[current.Scheme];
+ }
_qualifiedSites = _qualifiedSites ?? new Dictionary>();
@@ -314,7 +365,10 @@ namespace Umbraco.Cms.Core.Routing
return _qualifiedSites[current.Scheme] = Sites
.ToDictionary(
kvp => kvp.Key,
- kvp => kvp.Value.Select(d => new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme)).GetLeftPart(UriPartial.Authority)).ToArray()
+ kvp => kvp.Value.Select(d =>
+ new Uri(UriUtilityCore.StartWithScheme(d, current.Scheme))
+ .GetLeftPart(UriPartial.Authority))
+ .ToArray()
);
// .ToDictionary will evaluate and create the dictionary immediately
@@ -322,61 +376,54 @@ namespace Umbraco.Cms.Core.Routing
// therefore it is safe to return and exit the configuration lock
}
- private DomainAndUri MapDomain(IReadOnlyCollection domainAndUris, Dictionary qualifiedSites, string currentAuthority, string culture, string defaultCulture)
+ private DomainAndUri MapDomain(IReadOnlyCollection domainAndUris,
+ Dictionary qualifiedSites, string currentAuthority, string culture, string defaultCulture)
{
if (domainAndUris == null)
+ {
throw new ArgumentNullException(nameof(domainAndUris));
+ }
+
if (domainAndUris.Count == 0)
+ {
throw new ArgumentException("Cannot be empty.", nameof(domainAndUris));
+ }
- // TODO: how shall we deal with cultures?
-
- // we do our best, but can't do the impossible
- // get the "default" domain ie the first one for the culture, else the first one (exists, length > 0)
if (qualifiedSites == null)
{
return domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture))
- ?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture));
+ ?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(defaultCulture))
+ ?? (culture is null ? domainAndUris.First() : null);
}
// find a site that contains the current authority
- var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
+ KeyValuePair currentSite =
+ qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority));
// if current belongs to a site - try to pick the first element
// from domainAndUris that also belongs to that site
- var ret = currentSite.Equals(default(KeyValuePair))
+ DomainAndUri ret = currentSite.Equals(default(KeyValuePair))
? null
- : domainAndUris.FirstOrDefault(d => currentSite.Value.Contains(d.Uri.GetLeftPart(UriPartial.Authority)));
+ : domainAndUris.FirstOrDefault(d =>
+ currentSite.Value.Contains(d.Uri.GetLeftPart(UriPartial.Authority)));
// no match means that either current does not belong to a site, or the site it belongs to
- // does not contain any of domainAndUris.
+ // does not contain any of domainAndUris. Yet we have to return something. here, it becomes
+ // a bit arbitrary.
+
+ // look through sites in order and pick the first domainAndUri that belongs to a site
+ ret = ret ?? qualifiedSites
+ .Where(site => site.Key != currentSite.Key)
+ .Select(site => domainAndUris.FirstOrDefault(domainAndUri =>
+ site.Value.Contains(domainAndUri.Uri.GetLeftPart(UriPartial.Authority))))
+ .FirstOrDefault(domainAndUri => domainAndUri != null);
+
+ // random, really
+ ret = ret ?? domainAndUris.FirstOrDefault(x => x.Culture.InvariantEquals(culture)) ?? domainAndUris.First();
+
return ret;
}
-
#endregion
-
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
- {
- if (disposing)
- {
- // This is pretty nasty disposing a static on an instance but it's because this whole class
- // is pretty fubar. I'm sure we've fixed this all up in netcore now? We need to remove all statics.
- _configLock.Dispose();
- }
-
- _disposedValue = true;
- }
- }
-
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- Dispose(disposing: true);
- }
-
-
}
}
diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
index 085629eac8..3853988aab 100644
--- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
+++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
@@ -15,11 +15,11 @@ namespace Umbraco.Extensions
public static class UrlProviderExtensions
{
///
- /// Gets the URLs of the content item.
+ /// Gets the URLs of the content item.
///
///
- /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.
- /// Contains all the URLs that we can figure out (based upon domains, etc).
+ /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.
+ /// Contains all the URLs that we can figure out (based upon domains, etc).
///
public static async Task> GetContentUrlsAsync(
this IContent content,
@@ -33,16 +33,55 @@ namespace Umbraco.Extensions
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
- if (content == null) throw new ArgumentNullException(nameof(content));
- if (publishedRouter == null) throw new ArgumentNullException(nameof(publishedRouter));
- if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
- if (localizationService == null) throw new ArgumentNullException(nameof(localizationService));
- if (textService == null) throw new ArgumentNullException(nameof(textService));
- if (contentService == null) throw new ArgumentNullException(nameof(contentService));
- if (logger == null) throw new ArgumentNullException(nameof(logger));
- if (publishedUrlProvider == null) throw new ArgumentNullException(nameof(publishedUrlProvider));
- if (uriUtility == null) throw new ArgumentNullException(nameof(uriUtility));
- if (variationContextAccessor == null) throw new ArgumentNullException(nameof(variationContextAccessor));
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ if (publishedRouter == null)
+ {
+ throw new ArgumentNullException(nameof(publishedRouter));
+ }
+
+ if (umbracoContext == null)
+ {
+ throw new ArgumentNullException(nameof(umbracoContext));
+ }
+
+ if (localizationService == null)
+ {
+ throw new ArgumentNullException(nameof(localizationService));
+ }
+
+ if (textService == null)
+ {
+ throw new ArgumentNullException(nameof(textService));
+ }
+
+ if (contentService == null)
+ {
+ throw new ArgumentNullException(nameof(contentService));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ if (publishedUrlProvider == null)
+ {
+ throw new ArgumentNullException(nameof(publishedUrlProvider));
+ }
+
+ if (uriUtility == null)
+ {
+ throw new ArgumentNullException(nameof(uriUtility));
+ }
+
+ if (variationContextAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(variationContextAccessor));
+ }
var result = new List();
@@ -68,7 +107,9 @@ namespace Umbraco.Extensions
// get all URLs for all cultures
// in a HashSet, so de-duplicates too
- foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider))
+ foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter,
+ umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility,
+ publishedUrlProvider))
{
urls.Add(cultureUrl);
}
@@ -79,15 +120,25 @@ namespace Umbraco.Extensions
// in some cases there will be the same URL for multiple cultures:
// * The entire branch is invariant
// * If there are less domain/cultures assigned to the branch than the number of cultures/languages installed
- foreach (UrlInfo dUrl in urlGroup.LegacyDistinctBy(x => x.Text.ToUpperInvariant()).OrderBy(x => x.Text).ThenBy(x => x.Culture))
+
+ if (urlGroup.Key)
{
- result.Add(dUrl);
+ result.AddRange(urlGroup.LegacyDistinctBy(x => x.Text.ToUpperInvariant())
+ .OrderBy(x => x.Text).ThenBy(x => x.Culture));
}
+ else
+ {
+ result.AddRange(urlGroup);
+ }
+
+
+
}
// get the 'other' URLs - ie not what you'd get with GetUrl() but URLs that would route to the document, nevertheless.
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
- foreach (var otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
+ foreach (UrlInfo otherUrl in publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text)
+ .ThenBy(x => x.Culture))
{
// avoid duplicates
if (urls.Add(otherUrl))
@@ -100,7 +151,7 @@ namespace Umbraco.Extensions
}
///
- /// Tries to return a for each culture for the content while detecting collisions/errors
+ /// Tries to return a for each culture for the content while detecting collisions/errors
///
private static async Task> GetContentUrlsByCultureAsync(
IContent content,
@@ -151,7 +202,8 @@ namespace Umbraco.Extensions
// got a URL, deal with collisions, add URL
default:
// detect collisions, etc
- Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility);
+ Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture,
+ umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility);
if (hasCollision)
{
result.Add(hasCollision.Result);
@@ -168,7 +220,8 @@ namespace Umbraco.Extensions
return result;
}
- private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService, ILocalizedTextService textService)
+ private static UrlInfo HandleCouldNotGetUrl(IContent content, string culture, IContentService contentService,
+ ILocalizedTextService textService)
{
// document has a published version yet its URL is "#" => a parent must be
// unpublished, walk up the tree until we find it, and report.
@@ -176,27 +229,31 @@ namespace Umbraco.Extensions
do
{
parent = parent.ParentId > 0 ? contentService.GetParent(parent) : null;
- }
- while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
+ } while (parent != null && parent.Published &&
+ (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture)));
if (parent == null)
{
// oops, internal error
return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture);
}
- else if (!parent.Published)
+
+ if (!parent.Published)
{
// totally not published
- return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture);
- }
- else
- {
- // culture not published
- return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture);
+ return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }),
+ culture);
}
+
+ // culture not published
+ return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] { parent.Name }),
+ culture);
}
- private static async Task> DetectCollisionAsync(ILogger logger, IContent content, string url, string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility)
+ private static async Task> DetectCollisionAsync(ILogger logger, IContent content, string url,
+ string culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter,
+ ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor,
+ UriUtility uriUtility)
{
// test for collisions on the 'main' URL
var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute);
@@ -207,11 +264,13 @@ namespace Umbraco.Extensions
uri = uriUtility.UriToUmbraco(uri);
IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri);
- IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound));
+ IPublishedRequest pcr =
+ await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound));
if (!pcr.HasPublishedContent())
{
- var logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
+ var logMsg = nameof(DetectCollisionAsync) +
+ " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
if (pcr.IgnorePublishedContentCollisions)
{
logger.LogDebug(logMsg, url, uri, culture);
@@ -243,14 +302,7 @@ namespace Umbraco.Extensions
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
- var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture);
- return Attempt.Succeed(urlInfo);
- }
-
- // collisions with a different culture of the same content can never be routed.
- if (!culture.InvariantEquals(pcr.Culture))
- {
- var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture);
+ var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture);
return Attempt.Succeed(urlInfo);
}
diff --git a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
new file mode 100644
index 0000000000..86e2988307
--- /dev/null
+++ b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ ///
+ /// Used to filter historic content versions for cleanup.
+ ///
+ public interface IContentVersionCleanupPolicy
+ {
+ ///
+ /// Filters a set of candidates historic content versions for cleanup according to policy settings.
+ ///
+ IEnumerable Apply(DateTime asAtDate, IEnumerable items);
+ }
+}
diff --git a/src/Umbraco.Core/Services/IContentVersionService.cs b/src/Umbraco.Core/Services/IContentVersionService.cs
new file mode 100644
index 0000000000..1f56138c3d
--- /dev/null
+++ b/src/Umbraco.Core/Services/IContentVersionService.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public interface IContentVersionService
+ {
+ ///
+ /// Removes historic content versions according to a policy.
+ ///
+ IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate);
+
+ ///
+ /// Gets paginated content versions for given content id paginated.
+ ///
+ /// Thrown when is invalid.
+ IEnumerable GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string culture = null);
+
+ ///
+ /// Updates preventCleanup value for given content version.
+ ///
+ void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1);
+ }
+}
diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs
index a1f6c09d5c..57d09077fc 100644
--- a/src/Umbraco.Core/Services/UserServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -59,7 +59,7 @@ namespace Umbraco.Extensions
///
public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds)
{
- userService.ReplaceUserGroupPermissions(groupId, new char[] {}, entityIds);
+ userService.ReplaceUserGroupPermissions(groupId, null, entityIds);
}
///
@@ -69,7 +69,7 @@ namespace Umbraco.Extensions
///
public static void RemoveUserGroupPermissions(this IUserService userService, int groupId)
{
- userService.ReplaceUserGroupPermissions(groupId, new char[] { });
+ userService.ReplaceUserGroupPermissions(groupId, null);
}
diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs
index a266b52f6b..fff22936a0 100644
--- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs
+++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs
@@ -32,7 +32,13 @@ namespace Umbraco.Cms.Core.Strings
if (content.HasProperty(Constants.Conventions.Content.UrlName))
source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(source))
- source = content.GetCultureName(culture);
+ {
+ // If the name of a node has been updated, but it has not been published, the url should use the published name, not the current node name
+ // If this node has never been published (GetPublishName is null), use the unpublished name
+ source = (content is IContent document) && document.Edited && document.GetPublishName(culture) != null
+ ? document.GetPublishName(culture)
+ : content.GetCultureName(culture);
+ }
return source;
}
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 5996d238bb..9a1da38222 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -1,61 +1,65 @@
-
- netstandard2.0
- Umbraco.Cms.Core
- Umbraco CMS
- Umbraco.Cms.Core
- Umbraco CMS Core
- Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco
- Umbraco CMS
-
+
+ netstandard2.0
+ Umbraco.Cms.Core
+ Umbraco CMS
+ Umbraco.Cms.Core
+ Umbraco CMS Core
+ Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco
+ Umbraco CMS
+
-
- bin\Release\Umbraco.Core.xml
-
+
+ bin\Release\Umbraco.Core.xml
+
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
- all
-
-
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+
+
-
-
- <_Parameter1>Umbraco.Tests
-
-
- <_Parameter1>Umbraco.Tests.Common
-
-
- <_Parameter1>Umbraco.Tests.UnitTests
-
-
- <_Parameter1>Umbraco.Tests.Benchmarks
-
-
- <_Parameter1>Umbraco.Tests.Integration
-
-
- <_Parameter1>DynamicProxyGenAssembly2
-
-
+
+
+ <_Parameter1>Umbraco.Tests
+
+
+ <_Parameter1>Umbraco.Tests.Common
+
+
+ <_Parameter1>Umbraco.Tests.UnitTests
+
+
+ <_Parameter1>Umbraco.Tests.Benchmarks
+
+
+ <_Parameter1>Umbraco.Tests.Integration
+
+
+ <_Parameter1>DynamicProxyGenAssembly2
+
+
-
-
-
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
index a77bee7c44..b8a942edc5 100644
--- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
+++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
@@ -145,9 +145,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
totalFound = result.TotalItemCount;
- var pagedResult = result.Skip(Convert.ToInt32(pageIndex));
-
- return pagedResult;
+ return result;
}
private bool BuildQuery(StringBuilder sb, string query, string searchFrom, List fields, string type)
diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs
index 2f7cb646a2..5121c32d32 100644
--- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs
+++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs
@@ -8,8 +8,6 @@ using Examine;
using Examine.Lucene;
using Examine.Lucene.Providers;
using Lucene.Net.Documents;
-using Lucene.Net.Index;
-using Lucene.Net.Store;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -103,10 +101,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
//remove the original value so we can store it the correct way
d.RemoveField(f.Key);
- d.Add(new StringField(
- f.Key,
- f.Value[0].ToString(),
- Field.Store.YES));
+ d.Add(new StoredField(f.Key, f.Value[0].ToString()));
}
}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
index 89d6f5ee2c..173c80ea7b 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
@@ -166,7 +166,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddScoped();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddSingleton(sp =>
+ new PublishedContentQueryAccessor(sp.GetRequiredService())
+ );
builder.Services.AddScoped(factory =>
{
var umbCtx = factory.GetRequiredService();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
index edb8033f3d..6582cfb0c6 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
@@ -39,6 +39,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
// register the scheme for media paths
builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+
builder.SetMediaFileSystem(factory =>
{
IIOHelper ioHelper = factory.GetRequiredService();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
index 360e785906..d3ebb28f9c 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
@@ -25,6 +25,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
index 861a05b459..661ed93292 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
@@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
-using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
@@ -16,6 +15,7 @@ using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Infrastructure.Packaging;
+using Umbraco.Cms.Infrastructure.Services.Implement;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.DependencyInjection
@@ -43,6 +43,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs
index b311b1f0da..e3839e152b 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs
@@ -27,6 +27,19 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
return builder;
}
+ ///
+ /// Sets the default view content provider
+ ///
+ /// The type of the provider.
+ /// The builder.
+ ///
+ public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder)
+ where T : class, IDefaultViewContentProvider
+ {
+ builder.Services.AddUnique();
+ return builder;
+ }
+
///
/// Sets the culture dictionary factory.
///
diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs
index 4fbbb35a6d..0d881888a1 100644
--- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs
+++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs
@@ -194,9 +194,8 @@ namespace Umbraco.Cms.Infrastructure.Examine
.Field("nodeType", id.ToInvariantString())
.Execute(QueryOptions.SkipTake(page * pageSize, pageSize));
total = results.TotalItemCount;
- var paged = results.Skip(page * pageSize);
-
- foreach (ISearchResult item in paged)
+
+ foreach (ISearchResult item in results)
{
if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int contentId))
{
diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs
new file mode 100644
index 0000000000..5f3aba5f3f
--- /dev/null
+++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Infrastructure.HostedServices
+{
+ ///
+ /// Recurring hosted service that executes the content history cleanup.
+ ///
+ public class ContentVersionCleanup : RecurringHostedServiceBase
+ {
+ private readonly IRuntimeState _runtimeState;
+ private readonly ILogger _logger;
+ private readonly IOptionsMonitor _settingsMonitor;
+ private readonly IContentVersionService _service;
+ private readonly IMainDom _mainDom;
+ private readonly IServerRoleAccessor _serverRoleAccessor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentVersionCleanup(
+ IRuntimeState runtimeState,
+ ILogger logger,
+ IOptionsMonitor settingsMonitor,
+ IContentVersionService service,
+ IMainDom mainDom,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(TimeSpan.FromHours(1), TimeSpan.FromMinutes(3))
+ {
+ _runtimeState = runtimeState;
+ _logger = logger;
+ _settingsMonitor = settingsMonitor;
+ _service = service;
+ _mainDom = mainDom;
+ _serverRoleAccessor = serverRoleAccessor;
+ }
+
+ ///
+ public override Task PerformExecuteAsync(object state)
+ {
+ // Globally disabled by feature flag
+ if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup)
+ {
+ _logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration");
+ return Task.CompletedTask;
+ }
+
+ if (_runtimeState.Level != RuntimeLevel.Run)
+ {
+ return Task.FromResult(true); // repeat...
+ }
+
+ // Don't run on replicas nor unknown role servers
+ switch (_serverRoleAccessor.CurrentServerRole)
+ {
+ case ServerRole.Subscriber:
+ _logger.LogDebug("Does not run on subscriber servers");
+ return Task.CompletedTask;
+ case ServerRole.Unknown:
+ _logger.LogDebug("Does not run on servers with unknown role");
+ return Task.CompletedTask;
+ case ServerRole.Single:
+ case ServerRole.SchedulingPublisher:
+ default:
+ break;
+ }
+
+ // Ensure we do not run if not main domain, but do NOT lock it
+ if (!_mainDom.IsMainDom)
+ {
+ _logger.LogDebug("Does not run if not MainDom");
+ return Task.FromResult(false); // do NOT repeat, going down
+ }
+
+ var count = _service.PerformContentVersionCleanup(DateTime.Now).Count;
+
+ if (count > 0)
+ {
+ _logger.LogInformation("Deleted {count} ContentVersion(s)", count);
+ }
+ else
+ {
+ _logger.LogDebug("Task complete, no items were Deleted");
+ }
+
+ return Task.FromResult(true);
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs
index 68a15580e4..00adc5018b 100644
--- a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs
+++ b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs
@@ -1,7 +1,18 @@
-using Umbraco.Cms.Infrastructure;
-
namespace Umbraco.Cms.Core
{
+ ///
+ /// Not intended for use in background threads where you should make use of
+ /// and instead resolve IPublishedContentQuery from a
+ /// e.g. using
+ ///
+ ///
+ /// // Background thread example
+ /// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext();
+ /// using IServiceScope serviceScope = _serviceProvider.CreateScope();
+ /// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();
+ ///
+ ///
+ ///
public interface IPublishedContentQueryAccessor
{
bool TryGetValue(out IPublishedContentQuery publishedContentQuery);
diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs
index 73b9d702b7..4ef8fa4e28 100644
--- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs
+++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs
@@ -117,7 +117,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps
{
minCharLength = _passwordConfiguration.RequiredLength,
minNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars(),
- quickInstallAvailable = DatabaseConfigureStep.IsSqlCeAvailable()
+ quickInstallAvailable = DatabaseConfigureStep.IsSqlCeAvailable() || DatabaseConfigureStep.IsLocalDbAvailable(),
+ customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured)
};
}
}
diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs
index 098d690345..31b3d67bd0 100644
--- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs
+++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs
@@ -1,10 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
+using System.IO;
using System.Net.Mail;
using System.Threading.Tasks;
+using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using MimeKit;
+using MimeKit.IO;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mail;
@@ -71,12 +76,56 @@ namespace Umbraco.Cms.Infrastructure.Mail
}
}
- if (_globalSettings.IsSmtpServerConfigured == false)
+ var isPickupDirectoryConfigured = !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.PickupDirectoryLocation);
+
+ if (_globalSettings.IsSmtpServerConfigured == false && !isPickupDirectoryConfigured)
{
_logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject);
return;
}
+ if (isPickupDirectoryConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From))
+ {
+ // The following code snippet is the recommended way to handle PickupDirectoryLocation.
+ // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory
+ do {
+ var path = Path.Combine(_globalSettings.Smtp?.PickupDirectoryLocation, Guid.NewGuid () + ".eml");
+ Stream stream;
+
+ try
+ {
+ stream = File.Open(path, FileMode.CreateNew);
+ }
+ catch (IOException)
+ {
+ if (File.Exists(path))
+ {
+ continue;
+ }
+ throw;
+ }
+
+ try {
+ using (stream)
+ {
+ using var filtered = new FilteredStream(stream);
+ filtered.Add(new SmtpDataFilter());
+
+ FormatOptions options = FormatOptions.Default.Clone();
+ options.NewLineFormat = NewLineFormat.Dos;
+
+ await message.ToMimeMessage(_globalSettings.Smtp?.From).WriteToAsync(options, filtered);
+ filtered.Flush();
+ return;
+
+ }
+ } catch {
+ File.Delete(path);
+ throw;
+ }
+ } while (true);
+ }
+
using var client = new SmtpClient();
await client.ConnectAsync(_globalSettings.Smtp.Host,
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index b36a21b95b..b3db06fd5b 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -12,21 +12,83 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
using Umbraco.Extensions;
+using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
namespace Umbraco.Cms.Infrastructure.Migrations.Install
{
///
- /// Creates the initial database schema during install.
+ /// Creates the initial database schema during install.
///
public class DatabaseSchemaCreator
{
+ // all tables, in order
+ internal static readonly List OrderedTables = new()
+ {
+ typeof(UserDto),
+ typeof(NodeDto),
+ typeof(ContentTypeDto),
+ typeof(TemplateDto),
+ typeof(ContentDto),
+ typeof(ContentVersionDto),
+ typeof(MediaVersionDto),
+ typeof(DocumentDto),
+ typeof(ContentTypeTemplateDto),
+ typeof(DataTypeDto),
+ typeof(DictionaryDto),
+ typeof(LanguageDto),
+ typeof(LanguageTextDto),
+ typeof(DomainDto),
+ typeof(LogDto),
+ typeof(MacroDto),
+ typeof(MacroPropertyDto),
+ typeof(MemberPropertyTypeDto),
+ typeof(MemberDto),
+ typeof(Member2MemberGroupDto),
+ typeof(PropertyTypeGroupDto),
+ typeof(PropertyTypeDto),
+ typeof(PropertyDataDto),
+ typeof(RelationTypeDto),
+ typeof(RelationDto),
+ typeof(TagDto),
+ typeof(TagRelationshipDto),
+ typeof(ContentType2ContentTypeDto),
+ typeof(ContentTypeAllowedContentTypeDto),
+ typeof(User2NodeNotifyDto),
+ typeof(ServerRegistrationDto),
+ typeof(AccessDto),
+ typeof(AccessRuleDto),
+ typeof(CacheInstructionDto),
+ typeof(ExternalLoginDto),
+ typeof(ExternalLoginTokenDto),
+ typeof(RedirectUrlDto),
+ typeof(LockDto),
+ typeof(UserGroupDto),
+ typeof(User2UserGroupDto),
+ typeof(UserGroup2NodePermissionDto),
+ typeof(UserGroup2AppDto),
+ typeof(UserStartNodeDto),
+ typeof(ContentNuDto),
+ typeof(DocumentVersionDto),
+ typeof(KeyValueDto),
+ typeof(UserLoginDto),
+ typeof(ConsentDto),
+ typeof(AuditEntryDto),
+ typeof(ContentVersionCultureVariationDto),
+ typeof(DocumentCultureVariationDto),
+ typeof(ContentScheduleDto),
+ typeof(LogViewerQueryDto),
+ typeof(ContentVersionCleanupPolicyDto),
+ typeof(UserGroup2NodeDto)
+ };
+
private readonly IUmbracoDatabase _database;
+ private readonly IEventAggregator _eventAggregator;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IUmbracoVersion _umbracoVersion;
- private readonly IEventAggregator _eventAggregator;
- public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger, ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator)
+ public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger,
+ ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -42,74 +104,16 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax;
- // all tables, in order
- internal static readonly List OrderedTables = new List
- {
- typeof (UserDto),
- typeof (NodeDto),
- typeof (ContentTypeDto),
- typeof (TemplateDto),
- typeof (ContentDto),
- typeof (ContentVersionDto),
- typeof (MediaVersionDto),
- typeof (DocumentDto),
- typeof (ContentTypeTemplateDto),
- typeof (DataTypeDto),
- typeof (DictionaryDto),
- typeof (LanguageDto),
- typeof (LanguageTextDto),
- typeof (DomainDto),
- typeof (LogDto),
- typeof (MacroDto),
- typeof (MacroPropertyDto),
- typeof (MemberPropertyTypeDto),
- typeof (MemberDto),
- typeof (Member2MemberGroupDto),
- typeof (PropertyTypeGroupDto),
- typeof (PropertyTypeDto),
- typeof (PropertyDataDto),
- typeof (RelationTypeDto),
- typeof (RelationDto),
- typeof (TagDto),
- typeof (TagRelationshipDto),
- typeof (ContentType2ContentTypeDto),
- typeof (ContentTypeAllowedContentTypeDto),
- typeof (User2NodeNotifyDto),
- typeof (ServerRegistrationDto),
- typeof (AccessDto),
- typeof (AccessRuleDto),
- typeof (CacheInstructionDto),
- typeof (ExternalLoginDto),
- typeof (ExternalLoginTokenDto),
- typeof (RedirectUrlDto),
- typeof (LockDto),
- typeof (UserGroupDto),
- typeof (User2UserGroupDto),
- typeof (UserGroup2NodePermissionDto),
- typeof (UserGroup2AppDto),
- typeof (UserStartNodeDto),
- typeof (ContentNuDto),
- typeof (DocumentVersionDto),
- typeof (KeyValueDto),
- typeof (UserLoginDto),
- typeof (ConsentDto),
- typeof (AuditEntryDto),
- typeof (ContentVersionCultureVariationDto),
- typeof (DocumentCultureVariationDto),
- typeof (ContentScheduleDto),
- typeof (LogViewerQueryDto)
- };
-
///
- /// Drops all Umbraco tables in the db.
+ /// Drops all Umbraco tables in the db.
///
internal void UninstallDatabaseSchema()
{
_logger.LogInformation("Start UninstallDatabaseSchema");
- foreach (var table in OrderedTables.AsEnumerable().Reverse())
+ foreach (Type table in OrderedTables.AsEnumerable().Reverse())
{
- var tableNameAttribute = table.FirstAttribute();
+ TableNameAttribute tableNameAttribute = table.FirstAttribute();
var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value;
_logger.LogInformation("Uninstall {TableName}", tableName);
@@ -117,7 +121,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
try
{
if (TableExists(tableName))
+ {
DropTable(tableName);
+ }
}
catch (Exception ex)
{
@@ -129,13 +135,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
}
///
- /// Initializes the database by creating the umbraco db schema.
+ /// Initializes the database by creating the umbraco db schema.
///
/// This needs to execute as part of a transaction.
public void InitializeDatabaseSchema()
{
if (!_database.InTransaction)
+ {
throw new InvalidOperationException("Database is not in a transaction.");
+ }
var eventMessages = new EventMessages();
var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages);
@@ -143,22 +151,23 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
if (creatingNotification.Cancel == false)
{
- var dataCreation = new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), _umbracoVersion);
- foreach (var table in OrderedTables)
+ var dataCreation = new DatabaseDataCreator(_database,
+ _loggerFactory.CreateLogger(), _umbracoVersion);
+ foreach (Type table in OrderedTables)
+ {
CreateTable(false, table, dataCreation);
+ }
}
- DatabaseSchemaCreatedNotification createdNotification = new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification);
+ DatabaseSchemaCreatedNotification createdNotification =
+ new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification);
FireAfterCreation(createdNotification);
}
///
- /// Validates the schema of the current database.
+ /// Validates the schema of the current database.
///
- internal DatabaseSchemaResult ValidateSchema()
- {
- return ValidateSchema(OrderedTables);
- }
+ internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables);
internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables)
{
@@ -179,26 +188,30 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
}
///
- /// This validates the Primary/Foreign keys in the database
+ /// This validates the Primary/Foreign keys in the database
///
///
///
- /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database with non PK/FK constraints.
- /// Any unique "constraints" in the database are done with unique indexes.
+ /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database
+ /// with non PK/FK constraints.
+ /// Any unique "constraints" in the database are done with unique indexes.
///
private void ValidateDbConstraints(DatabaseSchemaResult result)
{
//Check constraints in configured database against constraints in schema
var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).LegacyDistinctBy(x => x.Item3).ToList();
- var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList();
- var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList();
+ var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_"))
+ .Select(x => x.Item3).ToList();
+ var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_"))
+ .Select(x => x.Item3).ToList();
var unknownConstraintsInDatabase =
constraintsInDatabase.Where(
x =>
- x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false &&
- x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList();
- var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList();
+ x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false &&
+ x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList();
+ var foreignKeysInSchema =
+ result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList();
var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName))
.Where(x => x.IsNullOrWhiteSpace() == false).ToList();
@@ -219,14 +232,17 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
//Foreign keys:
- var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase);
+ IEnumerable validForeignKeyDifferences =
+ foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase);
foreach (var foreignKey in validForeignKeyDifferences)
{
result.ValidConstraints.Add(foreignKey);
}
- var invalidForeignKeyDifferences =
+
+ IEnumerable invalidForeignKeyDifferences =
foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase)
- .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase));
+ .Union(foreignKeysInSchema.Except(foreignKeysInDatabase,
+ StringComparer.InvariantCultureIgnoreCase));
foreach (var foreignKey in invalidForeignKeyDifferences)
{
result.Errors.Add(new Tuple("Constraint", foreignKey));
@@ -236,37 +252,43 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
//Primary keys:
//Add valid and invalid primary key differences to the result object
- var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase);
+ IEnumerable validPrimaryKeyDifferences =
+ primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase);
foreach (var primaryKey in validPrimaryKeyDifferences)
{
result.ValidConstraints.Add(primaryKey);
}
- var invalidPrimaryKeyDifferences =
+
+ IEnumerable invalidPrimaryKeyDifferences =
primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)
- .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase));
+ .Union(primaryKeysInSchema.Except(primaryKeysInDatabase,
+ StringComparer.InvariantCultureIgnoreCase));
foreach (var primaryKey in invalidPrimaryKeyDifferences)
{
result.Errors.Add(new Tuple