diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml
index 2dd5626d9a..ecfcd84dca 100644
--- a/build/azure-pipelines.yml
+++ b/build/azure-pipelines.yml
@@ -523,7 +523,7 @@ stages:
inputs:
targetType: inline
script: |
- choco install docfx --version=2.58.5 -y
+ choco install docfx --version=2.59.0 -y
if ($lastexitcode -ne 0){
throw ("Error installing DocFX")
}
diff --git a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
index 9153a863a4..aa50d597ba 100644
--- a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
+++ b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
@@ -15,8 +15,8 @@
{{item.name.0.value}}
{{/inheritance.0}}
-{{__global.namespace}}:{{namespace}}
-{{__global.assembly}}:{{assemblies.0}}.dll
+{{__global.namespace}}: {{{namespace.specName.0.value}}}
+{{__global.assembly}}: {{assemblies.0}}.dll
{{__global.syntax}}
{{syntax.content.0.value}}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 68962caef4..d7161acb1f 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,10 +3,10 @@
- 9.4.0
- 9.4.0
- 9.4.0-rc
- 9.4.0
+ 9.5.0
+ 9.5.0
+ 9.5.0-rc
+ 9.5.0
9.0
en-US
Umbraco CMS
diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs
index 73c5ea18f5..f9aa6b500c 100644
--- a/src/JsonSchema/AppSettings.cs
+++ b/src/JsonSchema/AppSettings.cs
@@ -3,6 +3,7 @@
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Deploy.Core.Configuration.DebugConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration;
using Umbraco.Forms.Core.Configuration;
@@ -127,6 +128,8 @@ namespace JsonSchema
public DeploySettings Settings { get; set; }
public DeployProjectConfig Project { get; set; }
+
+ public DebugSettings Debug { get; set; }
}
}
}
diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj
index 7e87eed2e4..d2ef20e741 100644
--- a/src/JsonSchema/JsonSchema.csproj
+++ b/src/JsonSchema/JsonSchema.csproj
@@ -14,6 +14,13 @@
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs
index 77550b81d1..e1b65e2a32 100644
--- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs
@@ -46,6 +46,11 @@ namespace Umbraco.Cms.Core.Cache
{
var payloads = Deserialize(json);
+ Refresh(payloads);
+ }
+
+ public override void Refresh(JsonPayload[] payloads)
+ {
foreach (var payload in payloads)
{
foreach (var alias in GetCacheKeysForAlias(payload.Alias))
@@ -55,11 +60,13 @@ namespace Umbraco.Cms.Core.Cache
if (macroRepoCache)
{
macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id));
+ macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Alias)); // Repository caching of macro definition by alias
}
}
- base.Refresh(json);
+ base.Refresh(payloads);
}
+
#endregion
#region Json
diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index 93a97355d9..e6e5c7006f 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -157,6 +157,8 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const string StaticLoginBackgroundImage = "assets/img/login.jpg";
internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg";
internal const bool StaticHideBackOfficeLogo = false;
+ internal const bool StaticDisableDeleteWhenReferenced = false;
+ internal const bool StaticDisableUnpublishWhenReferenced = false;
///
/// Gets or sets a value for the content notification settings.
@@ -226,6 +228,18 @@ namespace Umbraco.Cms.Core.Configuration.Models
[DefaultValue(StaticHideBackOfficeLogo)]
public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo;
+ ///
+ /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items.
+ ///
+ [DefaultValue(StaticDisableDeleteWhenReferenced)]
+ public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced;
+
+ ///
+ /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items.
+ ///
+ [DefaultValue(StaticDisableUnpublishWhenReferenced)]
+ public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced;
+
///
/// Get or sets the model representing the global content version cleanup policy
///
diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index 7e3e1a2700..5e42d3b8be 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -29,6 +29,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml";
internal const string StaticSqlWriteLockTimeOut = "00:00:05";
internal const bool StaticSanitizeTinyMce = false;
+ internal const int StaticMainDomReleaseSignalPollingInterval = 2000;
///
/// Gets or sets a value for the reserved URLs (must end with a comma).
@@ -137,6 +138,26 @@ namespace Umbraco.Cms.Core.Configuration.Models
///
public string MainDomLock { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value to discriminate MainDom boundaries.
+ ///
+ /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments.
+ ///
+ ///
+ public string MainDomKeyDiscriminator { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep.
+ ///
+ ///
+ /// Doesn't apply to MainDomSemaphoreLock.
+ ///
+ /// The default value is 2000ms.
+ ///
+ ///
+ [DefaultValue(StaticMainDomReleaseSignalPollingInterval)]
+ public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval;
+
///
/// Gets or sets the telemetry ID.
///
@@ -174,18 +195,18 @@ namespace Umbraco.Cms.Core.Configuration.Models
public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation);
///
- /// Gets a value indicating whether TinyMCE scripting sanitization should be applied.
+ /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied.
///
[DefaultValue(StaticSanitizeTinyMce)]
- public bool SanitizeTinyMce => StaticSanitizeTinyMce;
+ public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce;
///
- /// Gets a value representing the time in milliseconds to lock the database for a write operation.
+ /// An int value representing the time in milliseconds to lock the database for a write operation
///
///
/// The default value is 5000 milliseconds.
///
[DefaultValue(StaticSqlWriteLockTimeOut)]
- public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut);
+ public TimeSpan SqlWriteLockTimeOut { get; set; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut);
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
index 6ea563c741..de8215a51b 100644
--- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
[UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)]
public class RichTextEditorSettings
{
- internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*]";
+ internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption";
internal const string StaticInvalidElements = "font";
private static readonly string[] s_default_plugins = new[]
diff --git a/src/Umbraco.Core/Constants-HttpClients.cs b/src/Umbraco.Core/Constants-HttpClients.cs
new file mode 100644
index 0000000000..474ec49a50
--- /dev/null
+++ b/src/Umbraco.Core/Constants-HttpClients.cs
@@ -0,0 +1,19 @@
+namespace Umbraco.Cms.Core
+{
+ ///
+ /// Defines constants.
+ ///
+ public static partial class Constants
+ {
+ ///
+ /// Defines constants for named http clients.
+ ///
+ public static class HttpClients
+ {
+ ///
+ /// Name for http client which ignores certificate errors.
+ ///
+ public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors";
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs
index 40b0267d73..f70dd199fc 100644
--- a/src/Umbraco.Core/Constants-SystemDirectories.cs
+++ b/src/Umbraco.Core/Constants-SystemDirectories.cs
@@ -46,11 +46,9 @@ namespace Umbraco.Cms.Core
public const string AppPlugins = "/App_Plugins";
[Obsolete("Use PluginIcons instead")]
- public const string AppPluginIcons = "/Backoffice/Icons";
- public const string PluginIcons = "/backoffice/icons";
-
- public const string CreatedPackages = "/created-packages";
+ public static string AppPluginIcons => "/Backoffice/Icons";
+ public const string PluginIcons = "/backoffice/icons";
public const string MvcViews = "~/Views";
@@ -60,6 +58,8 @@ namespace Umbraco.Cms.Core
public const string Packages = Data + "/packages";
+ public const string CreatedPackages = Data + "/CreatedPackages";
+
public const string Preview = Data + "/preview";
///
diff --git a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs
new file mode 100644
index 0000000000..b1fb31d2aa
--- /dev/null
+++ b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Models.Membership;
+
+namespace Umbraco.Cms.Core.ContentApps
+{
+ internal class DictionaryContentAppFactory : IContentAppFactory
+ {
+ private const int Weight = -100;
+
+ private ContentApp _dictionaryApp;
+
+ public ContentApp GetContentAppFor(object source, IEnumerable userGroups)
+ {
+ switch (source)
+ {
+ case IDictionaryItem _:
+ return _dictionaryApp ??= new ContentApp
+ {
+ Alias = "dictionaryContent",
+ Name = "Content",
+ Icon = "icon-document",
+ View = "views/dictionary/views/content/content.html",
+ Weight = Weight
+ };
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
index 811ee35c14..a0ff6104a7 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
@@ -46,7 +46,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
.Append()
.Append()
.Append()
- .Append();
+ .Append()
+ .Append();
// all built-in finders in the correct order,
// devs can then modify this list on application startup
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index c4a95d45e5..235dc71252 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -262,6 +262,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton();
// Register telemetry service used to gather data about installed packages
+ Services.AddUnique();
Services.AddUnique();
// Register a noop IHtmlSanitizer to be replaced
diff --git a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
index 0a6518a1ca..3a73173127 100644
--- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
+++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
@@ -33,7 +33,8 @@ namespace Umbraco.Cms.Core.Events
Constants.Conventions.RelationTypes.RelateDocumentOnCopyName,
true,
Constants.ObjectTypes.Document,
- Constants.ObjectTypes.Document);
+ Constants.ObjectTypes.Document,
+ false);
_relationService.Save(relationType);
}
diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
index bceddf1fd6..0319be3297 100644
--- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
+++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
@@ -132,7 +132,7 @@ namespace Umbraco.Extensions
}
}
- verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
+ verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
return true;
}
diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
index 37769afc53..d95fa6919d 100644
--- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
+++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
@@ -1,10 +1,13 @@
using System;
using System.Threading.Tasks;
+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.Install.Models;
+using Umbraco.Cms.Core.Telemetry;
+using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Install.InstallSteps
{
@@ -13,31 +16,29 @@ namespace Umbraco.Cms.Core.Install.InstallSteps
PerformsAppRestart = false)]
public class TelemetryIdentifierStep : InstallSetupStep
[DataMember(Name = "translations")]
public List Translations { get; private set; }
+
+ ///
+ /// Apps for the dictionary item
+ ///
+ [DataMember(Name = "apps")]
+ public List ContentApps { get; private set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
index b7bfb32808..a0d9bbbcb3 100644
--- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
@@ -1,17 +1,34 @@
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models.ContentEditing
{
[DataContract(Name = "historyCleanup", Namespace = "")]
- public class HistoryCleanup
+ public class HistoryCleanup : BeingDirtyBase
{
+ private bool _preventCleanup;
+ private int? _keepAllVersionsNewerThanDays;
+ private int? _keepLatestVersionPerDayForDays;
+
[DataMember(Name = "preventCleanup")]
- public bool PreventCleanup { get; set; }
+ public bool PreventCleanup
+ {
+ get => _preventCleanup;
+ set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup));
+ }
[DataMember(Name = "keepAllVersionsNewerThanDays")]
- public int? KeepAllVersionsNewerThanDays { get; set; }
+ public int? KeepAllVersionsNewerThanDays
+ {
+ get => _keepAllVersionsNewerThanDays;
+ set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays));
+ }
[DataMember(Name = "keepLatestVersionPerDayForDays")]
- public int? KeepLatestVersionPerDayForDays { get; set; }
+ public int? KeepLatestVersionPerDayForDays
+ {
+ get => _keepLatestVersionPerDayForDays;
+ set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays));
+ }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
index 27f0f525df..6a4c8e5f81 100644
--- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
@@ -55,5 +55,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "notifications")]
public List Notifications { get; private set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember(Name = "isDependency", IsRequired = true)]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
index b72a03eec4..f541158095 100644
--- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
@@ -23,5 +23,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "childObjectType", IsRequired = false)]
public Guid? ChildObjectType { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember(Name = "isDependency", IsRequired = true)]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs
index 6ff94f57f3..a252aa4723 100644
--- a/src/Umbraco.Core/Models/ContentType.cs
+++ b/src/Umbraco.Core/Models/ContentType.cs
@@ -96,7 +96,13 @@ namespace Umbraco.Cms.Core.Models
}
}
- public HistoryCleanup HistoryCleanup { get; set; }
+ private HistoryCleanup _historyCleanup;
+
+ public HistoryCleanup HistoryCleanup
+ {
+ get => _historyCleanup;
+ set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup));
+ }
///
/// Determines if AllowedTemplates contains templateId
@@ -162,5 +168,8 @@ namespace Umbraco.Cms.Core.Models
///
IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) =>
(IContentType)DeepCloneWithResetIdentities(newAlias);
+
+ ///
+ public override bool IsDirty() => base.IsDirty() || HistoryCleanup.IsDirty();
}
}
diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs
index 9efde4b939..3ee1517f55 100644
--- a/src/Umbraco.Core/Models/IRelationType.cs
+++ b/src/Umbraco.Core/Models/IRelationType.cs
@@ -4,6 +4,15 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models
{
+ public interface IRelationTypeWithIsDependency : IRelationType
+ {
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember]
+ bool IsDependency { get; set; }
+ }
+
public interface IRelationType : IEntity, IRememberBeingDirty
{
///
diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
index 3cfcc89085..e424dabb93 100644
--- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.ContentApps;
@@ -48,6 +48,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
public IEnumerable GetContentApps(IUmbracoEntity source)
+ {
+ return GetContentAppsForEntity(source);
+ }
+
+ public IEnumerable GetContentAppsForEntity(IEntity source)
{
var apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray();
diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
index 3625e90a14..2f85a95953 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
@@ -133,7 +133,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup)
{
- targetWithHistoryCleanup.HistoryCleanup = source.HistoryCleanup;
+ MapHistoryCleanup(source, targetWithHistoryCleanup);
}
target.AllowedTemplates = source.AllowedTemplates
@@ -147,6 +147,34 @@ namespace Umbraco.Cms.Core.Models.Mapping
: _fileService.GetTemplate(source.DefaultTemplate));
}
+ private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target)
+ {
+ // If source history cleanup is null we don't have to map all properties
+ if (source.HistoryCleanup is null)
+ {
+ target.HistoryCleanup = null;
+ return;
+ }
+
+ // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties
+ target.HistoryCleanup.ResetDirtyProperties(false);
+ if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup)
+ {
+ target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup;
+ }
+
+ if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays)
+ {
+ target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays;
+ }
+
+ if (target.HistoryCleanup.KeepLatestVersionPerDayForDays !=
+ source.HistoryCleanup.KeepLatestVersionPerDayForDays)
+ {
+ target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays;
+ }
+ }
+
// no MapAll - take care
private void Map(MediaTypeSave source, IMediaType target, MapperContext context)
{
@@ -196,7 +224,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.AllowCultureVariant = source.VariesByCulture();
target.AllowSegmentVariant = source.VariesBySegment();
- target.ContentApps = _commonMapper.GetContentApps(source);
+ target.ContentApps = _commonMapper.GetContentAppsForEntity(source);
//sync templates
target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates);
@@ -328,7 +356,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (source.GroupId > 0)
{
- target.PropertyGroupId = new Lazy(() => source.GroupId, false);
+ if (target.PropertyGroupId?.Value != source.GroupId)
+ {
+ target.PropertyGroupId = new Lazy(() => source.GroupId, false);
+ }
}
target.Alias = source.Alias;
@@ -523,7 +554,15 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Thumbnail = source.Thumbnail;
target.AllowedAsRoot = source.AllowAsRoot;
- target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i));
+
+ bool allowedContentTypesUnchanged = target.AllowedContentTypes.Select(x => x.Id.Value)
+ .SequenceEqual(source.AllowedContentTypes);
+
+ if (allowedContentTypesUnchanged is false)
+ {
+ target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i));
+ }
+
if (!(target is IMemberType))
{
@@ -574,13 +613,21 @@ namespace Umbraco.Cms.Core.Models.Mapping
// ensure no duplicate alias, then assign the group properties collection
EnsureUniqueAliases(destProperties);
- destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
+ if (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false)
+ {
+ destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
+ }
+
destGroups.Add(destGroup);
}
// ensure no duplicate name, then assign the groups collection
EnsureUniqueAliases(destGroups);
- target.PropertyGroups = new PropertyGroupCollection(destGroups);
+
+ if (target.PropertyGroups.SequenceEqual(destGroups) is false)
+ {
+ target.PropertyGroups = new PropertyGroupCollection(destGroups);
+ }
// because the property groups collection was rebuilt, there is no need to remove
// the old groups - they are just gone and will be cleared by the repository
diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
index 4c000f0173..b93f99c5c5 100644
--- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
+using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
@@ -14,12 +15,20 @@ namespace Umbraco.Cms.Core.Models.Mapping
public class DictionaryMapDefinition : IMapDefinition
{
private readonly ILocalizationService _localizationService;
+ private readonly CommonMapper _commonMapper;
+ [Obsolete("Use the constructor with the CommonMapper")]
public DictionaryMapDefinition(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
+ public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper)
+ {
+ _localizationService = localizationService;
+ _commonMapper = commonMapper;
+ }
+
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define((source, context) => new EntityBasic(), Map);
@@ -44,6 +53,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Name = source.ItemKey;
target.ParentId = source.ParentId ?? Guid.Empty;
target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key);
+ if (_commonMapper != null)
+ {
+ target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source));
+ }
// build up the path to make it possible to set active item in tree
// TODO: check if there is a better way
diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
index 41caa526e2..2b333652b9 100644
--- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
@@ -30,6 +30,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.ChildObjectType = source.ChildObjectType;
target.Id = source.Id;
target.IsBidirectional = source.IsBidirectional;
+
+ if (source is IRelationTypeWithIsDependency sourceWithIsDependency)
+ {
+ target.IsDependency = sourceWithIsDependency.IsDependency;
+ }
target.Key = source.Key;
target.Name = source.Name;
target.Alias = source.Alias;
@@ -74,6 +79,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.ChildObjectType = source.ChildObjectType;
target.Id = source.Id.TryConvertTo().Result;
target.IsBidirectional = source.IsBidirectional;
+ if (target is IRelationTypeWithIsDependency targetWithIsDependency)
+ {
+ targetWithIsDependency.IsDependency = source.IsDependency;
+ }
+
target.Key = source.Key;
target.Name = source.Name;
target.ParentObjectType = source.ParentObjectType;
diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs
new file mode 100644
index 0000000000..cebbc20951
--- /dev/null
+++ b/src/Umbraco.Core/Models/RelationItem.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.Entities;
+
+namespace Umbraco.Cms.Core.Models
+{
+ [DataContract(Name = "relationItem", Namespace = "")]
+ public class RelationItem
+ {
+ [DataMember(Name = "id")]
+ public int NodeId { get; set; }
+
+ [DataMember(Name = "key")]
+ public Guid NodeKey { get; set; }
+
+ [DataMember(Name = "name")]
+ public string NodeName { get; set; }
+
+ [DataMember(Name = "type")]
+ public string NodeType { get; set; }
+
+ [DataMember(Name = "udi")]
+ public Udi NodeUdi => Udi.Create(NodeType, NodeKey);
+
+ [DataMember(Name = "icon")]
+ public string ContentTypeIcon { get; set; }
+
+ [DataMember(Name = "alias")]
+ public string ContentTypeAlias { get; set; }
+
+ [DataMember(Name = "contentTypeName")]
+ public string ContentTypeName { get; set; }
+
+ [DataMember(Name = "relationTypeName")]
+ public string RelationTypeName { get; set; }
+
+ [DataMember(Name = "relationTypeIsBidirectional")]
+ public bool RelationTypeIsBidirectional { get; set; }
+
+ [DataMember(Name = "relationTypeIsDependency")]
+ public bool RelationTypeIsDependency { get; set; }
+
+ }
+}
diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs
index 2def0eb636..5de0aaac8f 100644
--- a/src/Umbraco.Core/Models/RelationType.cs
+++ b/src/Umbraco.Core/Models/RelationType.cs
@@ -9,20 +9,28 @@ namespace Umbraco.Cms.Core.Models
///
[Serializable]
[DataContract(IsReference = true)]
- public class RelationType : EntityBase, IRelationType
+ public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency
{
private string _name;
private string _alias;
private bool _isBidirectional;
+ private bool _isDependency;
private Guid? _parentObjectType;
private Guid? _childObjectType;
public RelationType(string alias, string name)
- : this(name: name, alias: alias, false, null, null)
+ : this(name: name, alias: alias, false, null, null, false)
{
}
+ [Obsolete("Use ctor with isDependency parameter")]
public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType)
+ :this(name,alias,isBidrectional, parentObjectType, childObjectType, false)
+ {
+
+ }
+
+ public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
@@ -32,6 +40,7 @@ namespace Umbraco.Cms.Core.Models
_name = name;
_alias = alias;
_isBidirectional = isBidrectional;
+ _isDependency = isDependency;
_parentObjectType = parentObjectType;
_childObjectType = childObjectType;
}
@@ -88,5 +97,11 @@ namespace Umbraco.Cms.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType));
}
+
+ public bool IsDependency
+ {
+ get => _isDependency;
+ set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency));
+ }
}
}
diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs
index 36b7a5d5d5..ac09655476 100644
--- a/src/Umbraco.Core/Packaging/PackagesRepository.cs
+++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
-using System.Text;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -33,7 +32,7 @@ namespace Umbraco.Cms.Core.Packaging
private readonly IEntityXmlSerializer _serializer;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly string _packageRepositoryFileName;
- private readonly string _mediaFolderPath;
+ private readonly string _createdPackagesFolderPath;
private readonly string _packagesFolderPath;
private readonly string _tempFolderPath;
private readonly PackageDefinitionXmlParser _parser;
@@ -93,7 +92,7 @@ namespace Umbraco.Cms.Core.Packaging
_tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
_packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages;
- _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);
+ _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages;
_parser = new PackageDefinitionXmlParser();
_mediaService = mediaService;
@@ -250,15 +249,8 @@ namespace Umbraco.Cms.Core.Packaging
}
}
-
-
- var directoryName =
- _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_')));
-
- if (Directory.Exists(directoryName) == false)
- {
- Directory.CreateDirectory(directoryName);
- }
+ var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_')));
+ Directory.CreateDirectory(directoryName);
var finalPackagePath = Path.Combine(directoryName, fileName);
@@ -276,14 +268,14 @@ namespace Umbraco.Cms.Core.Packaging
}
finally
{
- //Clean up
+ // Clean up
Directory.Delete(temporaryPath, true);
}
}
private void ValidatePackage(PackageDefinition definition)
{
- //ensure it's valid
+ // ensure it's valid
var context = new ValidationContext(definition, serviceProvider: null, items: null);
var results = new List();
var isValid = Validator.TryValidateObject(definition, context, results);
@@ -732,7 +724,6 @@ namespace Umbraco.Cms.Core.Packaging
private XDocument EnsureStorage(out string packagesFile)
{
var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath);
- //ensure it exists
Directory.CreateDirectory(packagesFolder);
packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile);
@@ -740,6 +731,8 @@ namespace Umbraco.Cms.Core.Packaging
{
var xml = new XDocument(new XElement("packages"));
xml.Save(packagesFile);
+
+ return xml;
}
var packagesXml = XDocument.Load(packagesFile);
@@ -749,9 +742,16 @@ namespace Umbraco.Cms.Core.Packaging
public void DeleteLocalRepositoryFiles()
{
var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile);
- File.Delete(packagesFile);
+ if (File.Exists(packagesFile))
+ {
+ File.Delete(packagesFile);
+ }
+
var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath);
- Directory.Delete(packagesFolder);
+ if (Directory.Exists(packagesFolder))
+ {
+ Directory.Delete(packagesFolder);
+ }
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs
index 5312bf6886..3c0b2c4d28 100644
--- a/src/Umbraco.Core/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs
@@ -65,6 +65,11 @@ namespace Umbraco.Cms.Core
/// All languages.
///
public const int Languages = -340;
+
+ ///
+ /// ScheduledPublishing job.
+ ///
+ public const int ScheduledPublishing = -341;
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs
new file mode 100644
index 0000000000..f6cd27ad60
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+ [Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")]
+ public interface IMacroWithAliasRepository : IMacroRepository
+ {
+ IMacro GetByAlias(string alias);
+
+ IEnumerable GetAllByAlias(string[] aliases);
+ }
+}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
index cff64d4a79..c863ef4d8e 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
new file mode 100644
index 0000000000..e6ca8eaa50
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+ public interface ITrackedReferencesRepository
+ {
+ ///
+ /// Gets a page of items which are in relation with the current item.
+ /// Basically, shows the items which depend on the current item.
+ ///
+ /// The identifier of the entity to retrieve relations for.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// The total count of the items with reference to the current item.
+ /// An enumerable list of objects.
+ IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
+
+ ///
+ /// Gets a page of items used in any kind of relation from selected integer ids.
+ ///
+ /// The identifiers of the entities to check for relations.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// The total count of the items in any kind of relation.
+ /// An enumerable list of objects.
+ IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
+
+ ///
+ /// Gets a page of the descending items that have any references, given a parent id.
+ ///
+ /// The unique identifier of the parent to retrieve descendants for.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// The total count of descending items.
+ /// An enumerable list of objects.
+ IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords);
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs
index 048ad40ac0..4d88431e7c 100644
--- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs
@@ -1,5 +1,5 @@
-using Microsoft.Extensions.Logging;
-using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -28,5 +28,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
DefaultConfiguration.Add("minNumber",0 );
DefaultConfiguration.Add("maxNumber", 0);
}
+
+ protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute);
+
+ internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase
+ {
+ public MultipleContentPickerParamateterValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService)
+ {
+ }
+
+ public override string UdiEntityType { get; } = Constants.UdiEntityType.Document;
+ public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document;
+ }
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs
index d8f74b1b28..dfdd6f9b9c 100644
--- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs
@@ -1,5 +1,9 @@
-using Microsoft.Extensions.Logging;
-using Umbraco.Cms.Core.Hosting;
+using System;
+using System.Collections.Generic;
+using System.Reflection.Metadata;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -26,5 +30,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
{
DefaultConfiguration.Add("multiPicker", "1");
}
+
+ protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute);
+
+ internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase
+ {
+ public MultipleMediaPickerPropertyValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService)
+ {
+ }
+
+ public override string UdiEntityType { get; } = Constants.UdiEntityType.Media;
+ public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media;
+ }
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs
new file mode 100644
index 0000000000..2c4f27b560
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Strings;
+
+namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors
+{
+ internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference
+ {
+ private readonly IEntityService _entityService;
+
+ public MultiplePickerParamateterValueEditorBase(
+ ILocalizedTextService localizedTextService,
+ IShortStringHelper shortStringHelper,
+ IJsonSerializer jsonSerializer,
+ IIOHelper ioHelper,
+ DataEditorAttribute attribute,
+ IEntityService entityService)
+ : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
+ {
+ _entityService = entityService;
+ }
+
+ public abstract string UdiEntityType { get; }
+ public abstract UmbracoObjectTypes UmbracoObjectType { get; }
+ public IEnumerable GetReferences(object value)
+ {
+ var asString = value is string str ? str : value?.ToString();
+
+ if (string.IsNullOrEmpty(asString))
+ {
+ yield break;
+ }
+
+ foreach (var udiStr in asString.Split(','))
+ {
+ if (UdiParser.TryParse(udiStr, out Udi udi))
+ {
+ yield return new UmbracoEntityReference(udi);
+ }
+
+ // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis
+ if (int.TryParse(udiStr, out var id))
+ {
+ Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType);
+ Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty;
+
+ if (guid != Guid.Empty)
+ {
+ yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid));
+ }
+
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
index 5c27760b2a..eea53aaa9c 100644
--- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
@@ -159,7 +159,7 @@ namespace Umbraco.Cms.Core.Routing
: 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 is null || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
+ if (domainUri is not null || string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
{
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
diff --git a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
new file mode 100644
index 0000000000..5b8fb819e6
--- /dev/null
+++ b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Cms.Core.Runtime
+{
+ ///
+ /// Defines a class which can generate a distinct key for a MainDom boundary.
+ ///
+ public interface IMainDomKeyGenerator
+ {
+ ///
+ /// Returns a key that signifies a MainDom boundary.
+ ///
+ string GenerateKey();
+ }
+}
diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs
index 08d11db5cd..d22176d9cf 100644
--- a/src/Umbraco.Core/Runtime/MainDom.cs
+++ b/src/Umbraco.Core/Runtime/MainDom.cs
@@ -87,7 +87,7 @@ namespace Umbraco.Cms.Core.Runtime
if (_isMainDom.HasValue == false)
{
- throw new InvalidOperationException("Register called when MainDom has not been acquired");
+ throw new InvalidOperationException("Register called before IsMainDom has been established");
}
else if (_isMainDom == false)
{
@@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Runtime
{
if (!_isMainDom.HasValue)
{
- throw new InvalidOperationException("MainDom has not been acquired yet");
+ throw new InvalidOperationException("IsMainDom has not been established yet");
}
return _isMainDom.Value;
}
diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
index ca3045a4de..b8c7596b2d 100644
--- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
+++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
@@ -8,12 +8,17 @@ namespace Umbraco.Cms.Core.Security
{
///
- /// Handles password hashing and formatting for legacy hashing algorithms
+ /// Handles password hashing and formatting for legacy hashing algorithms.
///
+ ///
+ /// Should probably be internal.
+ ///
public class LegacyPasswordSecurity
{
+ // TODO: Remove v11
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string HashPasswordForStorage(string algorithmType, string password)
{
if (string.IsNullOrWhiteSpace(password))
@@ -24,13 +29,15 @@ namespace Umbraco.Cms.Core.Security
return FormatPasswordForStorage(algorithmType, hashed, salt);
}
+ // TODO: Remove v11
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
- if (IsLegacySHA1Algorithm(algorithmType))
+ if (!SupportHashAlgorithm(algorithmType))
{
- return hashedPassword;
+ throw new InvalidOperationException($"{algorithmType} is not supported");
}
return salt + hashedPassword;
@@ -45,10 +52,15 @@ namespace Umbraco.Cms.Core.Security
///
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
- if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
+ if (string.IsNullOrWhiteSpace(dbPassword))
+ {
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
+ }
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
+ {
return false;
+ }
try
{
@@ -61,7 +73,6 @@ namespace Umbraco.Cms.Core.Security
//This can happen if the length of the password is wrong and a salt cannot be extracted.
return false;
}
-
}
///
@@ -69,12 +80,13 @@ namespace Umbraco.Cms.Core.Security
///
public bool VerifyLegacyHashedPassword(string password, string dbPassword)
{
- var hashAlgorith = new HMACSHA1
+ var hashAlgorithm = new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password)
};
- var hashed = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password)));
+
+ var hashed = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return dbPassword == hashed;
}
@@ -87,6 +99,8 @@ namespace Umbraco.Cms.Core.Security
///
///
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
+ // TODO: Remove v11
+ [Obsolete("We shouldn't be altering our public API to make test code easier, removing v11")]
public string HashNewPassword(string algorithm, string newPassword, out string salt)
{
salt = GenerateSalt();
@@ -102,15 +116,15 @@ namespace Umbraco.Cms.Core.Security
///
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
- if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
-
- // This is for the <= v4 hashing algorithm for which there was no salt
- if (IsLegacySHA1Algorithm(algorithm))
+ if (string.IsNullOrWhiteSpace(storedString))
{
- salt = string.Empty;
- return storedString;
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
}
+ if (!SupportHashAlgorithm(algorithm))
+ {
+ throw new InvalidOperationException($"{algorithm} is not supported");
+ }
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
@@ -133,12 +147,12 @@ namespace Umbraco.Cms.Core.Security
///
private string HashPassword(string algorithmType, string pass, string salt)
{
- if (IsLegacySHA1Algorithm(algorithmType))
+ if (!SupportHashAlgorithm(algorithmType))
{
- return HashLegacySHA1Password(pass);
+ throw new InvalidOperationException($"{algorithmType} is not supported");
}
- //This is the correct way to implement this (as per the sql membership provider)
+ // This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
@@ -209,42 +223,17 @@ namespace Umbraco.Cms.Core.Security
{
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
+ {
return true;
+ }
- // This is for the <= v4 hashing algorithm
- if (IsLegacySHA1Algorithm(algorithm))
+ // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes)
+ if (algorithm.InvariantEquals("SHA1"))
+ {
return true;
+ }
return false;
}
-
- private bool IsLegacySHA1Algorithm(string algorithm) => algorithm.InvariantEquals(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName);
-
- ///
- /// Hashes the password with the old v4 algorithm
- ///
- /// The password.
- /// The encoded password.
- private string HashLegacySHA1Password(string password)
- {
- using var hashAlgorithm = GetLegacySHA1Algorithm(password);
- var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
- return hash;
- }
-
- ///
- /// Returns the old v4 algorithm and settings
- ///
- ///
- ///
- private HashAlgorithm GetLegacySHA1Algorithm(string password)
- {
- return new HMACSHA1
- {
- //the legacy salt was actually the password :(
- Key = Encoding.Unicode.GetBytes(password)
- };
- }
-
}
}
diff --git a/src/Umbraco.Core/Services/IMacroService.cs b/src/Umbraco.Core/Services/IMacroService.cs
index c4bc34997f..e1eb97ac00 100644
--- a/src/Umbraco.Core/Services/IMacroService.cs
+++ b/src/Umbraco.Core/Services/IMacroService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
@@ -17,13 +17,6 @@ namespace Umbraco.Cms.Core.Services
/// An object
IMacro GetByAlias(string alias);
- /////
- ///// Gets a list all available objects
- /////
- ///// Optional array of aliases to limit the results
- ///// An enumerable list of objects
- //IEnumerable GetAll(params string[] aliases);
-
IEnumerable GetAll();
IEnumerable GetAll(params int[] ids);
diff --git a/src/Umbraco.Core/Services/IMacroWithAliasService.cs b/src/Umbraco.Core/Services/IMacroWithAliasService.cs
new file mode 100644
index 0000000000..6e72777bfa
--- /dev/null
+++ b/src/Umbraco.Core/Services/IMacroWithAliasService.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ [Obsolete("This interface will be merged with IMacroService in Umbraco 11")]
+ public interface IMacroWithAliasService : IMacroService
+ {
+ ///
+ /// Gets a list of available objects by alias.
+ ///
+ /// Optional array of aliases to limit the results
+ /// An enumerable list of objects
+ IEnumerable GetAll(params string[] aliases);
+ }
+}
diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs
index ce00f774f8..4d0e977d38 100644
--- a/src/Umbraco.Core/Services/IRelationService.cs
+++ b/src/Umbraco.Core/Services/IRelationService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -215,7 +215,7 @@ namespace Umbraco.Cms.Core.Services
///
///
///
- ///
+ /// An enumerable list of
IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
///
@@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Services
///
///
///
- ///
+ /// An enumerable list of
IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
///
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
new file mode 100644
index 0000000000..dea99c0f6d
--- /dev/null
+++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
@@ -0,0 +1,38 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public interface ITrackedReferencesService
+ {
+ ///
+ /// Gets a paged result of items which are in relation with the current item.
+ /// Basically, shows the items which depend on the current item.
+ ///
+ /// The identifier of the entity to retrieve relations for.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// A paged result of objects.
+ PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+
+ ///
+ /// Gets a paged result of the descending items that have any references, given a parent id.
+ ///
+ /// The unique identifier of the parent to retrieve descendants for.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// A paged result of objects.
+ PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+
+ ///
+ /// Gets a paged result of items used in any kind of relation from selected integer ids.
+ ///
+ /// The identifiers of the entities to check for relations.
+ /// The page index.
+ /// The page size.
+ /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true).
+ /// A paged result of objects.
+ PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+ }
+}
diff --git a/src/Umbraco.Core/StaticApplicationLogging.cs b/src/Umbraco.Core/StaticApplicationLogging.cs
index e216011014..d7dfc8dd9a 100644
--- a/src/Umbraco.Core/StaticApplicationLogging.cs
+++ b/src/Umbraco.Core/StaticApplicationLogging.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -5,18 +6,14 @@ namespace Umbraco.Cms.Core
{
public static class StaticApplicationLogging
{
- private static ILoggerFactory _loggerFactory;
+ private static ILoggerFactory s_loggerFactory;
- public static void Initialize(ILoggerFactory loggerFactory)
- {
- _loggerFactory = loggerFactory;
- }
+ public static void Initialize(ILoggerFactory loggerFactory) => s_loggerFactory = loggerFactory;
public static ILogger
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
+ private readonly ILogger _logger;
private TimeSpan _period;
private readonly TimeSpan _delay;
private Timer _timer;
+ private bool _disposedValue;
///
/// Initializes a new instance of the class.
///
- /// Timepsan representing how often the task should recur.
- /// Timespan represeting the initial delay after application start-up before the first run of the task occurs.
- protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay)
+ /// Logger.
+ /// Timespan representing how often the task should recur.
+ /// Timespan representing the initial delay after application start-up before the first run of the task occurs.
+ protected RecurringHostedServiceBase(ILogger logger, TimeSpan period, TimeSpan delay)
{
+ _logger = logger;
_period = period;
_delay = delay;
}
+ // Scheduled for removal in V11
+ [Obsolete("Please use constructor that takes an ILogger instead")]
+ protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay)
+ : this(null, period, delay)
+ { }
+
///
public Task StartAsync(CancellationToken cancellationToken)
{
- _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
+ using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null)
+ {
+ _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
+ }
+
return Task.CompletedTask;
}
@@ -60,6 +76,11 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
// Hat-tip: https://stackoverflow.com/a/14207615/489433
await PerformExecuteAsync(state);
}
+ catch (Exception ex)
+ {
+ ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType());
+ logger.LogError(ex, "Unhandled exception in recurring hosted service.");
+ }
finally
{
// Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay.
@@ -78,7 +99,24 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
return Task.CompletedTask;
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _timer?.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
///
- public void Dispose() => _timer?.Dispose();
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
index 7591290bf4..6e5d412e71 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
@@ -23,7 +23,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
public ReportSiteTask(
ILogger logger,
ITelemetryService telemetryService)
- : base(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))
+ : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))
{
_logger = logger;
_telemetryService = telemetryService;
@@ -59,9 +59,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
// Send data to LIVE telemetry
s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
- // Set a low timeout - no need to use a larger default timeout for this POST request
- s_httpClient.Timeout = new TimeSpan(0, 0, 1);
-
#if DEBUG
// Send data to DEBUG telemetry service
s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
index d59ea4fad3..fd70c05fc1 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
@@ -5,13 +5,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Runtime;
-using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Infrastructure.HostedServices
{
@@ -27,20 +29,16 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _serverMessenger;
+ private readonly IScopeProvider _scopeProvider;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
///
/// Initializes a new instance of the class.
///
- /// Representation of the state of the Umbraco runtime.
- /// Representation of the main application domain.
- /// Provider of server registrations to the distributed cache.
- /// Service for handling content operations.
- /// Service for creating and managing Umbraco context.
- /// The typed logger.
- /// Service broadcasting cache notifications to registered servers.
- /// Creates and manages instances.
+ // Note: Ignoring the two version notice rule as this class should probably be internal.
+ // We don't expect anyone downstream to be instantiating a HostedService
+ [Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
public ScheduledPublishing(
IRuntimeState runtimeState,
IMainDom mainDom,
@@ -49,7 +47,31 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
IUmbracoContextFactory umbracoContextFactory,
ILogger logger,
IServerMessenger serverMessenger)
- : base(TimeSpan.FromMinutes(1), DefaultDelay)
+ : this(
+ runtimeState,
+ mainDom,
+ serverRegistrar,
+ contentService,
+ umbracoContextFactory,
+ logger,
+ serverMessenger,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ScheduledPublishing(
+ IRuntimeState runtimeState,
+ IMainDom mainDom,
+ IServerRoleAccessor serverRegistrar,
+ IContentService contentService,
+ IUmbracoContextFactory umbracoContextFactory,
+ ILogger logger,
+ IServerMessenger serverMessenger,
+ IScopeProvider scopeProvider)
+ : base(logger, TimeSpan.FromMinutes(1), DefaultDelay)
{
_runtimeState = runtimeState;
_mainDom = mainDom;
@@ -58,6 +80,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
_umbracoContextFactory = umbracoContextFactory;
_logger = logger;
_serverMessenger = serverMessenger;
+ _scopeProvider = scopeProvider;
}
public override Task PerformExecuteAsync(object state)
@@ -93,8 +116,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
try
{
- // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
- // so it's safe as it will create it's own ambient scope.
// Ensure we run with an UmbracoContext, because this will run in a background task,
// and developers may be using the UmbracoContext in the event handlers.
@@ -105,6 +126,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
// - and we should definitively *not* have to flush it here (should be auto)
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+
+ /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
+ * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
+ * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
+ * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
+ * only until the old SchedulingPublisher shuts down. */
+ scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
try
{
// Run
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
index 57354aafdb..3aa49f3f71 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _messenger;
private readonly ILogger _logger;
+ private bool _disposedValue;
///
/// Initializes a new instance of the class.
@@ -29,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
/// The typed logger.
/// The configuration for global settings.
public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings)
- : base(globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
+ : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
{
_runtimeState = runtimeState;
_messenger = messenger;
@@ -54,5 +55,20 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
return Task.CompletedTask;
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing && _messenger is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+
+ base.Dispose(disposing);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
index d54d67338e..5f20a3654e 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
@@ -44,7 +44,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
ILogger logger,
IOptions globalSettings,
IServerRoleAccessor serverRoleAccessor)
- : base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
+ : base(logger, globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
{
_runtimeState = runtimeState;
_serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService));
diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
index e59cca5fbd..8a2a312455 100644
--- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
@@ -33,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
/// Representation of the main application domain.
/// The typed logger.
public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger)
- : base(TimeSpan.FromMinutes(60), DefaultDelay)
+ : base(logger, TimeSpan.FromMinutes(60), DefaultDelay)
{
_ioHelper = ioHelper;
_mainDom = mainDom;
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index 4f2ef1f2e9..b19802996b 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -175,6 +175,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" });
}
@@ -420,21 +421,21 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
private void CreateRelationTypeData()
{
- var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName };
+ var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, IsDependency = false};
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName };
+ relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, IsDependency = false };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName };
+ relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, IsDependency = false };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName };
+ relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, IsDependency = true };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName };
+ relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, IsDependency = true };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 39d7d886b3..d3a920a60b 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -16,6 +16,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0;
+using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
@@ -280,6 +281,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
To("{0828F206-DCF7-4F73-ABBB-6792275532EB}");
+ // TO 9.4.0
+ To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}");
+ To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}");
}
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs
new file mode 100644
index 0000000000..01cfb22a3d
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs
@@ -0,0 +1,15 @@
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
+{
+ internal class AddScheduledPublishingLock : MigrationBase
+ {
+ public AddScheduledPublishingLock(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override void Migrate() =>
+ Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs
new file mode 100644
index 0000000000..1c8fe7ed72
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
+{
+ internal class UpdateRelationTypesToHandleDependencies : MigrationBase
+ {
+ public UpdateRelationTypesToHandleDependencies(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "isDependency");
+
+ var aliasesWithDependencies = new[]
+ {
+ Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias,
+ Core.Constants.Conventions.RelationTypes.RelatedMediaAlias
+ };
+
+ Database.Execute(
+ Sql()
+ .Update(u => u.Set(x => x.IsDependency, true))
+ .WhereIn(x => x.Alias, aliasesWithDependencies));
+
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
index 50d7960ff8..388fa58941 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
@@ -40,5 +40,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
[Length(100)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")]
public string Alias { get; set; }
+
+ [Constraint(Default = "0")]
+ [Column("isDependency")]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
index 51f5261199..93cb74cd74 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
public static IRelationType BuildEntity(RelationTypeDto dto)
{
- var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType);
+ var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency);
try
{
@@ -30,11 +30,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
public static RelationTypeDto BuildDto(IRelationType entity)
{
+ var isDependency = false;
+ if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency)
+ {
+ isDependency = relationTypeWithIsDependency.IsDependency;
+ }
var dto = new RelationTypeDto
{
Alias = entity.Alias,
ChildObjectType = entity.ChildObjectType,
Dual = entity.IsBidirectional,
+ IsDependency = isDependency,
Name = entity.Name,
ParentObjectType = entity.ParentObjectType,
UniqueId = entity.Key
@@ -47,6 +53,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
return dto;
}
+
+
#endregion
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
index 965a659631..732563fef7 100644
--- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
@@ -22,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias));
DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType));
DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual));
+ DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency));
DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name));
DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType));
}
diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
index 05f15f7372..47cca58ce2 100644
--- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
+++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
@@ -72,7 +72,27 @@ namespace Umbraco.Extensions
/// The Sql statement.
public static Sql WhereIn(this Sql sql, Expression> field, Sql values)
{
- return sql.WhereIn(field, values, false);
+ return WhereIn(sql, field, values, false, null);
+ }
+
+ public static Sql WhereIn(this Sql sql, Expression> field, Sql values, string tableAlias)
+ {
+ return sql.WhereIn(field, values, false, tableAlias);
+ }
+
+
+ public static Sql WhereLike(this Sql sql, Expression> fieldSelector, Sql valuesSql)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ sql.Where(fieldName + " LIKE (" + valuesSql.SQL + ")", valuesSql.Arguments);
+ return sql;
+ }
+
+ public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ sql.Where(fieldName + " LIKE ('" + likeValue + "')");
+ return sql;
}
///
@@ -130,7 +150,12 @@ namespace Umbraco.Extensions
private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql valuesSql, bool not)
{
- var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ return WhereIn(sql, fieldSelector, valuesSql, not, null);
+ }
+
+ private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql valuesSql, bool not, string tableAlias)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias);
sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql.SQL + ")", valuesSql.Arguments);
return sql;
}
@@ -252,7 +277,7 @@ namespace Umbraco.Extensions
/// The Sql statement.
public static Sql OrderByDescending(this Sql sql, Expression> field)
{
- return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ") DESC");
+ return sql.OrderByDescending(sql.SqlContext.SqlSyntax.GetFieldName(field));
}
///
@@ -268,7 +293,7 @@ namespace Umbraco.Extensions
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
: fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
- return sql.OrderBy(columns.Select(x => x + " DESC"));
+ return sql.OrderByDescending(columns);
}
///
@@ -634,6 +659,12 @@ namespace Umbraco.Extensions
return sql;
}
+ public static Sql SelectDistinct(this Sql sql, params object[] columns)
+ {
+ sql.Append("SELECT DISTINCT " + string.Join(", ", columns));
+ return sql;
+ }
+
//this.Append("SELECT " + string.Join(", ", columns), new object[0]);
///
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs
index be1a31c2c9..b705bcabca 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs
@@ -39,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
private readonly IMacroService _macroService;
private readonly IContentTypeService _contentTypeService;
private readonly string _tempFolderPath;
- private readonly string _mediaFolderPath;
+ private readonly string _createdPackagesFolderPath;
///
/// Initializes a new instance of the class.
@@ -76,9 +76,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
_macroService = macroService;
_contentTypeService = contentTypeService;
_xmlParser = new PackageDefinitionXmlParser();
- _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);
- _tempFolderPath =
- tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
+ _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages;
+ _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles";
}
public IEnumerable GetAll()
@@ -192,17 +191,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
public string ExportPackage(PackageDefinition definition)
{
-
// Ensure it's valid
ValidatePackage(definition);
// Create a folder for building this package
- var temporaryPath =
- _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid());
- if (Directory.Exists(temporaryPath) == false)
- {
- Directory.CreateDirectory(temporaryPath);
- }
+ var temporaryPath = _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString()));
+ Directory.CreateDirectory(temporaryPath);
try
{
@@ -218,8 +212,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
PackageTemplates(definition, root);
PackageStylesheets(definition, root);
PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem);
- PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View",
- _fileSystems.PartialViewsFileSystem);
+ PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem);
PackageMacros(definition, root);
PackageDictionaryItems(definition, root);
PackageLanguages(definition, root);
@@ -265,27 +258,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
}
- var directoryName =
- _hostingEnvironment.MapPathWebRoot(
- Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_')));
-
- if (Directory.Exists(directoryName) == false)
- {
- Directory.CreateDirectory(directoryName);
- }
+ var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_')));
+ Directory.CreateDirectory(directoryName);
var finalPackagePath = Path.Combine(directoryName, fileName);
- if (File.Exists(finalPackagePath))
+ // Clean existing files
+ foreach (var packagePath in new[]
{
- File.Delete(finalPackagePath);
- }
-
- if (File.Exists(finalPackagePath.Replace("zip", "xml")))
- {
- File.Delete(finalPackagePath.Replace("zip", "xml"));
+ definition.PackagePath,
+ finalPackagePath
+ })
+ {
+ if (File.Exists(packagePath))
+ {
+ File.Delete(packagePath);
+ }
}
+ // Move to final package path
File.Move(tempPackagePath, finalPackagePath);
definition.PackagePath = finalPackagePath;
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs
index 535895e8ed..21638027ea 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs
@@ -18,14 +18,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
{
- internal class MacroRepository : EntityRepositoryBase, IMacroRepository
+ internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository
{
private readonly IShortStringHelper _shortStringHelper;
+ private readonly IRepositoryCachePolicy _macroByAliasCachePolicy;
public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger)
{
_shortStringHelper = shortStringHelper;
+ _macroByAliasCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions);
}
protected override IMacro PerformGet(int id)
@@ -68,6 +70,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
return Get(id) != null;
}
+ public IMacro GetByAlias(string alias)
+ {
+ return _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias);
+ }
+
+ public IEnumerable GetAllByAlias(string[] aliases)
+ {
+ if (aliases.Any() is false)
+ {
+ return base.GetMany();
+ }
+
+ return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias);
+ }
+
+ private IMacro PerformGetByAlias(string alias)
+ {
+ var query = Query().Where(x => x.Alias.Equals(alias));
+ return PerformGetByQuery(query).FirstOrDefault();
+ }
+
+ private IEnumerable PerformGetAllByAlias(params string[] aliases)
+ {
+ if (aliases.Any() is false)
+ {
+ return base.GetMany();
+ }
+
+ var query = Query().Where(x => aliases.Contains(x.Alias));
+ return PerformGetByQuery(query);
+ }
+
protected override IEnumerable PerformGetAll(params int[] ids)
{
return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds();
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
index 6ab29aa47e..5e9a8413b4 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs
@@ -60,14 +60,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
var urlHash = url.GenerateHash();
Sql sql = GetBaseQuery(false)
.Where(x => x.Url == url && x.UrlHash == urlHash &&
- (x.Culture == culture.ToLower() || x.Culture == string.Empty))
+ (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty))
.OrderByDescending(x => x.CreateDateUtc);
List dtos = Database.Fetch(sql);
RedirectUrlDto dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower());
if (dto == null)
{
- dto = dtos.FirstOrDefault(f => f.Culture == string.Empty);
+ dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture));
}
return dto == null ? null : Map(dto);
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
index 749fc9d77b..7ba20d1db5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
@@ -171,6 +172,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes)
+ {
+ return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes);
+ }
+
+ public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes)
{
// var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }
// we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data
@@ -184,10 +190,20 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
sql.Where(rel => rel.ChildId == childId);
sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId);
+
+ if (relationTypes != null && relationTypes.Any())
+ {
+ sql.WhereIn(rel => rel.RelationType, relationTypes);
+ }
});
}
public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes)
+ {
+ return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes);
+ }
+
+ public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes)
{
// var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }
// we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data
@@ -201,6 +217,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
sql.Where(rel => rel.ParentId == parentId);
sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId);
+
+ if (relationTypes != null && relationTypes.Any())
+ {
+ sql.WhereIn(rel => rel.RelationType, relationTypes);
+ }
});
}
@@ -399,4 +420,40 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
sql.OrderByDescending(orderBy);
}
}
+
+ internal class RelationItemDto
+ {
+ [Column(Name = "nodeId")]
+ public int ChildNodeId { get; set; }
+
+ [Column(Name = "nodeKey")]
+ public Guid ChildNodeKey { get; set; }
+
+ [Column(Name = "nodeName")]
+ public string ChildNodeName { get; set; }
+
+ [Column(Name = "nodeObjectType")]
+ public Guid ChildNodeObjectType { get; set; }
+
+ [Column(Name = "contentTypeIcon")]
+ public string ChildContentTypeIcon { get; set; }
+
+ [Column(Name = "contentTypeAlias")]
+ public string ChildContentTypeAlias { get; set; }
+
+ [Column(Name = "contentTypeName")]
+ public string ChildContentTypeName { get; set; }
+
+ [Column(Name = "relationTypeName")]
+ public string RelationTypeName { get; set; }
+
+ [Column(Name = "relationTypeAlias")]
+ public string RelationTypeAlias { get; set; }
+
+ [Column(Name = "relationTypeIsDependency")]
+ public bool RelationTypeIsDependency { get; set; }
+
+ [Column(Name = "relationTypeIsBidirectional")]
+ public bool RelationTypeIsBidirectional { get; set; }
+ }
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs
new file mode 100644
index 0000000000..0e70d47cbf
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NPoco;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
+{
+ internal class TrackedReferencesRepository : ITrackedReferencesRepository
+ {
+ private readonly IScopeAccessor _scopeAccessor;
+
+ public TrackedReferencesRepository(IScopeAccessor scopeAccessor)
+ {
+ _scopeAccessor = scopeAccessor;
+ }
+
+ ///
+ /// Gets a page of items used in any kind of relation from selected integer ids.
+ ///
+ public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords)
+ {
+ var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct(
+ "[pn].[id] as nodeId",
+ "[pn].[uniqueId] as nodeKey",
+ "[pn].[text] as nodeName",
+ "[pn].[nodeObjectType] as nodeObjectType",
+ "[ct].[icon] as contentTypeIcon",
+ "[ct].[alias] as contentTypeAlias",
+ "[ctn].[text] as contentTypeName",
+ "[umbracoRelationType].[alias] as relationTypeAlias",
+ "[umbracoRelationType].[name] as relationTypeName",
+ "[umbracoRelationType].[isDependency] as relationTypeIsDependency",
+ "[umbracoRelationType].[dual] as relationTypeIsBidirectional")
+ .From("r")
+ .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType")
+ .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType")
+ .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn")
+ .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c")
+ .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct")
+ .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn");
+
+ if (ids.Any())
+ {
+ sql = sql.Where(x => ids.Contains(x.NodeId), "pn");
+ }
+
+ if (filterMustBeIsDependency)
+ {
+ sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType");
+ }
+
+ // Ordering is required for paging
+ sql = sql.OrderBy(x => x.Alias);
+
+ var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql);
+ totalRecords = pagedResult.TotalItems;
+
+ return pagedResult.Items.Select(MapDtoToEntity);
+ }
+
+ ///
+ /// Gets a page of the descending items that have any references, given a parent id.
+ ///
+ public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords)
+ {
+ var syntax = _scopeAccessor.AmbientScope.Database.SqlContext.SqlSyntax;
+
+ // Gets the path of the parent with ",%" added
+ var subsubQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql()
+ .Select(syntax.GetConcat("[node].[path]", "',%'"))
+ .From("node")
+ .Where(x => x.NodeId == parentId, "node");
+
+ // Gets the descendants of the parent node
+ Sql subQuery;
+
+ if (_scopeAccessor.AmbientScope.Database.DatabaseType.IsSqlCe())
+ {
+ // SqlCE does not support nested selects that returns a scalar. So we need to do this in multiple queries
+
+ var pathForLike = _scopeAccessor.AmbientScope.Database.ExecuteScalar(subsubQuery);
+
+ subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql()
+ .Select(x => x.NodeId)
+ .From()
+ .WhereLike(x => x.Path, pathForLike);
+ }
+ else
+ {
+ subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql()
+ .Select(x => x.NodeId)
+ .From()
+ .WhereLike(x => x.Path, subsubQuery);
+ }
+
+ // Get all relations where parent is in the sub query
+ var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct(
+ "[pn].[id] as nodeId",
+ "[pn].[uniqueId] as nodeKey",
+ "[pn].[text] as nodeName",
+ "[pn].[nodeObjectType] as nodeObjectType",
+ "[ct].[icon] as contentTypeIcon",
+ "[ct].[alias] as contentTypeAlias",
+ "[ctn].[text] as contentTypeName",
+ "[umbracoRelationType].[alias] as relationTypeAlias",
+ "[umbracoRelationType].[name] as relationTypeName",
+ "[umbracoRelationType].[isDependency] as relationTypeIsDependency",
+ "[umbracoRelationType].[dual] as relationTypeIsBidirectional")
+ .From("r")
+ .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType")
+ .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType")
+ .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn")
+ .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c")
+ .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct")
+ .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn")
+ .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn");
+
+ if (filterMustBeIsDependency)
+ {
+ sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType");
+ }
+
+ // Ordering is required for paging
+ sql = sql.OrderBy(x => x.Alias);
+
+ var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql);
+ totalRecords = pagedResult.TotalItems;
+
+ return pagedResult.Items.Select(MapDtoToEntity);
+ }
+
+ ///
+ /// Gets a page of items which are in relation with the current item.
+ /// Basically, shows the items which depend on the current item.
+ ///
+ public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords)
+ {
+ var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct(
+ "[cn].[id] as nodeId",
+ "[cn].[uniqueId] as nodeKey",
+ "[cn].[text] as nodeName",
+ "[cn].[nodeObjectType] as nodeObjectType",
+ "[ct].[icon] as contentTypeIcon",
+ "[ct].[alias] as contentTypeAlias",
+ "[ctn].[text] as contentTypeName",
+ "[umbracoRelationType].[alias] as relationTypeAlias",
+ "[umbracoRelationType].[name] as relationTypeName",
+ "[umbracoRelationType].[isDependency] as relationTypeIsDependency",
+ "[umbracoRelationType].[dual] as relationTypeIsBidirectional")
+ .From("r")
+ .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType")
+ .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType")
+ .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn")
+ .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "cn", aliasRight: "c")
+ .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct")
+ .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn")
+ .Where(x => x.NodeId == id, "pn");
+
+ if (filterMustBeIsDependency)
+ {
+ sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType");
+ }
+
+ // Ordering is required for paging
+ sql = sql.OrderBy(x => x.Alias);
+
+ var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql);
+ totalRecords = pagedResult.TotalItems;
+
+ return pagedResult.Items.Select(MapDtoToEntity);
+ }
+
+ private RelationItem MapDtoToEntity(RelationItemDto dto)
+ {
+ return new RelationItem()
+ {
+ NodeId = dto.ChildNodeId,
+ NodeKey = dto.ChildNodeKey,
+ NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType),
+ NodeName = dto.ChildNodeName,
+ RelationTypeName = dto.RelationTypeName,
+ RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional,
+ RelationTypeIsDependency = dto.RelationTypeIsDependency,
+ ContentTypeAlias = dto.ChildContentTypeAlias,
+ ContentTypeIcon = dto.ChildContentTypeIcon,
+ ContentTypeName = dto.ChildContentTypeName,
+ };
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
index f149757919..c3d8be8f50 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
@@ -14,6 +15,8 @@ using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;
+using Umbraco.Cms.Infrastructure.Templates;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -37,6 +40,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly RichTextEditorPastedImages _pastedImages;
private readonly HtmlLocalLinkParser _localLinkParser;
private readonly IImageUrlGenerator _imageUrlGenerator;
+ private readonly IHtmlMacroParameterParser _macroParameterParser;
public GridPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
@@ -45,7 +49,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
RichTextEditorPastedImages pastedImages,
HtmlLocalLinkParser localLinkParser,
IIOHelper ioHelper,
- IImageUrlGenerator imageUrlGenerator)
+ IImageUrlGenerator imageUrlGenerator,
+ IHtmlMacroParameterParser macroParameterParser)
: base(dataValueEditorFactory)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -54,6 +59,20 @@ namespace Umbraco.Cms.Core.PropertyEditors
_pastedImages = pastedImages;
_localLinkParser = localLinkParser;
_imageUrlGenerator = imageUrlGenerator;
+ _macroParameterParser = macroParameterParser;
+ }
+
+ [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")]
+ public GridPropertyEditor(
+ IDataValueEditorFactory dataValueEditorFactory,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ HtmlImageSourceParser imageSourceParser,
+ RichTextEditorPastedImages pastedImages,
+ HtmlLocalLinkParser localLinkParser,
+ IIOHelper ioHelper,
+ IImageUrlGenerator imageUrlGenerator)
+ : this (dataValueEditorFactory, backOfficeSecurityAccessor, imageSourceParser, pastedImages, localLinkParser, ioHelper, imageUrlGenerator, StaticServiceProvider.Instance.GetRequiredService())
+ {
}
public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory();
@@ -74,6 +93,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor;
private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor;
private readonly IImageUrlGenerator _imageUrlGenerator;
+ private readonly IHtmlMacroParameterParser _macroParameterParser;
public GridPropertyValueEditor(
IDataValueEditorFactory dataValueEditorFactory,
@@ -85,7 +105,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
IShortStringHelper shortStringHelper,
IImageUrlGenerator imageUrlGenerator,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper)
+ IIOHelper ioHelper,
+ IHtmlMacroParameterParser macroParameterParser)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -96,6 +117,25 @@ namespace Umbraco.Cms.Core.PropertyEditors
_mediaPickerPropertyValueEditor =
dataValueEditorFactory.Create(attribute);
_imageUrlGenerator = imageUrlGenerator;
+ _macroParameterParser = macroParameterParser;
+ }
+
+ [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")]
+ public GridPropertyValueEditor(
+ IDataValueEditorFactory dataValueEditorFactory,
+ DataEditorAttribute attribute,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ ILocalizedTextService localizedTextService,
+ HtmlImageSourceParser imageSourceParser,
+ RichTextEditorPastedImages pastedImages,
+ IShortStringHelper shortStringHelper,
+ IImageUrlGenerator imageUrlGenerator,
+ IJsonSerializer jsonSerializer,
+ IIOHelper ioHelper)
+ : this (dataValueEditorFactory, attribute, backOfficeSecurityAccessor, localizedTextService,
+ imageSourceParser, pastedImages, shortStringHelper, imageUrlGenerator, jsonSerializer, ioHelper,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
}
///
@@ -120,7 +160,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
var mediaParent = config?.MediaParentId;
var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid;
- var grid = DeserializeGridValue(rawJson, out var rtes, out _);
+ var grid = DeserializeGridValue(rawJson, out var rtes, out _, out _);
var userId = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser?.Id ?? Constants.Security.SuperUserId;
@@ -154,7 +194,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
if (val.IsNullOrWhiteSpace())
return string.Empty;
- var grid = DeserializeGridValue(val, out var rtes, out _);
+ var grid = DeserializeGridValue(val, out var rtes, out _, out _);
//process the rte values
foreach (var rte in rtes.ToList())
@@ -168,7 +208,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
return grid;
}
- private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues)
+ private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues, out IEnumerable macroValues)
{
var grid = JsonConvert.DeserializeObject(rawJson);
@@ -177,6 +217,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
richTextValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "rte");
mediaValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "media");
+ // Find all the macros
+ macroValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "macro");
+
return grid;
}
@@ -192,7 +235,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
if (rawJson.IsNullOrWhiteSpace())
yield break;
- DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues);
+ DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues, out var macroValues);
foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x =>
_richTextPropertyValueEditor.GetReferences(x.Value)))
@@ -201,6 +244,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues)
.SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"])))
yield return umbracoEntityReference;
+
+ foreach (var umbracoEntityReference in _macroParameterParser.FindUmbracoEntityReferencesFromGridControlMacros(macroValues))
+ yield return umbracoEntityReference;
}
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index 1cfbc3449e..18c3fe0902 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -3,8 +3,7 @@
using System;
using System.Collections.Generic;
-using Microsoft.Extensions.Logging;
-using Umbraco.Cms.Core.Hosting;
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
@@ -16,6 +15,8 @@ using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Macros;
+using Umbraco.Cms.Infrastructure.Templates;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -36,12 +37,13 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _localLinkParser;
+ private readonly IHtmlMacroParameterParser _macroParameterParser;
private readonly RichTextEditorPastedImages _pastedImages;
private readonly IIOHelper _ioHelper;
private readonly IImageUrlGenerator _imageUrlGenerator;
///
- /// The constructor will setup the property editor based on the attribute if one is found
+ /// The constructor will setup the property editor based on the attribute if one is found.
///
public RichTextPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
@@ -50,7 +52,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
HtmlLocalLinkParser localLinkParser,
RichTextEditorPastedImages pastedImages,
IIOHelper ioHelper,
- IImageUrlGenerator imageUrlGenerator)
+ IImageUrlGenerator imageUrlGenerator,
+ IHtmlMacroParameterParser macroParameterParser)
: base(dataValueEditorFactory)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -59,6 +62,20 @@ namespace Umbraco.Cms.Core.PropertyEditors
_pastedImages = pastedImages;
_ioHelper = ioHelper;
_imageUrlGenerator = imageUrlGenerator;
+ _macroParameterParser = macroParameterParser;
+ }
+
+ [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")]
+ public RichTextPropertyEditor(
+ IDataValueEditorFactory dataValueEditorFactory,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ HtmlImageSourceParser imageSourceParser,
+ HtmlLocalLinkParser localLinkParser,
+ RichTextEditorPastedImages pastedImages,
+ IIOHelper ioHelper,
+ IImageUrlGenerator imageUrlGenerator)
+ : this (dataValueEditorFactory, backOfficeSecurityAccessor, imageSourceParser, localLinkParser, pastedImages, ioHelper, imageUrlGenerator, StaticServiceProvider.Instance.GetRequiredService())
+ {
}
///
@@ -79,6 +96,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _localLinkParser;
+ private readonly IHtmlMacroParameterParser _macroParameterParser;
private readonly RichTextEditorPastedImages _pastedImages;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly IHtmlSanitizer _htmlSanitizer;
@@ -94,7 +112,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
IImageUrlGenerator imageUrlGenerator,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
- IHtmlSanitizer htmlSanitizer)
+ IHtmlSanitizer htmlSanitizer,
+ IHtmlMacroParameterParser macroParameterParser)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -103,6 +122,26 @@ namespace Umbraco.Cms.Core.PropertyEditors
_pastedImages = pastedImages;
_imageUrlGenerator = imageUrlGenerator;
_htmlSanitizer = htmlSanitizer;
+ _macroParameterParser = macroParameterParser;
+ }
+
+ [Obsolete("Use the constructor which takes an HtmlMacroParameterParser instead")]
+ public RichTextPropertyValueEditor(
+ DataEditorAttribute attribute,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ ILocalizedTextService localizedTextService,
+ IShortStringHelper shortStringHelper,
+ HtmlImageSourceParser imageSourceParser,
+ HtmlLocalLinkParser localLinkParser,
+ RichTextEditorPastedImages pastedImages,
+ IImageUrlGenerator imageUrlGenerator,
+ IJsonSerializer jsonSerializer,
+ IIOHelper ioHelper,
+ IHtmlSanitizer htmlSanitizer)
+ : this(attribute, backOfficeSecurityAccessor, localizedTextService, shortStringHelper, imageSourceParser,
+ localLinkParser, pastedImages, imageUrlGenerator, jsonSerializer, ioHelper, htmlSanitizer,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
}
///
@@ -182,6 +221,10 @@ namespace Umbraco.Cms.Core.PropertyEditors
yield return new UmbracoEntityReference(udi);
//TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs
+ //UPDATE: We are getting the Macros in 'FindUmbracoEntityReferencesFromEmbeddedMacros' - perhaps we just return the macro Udis here too or do they need their own relationAlias?
+
+ foreach (var umbracoEntityReference in _macroParameterParser.FindUmbracoEntityReferencesFromEmbeddedMacros(asString))
+ yield return umbracoEntityReference;
}
}
diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs
index 7f99b32b02..2ef2034d3d 100644
--- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs
+++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs
@@ -99,25 +99,32 @@ namespace Umbraco.Cms.Core.Routing
{
return;
}
- var contentCache = publishedSnapshot.Content;
- var entityContent = contentCache?.GetById(entity.Id);
- if (entityContent == null)
+
+ IPublishedContentCache contentCache = publishedSnapshot.Content;
+ IPublishedContent entityContent = contentCache?.GetById(entity.Id);
+ if (entityContent is null)
+ {
return;
+ }
// get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray()
?? new[] { (string)null };
- foreach (var x in entityContent.DescendantsOrSelf(_variationContextAccessor))
+
+ foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor))
{
// if this entity defines specific cultures, use those instead of the default ones
- var cultures = x.Cultures.Any() ? x.Cultures.Keys : defaultCultures;
+ IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures;
foreach (var culture in cultures)
{
- var route = contentCache.GetRouteById(x.Id, culture);
+ var route = contentCache.GetRouteById(publishedContent.Id, culture);
if (IsNotRoute(route))
+ {
continue;
- oldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route);
+ }
+
+ oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] = new ContentKeyAndOldRoute(publishedContent.Key, route);
}
}
}
@@ -135,13 +142,16 @@ namespace Umbraco.Cms.Core.Routing
{
_logger.LogWarning("Could not track redirects because there is no current published snapshot available.");
return;
- }
+ }
- foreach (var oldRoute in oldRoutes)
+ foreach (KeyValuePair oldRoute in oldRoutes)
{
var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture);
if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute)
+ {
continue;
+ }
+
_redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture);
}
}
diff --git a/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs b/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs
new file mode 100644
index 0000000000..11944e776c
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Security.Cryptography;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Runtime
+{
+
+ internal class DefaultMainDomKeyGenerator : IMainDomKeyGenerator
+ {
+ private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IOptionsMonitor _globalSettings;
+
+ public DefaultMainDomKeyGenerator(IHostingEnvironment hostingEnvironment, IOptionsMonitor globalSettings)
+ {
+ _hostingEnvironment = hostingEnvironment;
+ _globalSettings = globalSettings;
+ }
+
+ public string GenerateKey()
+ {
+ var machineName = Environment.MachineName;
+ var mainDomId = MainDom.GetMainDomId(_hostingEnvironment);
+ var discriminator = _globalSettings.CurrentValue.MainDomKeyDiscriminator;
+
+ var rawKey = $"{machineName}{mainDomId}{discriminator}";
+
+ return rawKey.GenerateHash();
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
new file mode 100644
index 0000000000..c4cbcef588
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.Runtime;
+
+namespace Umbraco.Cms.Infrastructure.Runtime
+{
+ internal class FileSystemMainDomLock : IMainDomLock
+ {
+ private readonly ILogger _logger;
+ private readonly IOptionsMonitor _globalSettings;
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+ private readonly string _lockFilePath;
+ private readonly string _releaseSignalFilePath;
+
+ private FileStream _lockFileStream;
+ private Task _listenForReleaseSignalFileTask;
+
+ public FileSystemMainDomLock(
+ ILogger logger,
+ IMainDomKeyGenerator mainDomKeyGenerator,
+ IHostingEnvironment hostingEnvironment,
+ IOptionsMonitor globalSettings)
+ {
+ _logger = logger;
+ _globalSettings = globalSettings;
+
+ var lockFileName = $"MainDom_{mainDomKeyGenerator.GenerateKey()}.lock";
+ _lockFilePath = Path.Combine(hostingEnvironment.LocalTempPath, lockFileName);
+ _releaseSignalFilePath = $"{_lockFilePath}_release";
+ }
+
+ public Task AcquireLockAsync(int millisecondsTimeout)
+ {
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ do
+ {
+ try
+ {
+ _logger.LogDebug("Attempting to obtain MainDom lock file handle {lockFilePath}", _lockFilePath);
+ _lockFileStream = File.Open(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ DeleteLockReleaseSignalFile();
+ return Task.FromResult(true);
+ }
+ catch (IOException)
+ {
+ _logger.LogDebug("Couldn't obtain MainDom lock file handle, signalling for release of {lockFilePath}", _lockFilePath);
+ CreateLockReleaseSignalFile();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected exception attempting to obtain MainDom lock file handle {lockFilePath}, giving up", _lockFilePath);
+ _lockFileStream?.Close();
+ return Task.FromResult(false);
+ }
+ }
+ while (stopwatch.ElapsedMilliseconds < millisecondsTimeout);
+
+ return Task.FromResult(false);
+ }
+
+ public void CreateLockReleaseSignalFile() =>
+ File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete)
+ .Close();
+
+ public void DeleteLockReleaseSignalFile() =>
+ File.Delete(_releaseSignalFilePath);
+
+ // Create a long running task to poll to check if anyone has created a lock release file.
+ public Task ListenAsync()
+ {
+ if (_listenForReleaseSignalFileTask != null)
+ {
+ return _listenForReleaseSignalFileTask;
+ }
+
+ _listenForReleaseSignalFileTask = Task.Factory.StartNew(
+ ListeningLoop,
+ _cancellationTokenSource.Token,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+
+ return _listenForReleaseSignalFileTask;
+ }
+
+ private void ListeningLoop()
+ {
+ while (true)
+ {
+ if (_cancellationTokenSource.IsCancellationRequested)
+ {
+ _logger.LogDebug("ListenAsync Task canceled, exiting loop");
+ return;
+ }
+
+ if (File.Exists(_releaseSignalFilePath))
+ {
+ _logger.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath);
+ _lockFileStream?.Close();
+ _lockFileStream = null;
+ break;
+ }
+
+ Thread.Sleep(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval);
+ }
+ }
+
+ public void Dispose()
+ {
+ _lockFileStream?.Close();
+ _lockFileStream = null;
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs
index 8d1c74b619..8a6698b92a 100644
--- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs
+++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NPoco;
@@ -18,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection;
@@ -30,7 +32,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private const string UpdatedSuffix = "_updated";
private readonly ILogger _logger;
private readonly IOptions _globalSettings;
- private readonly IHostingEnvironment _hostingEnvironment;
private readonly IUmbracoDatabase _db;
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private SqlServerSyntaxProvider _sqlServerSyntax;
@@ -41,6 +42,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime
private bool _hasTable = false;
private bool _acquireWhenTablesNotAvailable = false;
+ // Note: Ignoring the two version notice rule as this class should probably be internal.
+ // We don't expect anyone downstream to be instantiating a SqlMainDomLock, only resolving IMainDomLock
+ [Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
public SqlMainDomLock(
ILogger logger,
ILoggerFactory loggerFactory,
@@ -51,25 +55,20 @@ namespace Umbraco.Cms.Infrastructure.Runtime
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
NPocoMapperCollection npocoMappers,
string connectionStringName)
- {
- // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
- _lockId = Guid.NewGuid().ToString();
- _logger = logger;
- _globalSettings = globalSettings;
- _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings);
- _hostingEnvironment = hostingEnvironment;
- _dbFactory = new UmbracoDatabaseFactory(
- loggerFactory.CreateLogger(),
+ : this(
loggerFactory,
- _globalSettings,
- new MapperCollection(() => Enumerable.Empty()),
+ globalSettings,
+ connectionStrings,
dbProviderFactoryCreator,
+ StaticServiceProvider.Instance.GetRequiredService(),
databaseSchemaCreatorFactory,
- npocoMappers,
- connectionStringName);
- MainDomKey = MainDomKeyPrefix + "-" + (Environment.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash();
+ npocoMappers)
+ {
}
+ // Note: Ignoring the two version notice rule as this class should probably be internal.
+ // We don't expect anyone downstream to be instantiating a SqlMainDomLock, only resolving IMainDomLock
+ [Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
public SqlMainDomLock(
ILogger logger,
ILoggerFactory loggerFactory,
@@ -80,18 +79,42 @@ namespace Umbraco.Cms.Infrastructure.Runtime
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
NPocoMapperCollection npocoMappers)
: this(
- logger,
loggerFactory,
globalSettings,
connectionStrings,
dbProviderFactoryCreator,
- hostingEnvironment,
+ StaticServiceProvider.Instance.GetRequiredService(),
databaseSchemaCreatorFactory,
- npocoMappers,
- connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString
- )
+ npocoMappers)
{
+ }
+ public SqlMainDomLock(
+ ILoggerFactory loggerFactory,
+ IOptions globalSettings,
+ IOptionsMonitor connectionStrings,
+ IDbProviderFactoryCreator dbProviderFactoryCreator,
+ IMainDomKeyGenerator mainDomKeyGenerator,
+ DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
+ NPocoMapperCollection npocoMappers)
+ {
+ // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
+ _lockId = Guid.NewGuid().ToString();
+ _logger = loggerFactory.CreateLogger();
+ _globalSettings = globalSettings;
+ _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings);
+
+ _dbFactory = new UmbracoDatabaseFactory(
+ loggerFactory.CreateLogger(),
+ loggerFactory,
+ _globalSettings,
+ new MapperCollection(() => Enumerable.Empty()),
+ dbProviderFactoryCreator,
+ databaseSchemaCreatorFactory,
+ npocoMappers,
+ connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString);
+
+ MainDomKey = MainDomKeyPrefix + "-" + mainDomKeyGenerator.GenerateKey();
}
public async Task AcquireLockAsync(int millisecondsTimeout)
@@ -213,7 +236,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime
{
// poll every couple of seconds
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
- Thread.Sleep(2000);
+ Thread.Sleep(_globalSettings.Value.MainDomReleaseSignalPollingInterval);
if (!_dbFactory.Configured)
{
diff --git a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
index de5b6206fc..9e11916223 100644
--- a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
+++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
@@ -1,5 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+
using System.Linq;
using System.Security.Claims;
using Umbraco.Cms.Core;
@@ -11,7 +12,8 @@ namespace Umbraco.Extensions
{
// Ignore these Claims when merging, these claims are dynamically added whenever the ticket
// is re-issued and we don't want to merge old values of these.
- private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType };
+ // We do however want to merge these when the SecurityStampValidator refreshes the principal since it's still the same login session
+ private static readonly string[] s_ignoredClaims = { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType };
public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source)
{
diff --git a/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs b/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs
index e470bf0a6c..02aef30217 100644
--- a/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs
+++ b/src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs
@@ -71,6 +71,7 @@ namespace Umbraco.Cms.Core.Security
return result;
}
}
+
// We need to check for clear text passwords from members as the first thing. This was possible in v8 :(
else if (IsSuccessfulLegacyPassword(hashedPassword, providedPassword))
{
@@ -138,7 +139,7 @@ namespace Umbraco.Cms.Core.Security
}
var result = LegacyPasswordSecurity.VerifyPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, providedPassword, hashedPassword);
- return result || LegacyPasswordSecurity.VerifyPassword(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName, providedPassword, hashedPassword);
+ return result || LegacyPasswordSecurity.VerifyLegacyHashedPassword(providedPassword, hashedPassword);
}
private static string DecryptLegacyPassword(string encryptedPassword, string algorithmName, string decryptionKey)
diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
index 420d66b0b4..345a404fcf 100644
--- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
+++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
@@ -181,6 +181,7 @@ namespace Umbraco.Cms.Core.Security
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
+ var isTokensPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.LoginTokens));
MemberDataChangeType memberChangeType = UpdateMemberProperties(found, user);
if (memberChangeType == MemberDataChangeType.FullSave)
@@ -203,6 +204,16 @@ namespace Umbraco.Cms.Core.Security
x.ProviderKey,
x.UserData)));
}
+
+ if (isTokensPropertyDirty)
+ {
+ _externalLoginService.Save(
+ found.Key,
+ user.LoginTokens.Select(x => new ExternalLoginToken(
+ x.LoginProvider,
+ x.Name,
+ x.Value)));
+ }
}
return Task.FromResult(IdentityResult.Success);
@@ -535,6 +546,37 @@ namespace Umbraco.Cms.Core.Security
return found;
}
+ ///
+ /// Overridden to support Umbraco's own data storage requirements
+ ///
+ ///
+ /// The base class's implementation of this calls into FindTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
+ /// tracking ORMs like EFCore.
+ ///
+ ///
+ public override Task SetTokenAsync(MemberIdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
+ if (token == null)
+ {
+ user.LoginTokens.Add(new IdentityUserToken(loginProvider, name, value, user.Id));
+ }
+ else
+ {
+ token.Value = value;
+ }
+
+ return Task.CompletedTask;
+ }
+
private MemberIdentityUser AssignLoginsCallback(MemberIdentityUser user)
{
if (user != null)
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs b/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs
index da08bc8713..2847f13dc4 100644
--- a/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs
+++ b/src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models.Membership;
@@ -10,7 +11,6 @@ namespace Umbraco.Cms.Core.Security
where TUser: UmbracoIdentityUser
{
private readonly IJsonSerializer _jsonSerializer;
- private readonly PasswordHasher _aspnetV2PasswordHasher = new PasswordHasher(new V2PasswordHasherOptions());
public UmbracoPasswordHasher(LegacyPasswordSecurity legacyPasswordSecurity, IJsonSerializer jsonSerializer)
{
@@ -43,57 +43,64 @@ namespace Umbraco.Cms.Core.Security
{
if (user is null)
{
- throw new System.ArgumentNullException(nameof(user));
+ throw new ArgumentNullException(nameof(user));
}
- if (!user.PasswordConfig.IsNullOrWhiteSpace())
+ try
{
- // check if the (legacy) password security supports this hash algorith and if so then use it
- var deserialized = _jsonSerializer.Deserialize(user.PasswordConfig);
- if (LegacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
+ // Best case and most likely scenario, a modern hash supported by ASP.Net identity.
+ PasswordVerificationResult upstreamResult = base.VerifyHashedPassword(user, hashedPassword, providedPassword);
+ if (upstreamResult != PasswordVerificationResult.Failed)
{
- var result = LegacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
-
- //We need to special handle this case, apparently v8 still saves the user algorithm as {"hashAlgorithm":"HMACSHA256"}, when using legacy encoding and hasinging.
- if (result == false)
- {
- result = LegacyPasswordSecurity.VerifyLegacyHashedPassword(providedPassword, hashedPassword);
- }
-
- return result
- ? PasswordVerificationResult.SuccessRehashNeeded
- : PasswordVerificationResult.Failed;
- }
-
- // We will explicitly detect names here
- // The default is PBKDF2.ASPNETCORE.V3:
- // PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
- // The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
- // The PBKDF2.ASPNETCORE.V2 settings are:
- // PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
-
- switch (deserialized.HashAlgorithm)
- {
- case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
- return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
- case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
- var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
- if (legacyResult == PasswordVerificationResult.Success)
- return PasswordVerificationResult.SuccessRehashNeeded;
- return legacyResult;
+ return upstreamResult;
}
}
-
- // else go the default (v3)
- return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
- }
-
- private class V2PasswordHasherOptions : IOptions
- {
- public PasswordHasherOptions Value => new PasswordHasherOptions
+ catch (FormatException)
{
- CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
- };
+ // hash wasn't a valid base64 encoded string, MS concat the salt bytes and hash bytes and base 64 encode both together.
+ // We however historically base 64 encoded the salt bytes and hash bytes separately then concat the strings so we got 2 sets of padding.
+ // both salt bytes and hash bytes lengths were not evenly divisible by 3 hence 2 sets of padding.
+
+ // We could check upfront with TryFromBase64String, but not whilst we target netstandard 2.0
+ // so might as well just deal with the exception.
+ }
+
+ // At this point we either have a legacy password or a bad attempt.
+
+ // Check the supported worst case scenario, a "useLegacyEncoding" password - HMACSHA1 but with password used as key so not unique for users sharing same password
+ // This was the standard for v4.
+ // Do this first because with useLegacyEncoding the algorithm stored in the database is irrelevant.
+ if (LegacyPasswordSecurity.VerifyLegacyHashedPassword(providedPassword, hashedPassword))
+ {
+ return PasswordVerificationResult.SuccessRehashNeeded;
+ }
+
+ // For users we expect to know the historic algorithm.
+ // NOTE: MemberPasswordHasher subclasses this class to deal with the fact that PasswordConfig wasn't stored.
+ if (user.PasswordConfig.IsNullOrWhiteSpace())
+ {
+ return PasswordVerificationResult.Failed;
+ }
+
+ PersistedPasswordSettings deserialized;
+ try
+ {
+ deserialized = _jsonSerializer.Deserialize(user.PasswordConfig);
+ }
+ catch
+ {
+ return PasswordVerificationResult.Failed;
+ }
+
+ if (!LegacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
+ {
+ return PasswordVerificationResult.Failed;
+ }
+
+ // Last chance must be HMACSHA256 or SHA1
+ return LegacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword)
+ ? PasswordVerificationResult.SuccessRehashNeeded
+ : PasswordVerificationResult.Failed;
}
}
}
diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs
index a037cd1095..b4af98ad0a 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs
@@ -247,6 +247,13 @@ namespace Umbraco.Cms.Core.Services.Implement
///
private bool TryDeserializeInstructions(CacheInstruction instruction, out JArray jsonInstructions)
{
+ if (instruction.Instructions is null)
+ {
+ _logger.LogError("Failed to deserialize instructions ({DtoId}: 'null').", instruction.Id);
+ jsonInstructions = null;
+ return false;
+ }
+
try
{
jsonInstructions = JsonConvert.DeserializeObject(instruction.Instructions);
diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs
index 5dac893cf4..9c393b1308 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextServiceFileSources.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
diff --git a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs
index a79d9fddce..a1d556d805 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs
@@ -13,13 +13,12 @@ namespace Umbraco.Cms.Core.Services.Implement
///
/// Represents the Macro Service, which is an easy access to operations involving
///
- internal class MacroService : RepositoryService, IMacroService
+ internal class MacroService : RepositoryService, IMacroWithAliasService
{
private readonly IMacroRepository _macroRepository;
private readonly IAuditRepository _auditRepository;
- public MacroService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
- IMacroRepository macroRepository, IAuditRepository auditRepository)
+ public MacroService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMacroRepository macroRepository, IAuditRepository auditRepository)
: base(provider, loggerFactory, eventMessagesFactory)
{
_macroRepository = macroRepository;
@@ -33,10 +32,14 @@ namespace Umbraco.Cms.Core.Services.Implement
/// An object
public IMacro GetByAlias(string alias)
{
+ if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
+ {
+ return GetAll().FirstOrDefault(x => x.Alias == alias);
+ }
+
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
- var q = Query().Where(x => x.Alias == alias);
- return _macroRepository.Get(q).FirstOrDefault();
+ return macroWithAliasRepository.GetByAlias(alias);
}
}
@@ -61,6 +64,20 @@ namespace Umbraco.Cms.Core.Services.Implement
}
}
+ public IEnumerable GetAll(params string[] aliases)
+ {
+ if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository)
+ {
+ var hashset = new HashSet(aliases);
+ return GetAll().Where(x => hashset.Contains(x.Alias));
+ }
+
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ {
+ return macroWithAliasRepository.GetAllByAlias(aliases);
+ }
+ }
+
public IMacro GetById(int id)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
diff --git a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs
index 7a5d10c222..d52411a9c8 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
diff --git a/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs
new file mode 100644
index 0000000000..ec22e1095c
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs
@@ -0,0 +1,59 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Services.Implement
+{
+ public class TrackedReferencesService : ITrackedReferencesService
+ {
+ private readonly ITrackedReferencesRepository _trackedReferencesRepository;
+ private readonly IScopeProvider _scopeProvider;
+ private readonly IEntityService _entityService;
+
+ public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, IScopeProvider scopeProvider, IEntityService entityService)
+ {
+ _trackedReferencesRepository = trackedReferencesRepository;
+ _scopeProvider = scopeProvider;
+ _entityService = entityService;
+ }
+
+ ///
+ /// Gets a paged result of items which are in relation with the current item.
+ /// Basically, shows the items which depend on the current item.
+ ///
+ public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+ var items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems);
+
+ return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items };
+ }
+
+ ///
+ /// Gets a paged result of items used in any kind of relation from selected integer ids.
+ ///
+ public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+ var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems);
+
+ return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items };
+ }
+
+ ///
+ /// Gets a paged result of the descending items that have any references, given a parent id.
+ ///
+ public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+
+ var items = _trackedReferencesRepository.GetPagedDescendantsInReferences(
+ parentId,
+ pageIndex,
+ pageSize,
+ filterMustBeIsDependency,
+ out var totalItems);
+ return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items };
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
index 10556b7fe6..5f11d1578a 100644
--- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
+++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Sync
///
/// An that works by storing messages in the database.
///
- public abstract class DatabaseServerMessenger : ServerMessengerBase
+ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable
{
/*
* this messenger writes ALL instructions to the database,
@@ -39,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Sync
private DateTime _lastPruned;
private readonly Lazy _initialized;
private bool _syncing;
+ private bool _disposedValue;
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly CancellationToken _cancellationToken;
@@ -280,6 +281,28 @@ namespace Umbraco.Cms.Infrastructure.Sync
}
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _syncIdle?.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ ///
+ /// Dispose
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
#endregion
}
}
diff --git a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs
new file mode 100644
index 0000000000..6323139137
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Infrastructure.Macros;
+
+namespace Umbraco.Cms.Infrastructure.Templates
+{
+ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
+ {
+ private readonly IMacroService _macroService;
+ private readonly ILogger _logger;
+ private readonly ParameterEditorCollection _parameterEditors;
+
+ public HtmlMacroParameterParser(IMacroService macroService, ILogger logger, ParameterEditorCollection parameterEditors)
+ {
+ _macroService = macroService;
+ _logger = logger;
+ _parameterEditors = parameterEditors;
+ }
+
+ ///
+ /// Parses out media UDIs from an HTML string based on embedded macro parameter values.
+ ///
+ /// HTML string
+ ///
+ public IEnumerable FindUmbracoEntityReferencesFromEmbeddedMacros(string text)
+ {
+ // There may be more than one macro with the same alias on the page so using a tuple
+ var foundMacros = new List>>();
+
+ // This legacy ParseMacros() already finds the macros within a Rich Text Editor using regexes
+ // It seems to lowercase the macro parameter alias - so making the dictionary case insensitive
+ MacroTagParser.ParseMacros(text, textblock => { }, (macroAlias, macroAttributes) => foundMacros.Add(new Tuple>(macroAlias, new Dictionary(macroAttributes, StringComparer.OrdinalIgnoreCase))));
+ foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
+ {
+ yield return umbracoEntityReference;
+ }
+ }
+
+ ///
+ /// Parses out media UDIs from Macro Grid Control parameters.
+ ///
+ ///
+ ///
+ public IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable macroGridControls)
+ {
+ var foundMacros = new List>>();
+
+ foreach (var macroGridControl in macroGridControls)
+ {
+ // Deserialise JSON of Macro Grid Control to a class
+ var gridMacro = macroGridControl.Value.ToObject();
+ // Collect any macro parameters that contain the media udi format
+ if (gridMacro is not null && gridMacro.MacroParameters is not null && gridMacro.MacroParameters.Any())
+ {
+ foundMacros.Add(new Tuple>(gridMacro.MacroAlias, gridMacro.MacroParameters));
+ }
+ }
+
+ foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
+ {
+ yield return umbracoEntityReference;
+ }
+ }
+
+ private IEnumerable GetUmbracoEntityReferencesFromMacros(List>> macros)
+ {
+
+ if (_macroService is not IMacroWithAliasService macroWithAliasService)
+ {
+ yield break;
+ }
+
+ var uniqueMacroAliases = macros.Select(f => f.Item1).Distinct();
+ // TODO: Tracking Macro references
+ // Here we are finding the used macros' Udis (there should be a Related Macro relation type - but Relations don't accept 'Macro' as an option)
+ var foundMacroUmbracoEntityReferences = new List();
+ // Get all the macro configs in one hit for these unique macro aliases - this is now cached with a custom cache policy
+ var macroConfigs = macroWithAliasService.GetAll(uniqueMacroAliases.ToArray());
+
+ foreach (var macro in macros)
+ {
+ var macroConfig = macroConfigs.FirstOrDefault(f => f.Alias == macro.Item1);
+ if (macroConfig is null)
+ {
+ continue;
+ }
+ foundMacroUmbracoEntityReferences.Add(new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
+ // Only do this if the macros actually have parameters
+ if (macroConfig.Properties is not null && macroConfig.Properties.Keys.Any(f => f != "macroAlias"))
+ {
+ foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(macro.Item2, macroConfig, _parameterEditors))
+ {
+ yield return umbracoEntityReference;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Finds media UDIs in Macro Parameter Values by calling the GetReference method for all the Macro Parameter Editors for a particular macro.
+ ///
+ /// The parameters for the macro a dictionary of key/value strings
+ /// The macro configuration for this particular macro - contains the types of editors used for each parameter
+ /// A list of all the registered parameter editors used in the Umbraco implmentation - to look up the corresponding property editor for a macro parameter
+ ///
+ private IEnumerable GetUmbracoEntityReferencesFromMacroParameters(Dictionary macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
+ {
+ var foundUmbracoEntityReferences = new List();
+ foreach (var parameter in macroConfig.Properties)
+ {
+ if (macroParameters.TryGetValue(parameter.Alias, out string parameterValue))
+ {
+ var parameterEditorAlias = parameter.EditorAlias;
+ // Lookup propertyEditor from the registered ParameterEditors with the implmementation to avoid looking up for each parameter
+ var parameterEditor = parameterEditors.FirstOrDefault(f => string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
+ if (parameterEditor is not null)
+ {
+ // Get the ParameterValueEditor for this PropertyEditor (where the GetReferences method is implemented) - cast as IDataValueReference to determine if 'it is' implemented for the editor
+ if (parameterEditor.GetValueEditor() is IDataValueReference parameterValueEditor)
+ {
+ foreach (var entityReference in parameterValueEditor.GetReferences(parameterValue))
+ {
+ foundUmbracoEntityReferences.Add(entityReference);
+ }
+ }
+ else
+ {
+ _logger.LogInformation("{0} doesn't have a ValueEditor that implements IDataValueReference", parameterEditor.Alias);
+ }
+ }
+ }
+ }
+
+ return foundUmbracoEntityReferences;
+ }
+
+ // Poco class to deserialise the Json for a Macro Control
+ private class GridMacro
+ {
+ [JsonProperty("macroAlias")]
+ public string MacroAlias { get; set; }
+
+ [JsonProperty("macroParamsDictionary")]
+ public Dictionary MacroParameters { get; set; }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs
new file mode 100644
index 0000000000..6e484cc30a
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Editors;
+
+namespace Umbraco.Cms.Infrastructure.Templates
+{
+ ///
+ /// Provides methods to parse referenced entities as Macro parameters.
+ ///
+ public interface IHtmlMacroParameterParser
+ {
+ ///
+ /// Parses out media UDIs from an HTML string based on embedded macro parameter values.
+ ///
+ /// HTML string
+ ///
+ IEnumerable FindUmbracoEntityReferencesFromEmbeddedMacros(string text);
+
+ ///
+ /// Parses out media UDIs from Macro Grid Control parameters.
+ ///
+ ///
+ ///
+ IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable macroGridControls);
+ }
+}
diff --git a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs b/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs
index 3d8f14afd3..0ec6f0b7cb 100644
--- a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs
+++ b/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs
@@ -13,9 +13,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string LangId(string culture)
- {
- return culture != null ? ("-L:" + culture) : string.Empty;
- }
+ => string.IsNullOrEmpty(culture) ? string.Empty : ("-L:" + culture);
public static string PublishedContentChildren(Guid contentUid, bool previewing)
{
diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
index 240e6c8861..98fc4a3ffe 100644
--- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
+++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs
@@ -608,7 +608,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
throw new ArgumentException("Kit content cannot have children.", nameof(kit));
// ReSharper restore LocalizableElement
- _logger.LogDebug("Set content ID: {KitNodeId}", kit.Node.Id);
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug("Set content ID: {KitNodeId}", kit.Node.Id);
+ }
// get existing
_contentNodes.TryGetValue(kit.Node.Id, out var link);
@@ -727,7 +730,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
previousNode = null; // there is no previous sibling
}
- _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId);
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId);
+ }
+
SetValueLocked(_contentNodes, thisNode.Id, thisNode);
// if we are initializing from the database source ensure the local db is updated
@@ -784,7 +791,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
ok = false;
continue; // skip that one
}
- _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId);
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId);
+ }
+
SetValueLocked(_contentNodes, kit.Node.Id, kit.Node);
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
@@ -873,7 +885,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
if (link?.Value == null) return false;
var content = link.Value;
- _logger.LogDebug("Clear content ID: {ContentId}", content.Id);
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug("Clear content ID: {ContentId}", content.Id);
+ }
// clear the entire branch
ClearBranchLocked(content);
diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
index 0d9d8b903d..b74c4ea7b0 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
@@ -100,7 +100,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var keepOnlyKeys = new Dictionary
{
{"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "previewHubUrl", "iconApiBaseUrl"}},
- {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo"}},
+ {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo", "disableDeleteWhenReferenced", "disableUnpublishWhenReferenced"}},
{"application", new[] {"applicationPath", "cacheBuster"}},
{"isDebuggingEnabled", new string[] { }},
{"features", new [] {"disabledFeatures"}}
@@ -378,6 +378,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
"previewHubUrl", _previewRoutes.GetPreviewHubRoute()
},
+ {
+ "trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetPagedReferences(0, 1, 1, false))
+ }
}
},
{
@@ -409,6 +413,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{"loginBackgroundImage", _contentSettings.LoginBackgroundImage},
{"loginLogoImage", _contentSettings.LoginLogoImage },
{"hideBackofficeLogo", _contentSettings.HideBackOfficeLogo },
+ {"disableDeleteWhenReferenced", _contentSettings.DisableDeleteWhenReferenced },
+ {"disableUnpublishWhenReferenced", _contentSettings.DisableUnpublishWhenReferenced },
{"showUserInvite", _emailSender.CanSendRequiredEmail()},
{"canSendRequiredEmail", _emailSender.CanSendRequiredEmail()},
{"showAllowSegmentationForDocumentTypes", false},
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index 37d4889b4d..451cb17af0 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -820,11 +820,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
contentItem.Variants.Where(x => x.Save).Select(x => x.Culture).ToArray(),
defaultCulture);
+ //get the updated model
+ bool isBlueprint = contentItem.PersistedContent.Blueprint;
+
+ var contentSavedHeader = isBlueprint ? "editBlueprintSavedHeader" : "editContentSavedHeader";
+ var contentSavedText = isBlueprint ? "editBlueprintSavedText" : "editContentSavedText";
+
switch (contentItem.Action)
{
case ContentSaveAction.Save:
case ContentSaveAction.SaveNew:
- SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedText", "editVariantSavedText", cultureForInvariantErrors, out wasCancelled);
+ SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, contentSavedHeader, contentSavedText, "editVariantSavedText", cultureForInvariantErrors, out wasCancelled);
break;
case ContentSaveAction.Schedule:
case ContentSaveAction.ScheduleNew:
@@ -834,7 +840,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
wasCancelled = false;
break;
}
- SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, out wasCancelled);
+
+ SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedHeader", "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, out wasCancelled);
break;
case ContentSaveAction.SendPublish:
@@ -883,7 +890,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
if (!await ValidatePublishBranchPermissionsAsync(contentItem))
{
globalNotifications.AddErrorNotification(
- _localizedTextService.Localize(null,"publish"),
+ _localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "invalidPublishBranchPermissions"));
wasCancelled = false;
break;
@@ -900,7 +907,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
if (!await ValidatePublishBranchPermissionsAsync(contentItem))
{
globalNotifications.AddErrorNotification(
- _localizedTextService.Localize(null,"publish"),
+ _localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "invalidPublishBranchPermissions"));
wasCancelled = false;
break;
@@ -914,7 +921,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
throw new ArgumentOutOfRangeException();
}
- //get the updated model
+ // We have to map do display after we've actually saved the content, otherwise we'll miss information that's set when saving content, such as ID
var display = mapToDisplay(contentItem.PersistedContent);
//merge the tracked success messages with the outgoing model
@@ -1041,7 +1048,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
///
private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount,
Dictionary notifications, SimpleNotificationModel globalNotifications,
- string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors,
+ string savedContentHeaderLocalizationAlias, string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors,
out bool wasCancelled)
{
var saveResult = saveMethod(contentItem.PersistedContent);
@@ -1061,15 +1068,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var variantName = GetVariantName(culture, segment);
AddSuccessNotification(notifications, culture, segment,
- _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"),
- _localizedTextService.Localize(null,variantSavedLocalizationAlias, new[] { variantName }));
+ _localizedTextService.Localize("speechBubbles", savedContentHeaderLocalizationAlias),
+ _localizedTextService.Localize(null, variantSavedLocalizationAlias, new[] { variantName }));
}
}
else if (ModelState.IsValid)
{
globalNotifications.AddSuccessNotification(
- _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"),
- _localizedTextService.Localize("speechBubbles",invariantSavedLocalizationAlias));
+ _localizedTextService.Localize("speechBubbles", savedContentHeaderLocalizationAlias),
+ _localizedTextService.Localize("speechBubbles", invariantSavedLocalizationAlias));
}
}
}
@@ -2117,21 +2124,34 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
var variantIndex = 0;
+ var defaultCulture = _allLangs.Value.Values.FirstOrDefault(x => x.IsDefault)?.IsoCode;
- //loop through each variant, set the correct name and property values
+ // loop through each variant, set the correct name and property values
foreach (var variant in contentSave.Variants)
{
- //Don't update anything for this variant if Save is not true
- if (!variant.Save) continue;
+ // Don't update anything for this variant if Save is not true
+ if (!variant.Save)
+ {
+ continue;
+ }
- //Don't update the name if it is empty
+ // Don't update the name if it is empty
if (!variant.Name.IsNullOrWhiteSpace())
{
if (contentSave.PersistedContent.ContentType.VariesByCulture())
{
if (variant.Culture.IsNullOrWhiteSpace())
+ {
throw new InvalidOperationException($"Cannot set culture name without a culture.");
+ }
+
contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture);
+
+ // If the variant culture is the default culture we also want to update the name on the Content itself.
+ if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
+ {
+ contentSave.PersistedContent.Name = variant.Name;
+ }
}
else
{
@@ -2139,7 +2159,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
}
- //This is important! We only want to process invariant properties with the first variant, for any other variant
+ // This is important! We only want to process invariant properties with the first variant, for any other variant
// we need to exclude invariant properties from being processed, otherwise they will be double processed for the
// same value which can cause some problems with things such as file uploads.
var propertyCollection = variantIndex == 0
@@ -2147,10 +2167,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
: new ContentPropertyCollectionDto
{
Properties = variant.PropertyCollectionDto.Properties.Where(
- x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace())
+ x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()),
};
- //for each variant, map the property values
+ // for each variant, map the property values
MapPropertyValuesForPersistence(
contentSave,
propertyCollection,
@@ -2171,6 +2191,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
variantIndex++;
}
+ // Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events.
+ IEnumerable editedCultures = contentSave.PersistedContent.CultureInfos.Values
+ .Where(x => x.IsDirty())
+ .Select(x => x.Culture);
+ contentSave.PersistedContent.SetCultureEdited(editedCultures);
+
// handle template
if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null
{
@@ -2181,10 +2207,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
else // set: update if different
{
- var template = _fileService.GetTemplate(contentSave.TemplateAlias);
- if (template == null)
+ ITemplate template = _fileService.GetTemplate(contentSave.TemplateAlias);
+ if (template is null)
{
- //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias);
+ // ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias);
_logger.LogWarning("No template exists with the specified alias: {TemplateAlias}", contentSave.TemplateAlias);
}
else if (template.Id != contentSave.PersistedContent.TemplateId)
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
index f389641777..b22a7d715c 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs
@@ -224,7 +224,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
// if there's already a default event message, don't add our default one
IEventMessagesFactory messages = EventMessages;
- if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage))
+ if (messages?.GetOrDefault()?.GetAll().Any(x => x.IsDefaultEventMessage) == true)
{
return;
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
index 955081fa73..342686ceb3 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@@ -19,10 +20,12 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Controllers;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
@@ -43,10 +46,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
private readonly IDashboardService _dashboardService;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IShortStringHelper _shortStringHelper;
+ private readonly ISiteIdentifierService _siteIdentifierService;
private readonly ContentDashboardSettings _dashboardSettings;
+
///
/// Initializes a new instance of the with all its dependencies.
///
+ [ActivatorUtilitiesConstructor]
public DashboardController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
AppCaches appCaches,
@@ -54,7 +60,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IDashboardService dashboardService,
IUmbracoVersion umbracoVersion,
IShortStringHelper shortStringHelper,
- IOptions dashboardSettings)
+ IOptions dashboardSettings,
+ ISiteIdentifierService siteIdentifierService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -63,9 +70,32 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_dashboardService = dashboardService;
_umbracoVersion = umbracoVersion;
_shortStringHelper = shortStringHelper;
+ _siteIdentifierService = siteIdentifierService;
_dashboardSettings = dashboardSettings.Value;
}
+
+ [Obsolete("Use the constructor that accepts ISiteIdentifierService")]
+ public DashboardController(
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ AppCaches appCaches,
+ ILogger logger,
+ IDashboardService dashboardService,
+ IUmbracoVersion umbracoVersion,
+ IShortStringHelper shortStringHelper,
+ IOptions dashboardSettings)
+ : this(
+ backOfficeSecurityAccessor,
+ appCaches,
+ logger,
+ dashboardService,
+ umbracoVersion,
+ shortStringHelper,
+ dashboardSettings,
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
//we have just one instance of HttpClient shared for the entire application
private static readonly HttpClient HttpClient = new HttpClient();
@@ -79,6 +109,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var language = user.Language;
var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild();
var isAdmin = user.IsAdmin();
+ _siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid siteIdentifier);
if (!IsAllowedUrl(baseUrl))
{
@@ -90,14 +121,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return JObject.Parse(errorJson);
}
- var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}",
+ var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}&siteid={7}",
baseUrl,
_dashboardSettings.ContentDashboardPath,
section,
allowedSections,
language,
version,
- isAdmin);
+ isAdmin,
+ siteIdentifier);
var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
var content = _appCaches.RuntimeCache.GetCacheItem(key);
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
index 564d0dcdd9..327884689e 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
@@ -1,9 +1,11 @@
using System;
using System.IO;
+using System.Web;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
@@ -53,13 +55,26 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
///
public IActionResult GetResized(string imagePath, int width)
{
- var ext = Path.GetExtension(imagePath);
+ // We have to use HttpUtility to encode the path here, for non-ASCII characters
+ // We cannot use the WebUtility, as we only want to encode the path, and not the entire string
+ var encodedImagePath = HttpUtility.UrlPathEncode(imagePath);
+
+
+ var ext = Path.GetExtension(encodedImagePath);
+
+ // check if imagePath is local to prevent open redirect
+ if (!Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
+ {
+ return Unauthorized();
+ }
// we need to check if it is an image by extension
if (_imageUrlGenerator.IsSupportedImageFormat(ext) == false)
+ {
return NotFound();
+ }
- //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
+ // redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
DateTimeOffset? imageLastModified = null;
try
{
@@ -74,14 +89,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null;
- var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath)
+ var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath)
{
Width = width,
ImageCropMode = ImageCropMode.Max,
CacheBusterValue = rnd
});
-
- return new RedirectResult(imageUrl, false);
+ if (Url.IsLocalUrl(imageUrl))
+ {
+ return new LocalRedirectResult(imageUrl, false);
+ }
+ else
+ {
+ return Unauthorized();
+ }
}
///
diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs
index a4001ce79f..b9cda9fea6 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs
@@ -174,6 +174,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return ValidationProblem(ModelState);
}
+ // Update language
+ CultureInfo cultureAfterChange;
+ try
+ {
+ // language has the CultureName of the previous lang so we get information about new culture.
+ cultureAfterChange = CultureInfo.GetCultureInfo(language.IsoCode);
+ }
+ catch (CultureNotFoundException)
+ {
+ ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode);
+ return ValidationProblem(ModelState);
+ }
+ existingById.CultureName = cultureAfterChange.DisplayName;
existingById.IsDefault = language.IsDefault;
existingById.FallbackLanguageId = language.FallbackLanguageId;
existingById.IsoCode = language.IsoCode;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
index 14a9080586..c4328da2d4 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
@@ -33,12 +33,9 @@ using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence;
-using Umbraco.Cms.Web.BackOffice.ActionResults;
using Umbraco.Cms.Web.BackOffice.Authorization;
-using Umbraco.Cms.Web.BackOffice.Extensions;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.ModelBinders;
-using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
@@ -676,12 +673,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
return new ActionResult(parentIdResult.Result);
}
+
var parentId = parentIdResult.Value;
if (!parentId.HasValue)
{
return NotFound("The passed id doesn't exist");
}
+ var isFolderAllowed = IsFolderCreationAllowedHere(parentId.Value);
+ if (isFolderAllowed == false)
+ {
+ return ValidationProblem(_localizedTextService.Localize("speechBubbles", "folderCreationNotAllowed"));
+ }
+
var f = _mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder);
_mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -722,10 +726,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var tempFiles = new PostedFiles();
-
//in case we pass a path with a folder in it, we will create it and upload media to it.
if (!string.IsNullOrEmpty(path))
{
+ if (!IsFolderCreationAllowedHere(parentId.Value))
+ {
+ AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "folderUploadNotAllowed"));
+ return Ok(tempFiles);
+ }
var folders = path.Split(Constants.CharArrays.ForwardSlash);
@@ -735,7 +743,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IMedia folderMediaItem;
//if uploading directly to media root and not a subfolder
- if (parentId == -1)
+ if (parentId == Constants.System.Root)
{
//look for matching folder
folderMediaItem =
@@ -768,11 +776,50 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_mediaService.Save(folderMediaItem);
}
}
+
//set the media root to the folder id so uploaded files will end there.
parentId = folderMediaItem.Id;
}
}
+ var mediaTypeAlias = string.Empty;
+ var allMediaTypes = _mediaTypeService.GetAll().ToList();
+ var allowedContentTypes = new HashSet();
+
+ if (parentId != Constants.System.Root)
+ {
+ var mediaFolderItem = _mediaService.GetById(parentId.Value);
+ var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == mediaFolderItem.ContentType.Alias);
+
+ if (mediaFolderType != null)
+ {
+ IMediaType mediaTypeItem = null;
+
+ foreach (ContentTypeSort allowedContentType in mediaFolderType.AllowedContentTypes)
+ {
+ IMediaType checkMediaTypeItem = allMediaTypes.FirstOrDefault(x => x.Id == allowedContentType.Id.Value);
+ allowedContentTypes.Add(checkMediaTypeItem);
+
+ var fileProperty = checkMediaTypeItem?.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
+ if (fileProperty != null)
+ {
+ mediaTypeItem = checkMediaTypeItem;
+ }
+ }
+
+ //Only set the permission-based mediaType if we only allow 1 specific file under this parent.
+ if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
+ {
+ mediaTypeAlias = mediaTypeItem.Alias;
+ }
+ }
+ }
+ else
+ {
+ var typesAllowedAtRoot = allMediaTypes.Where(x => x.AllowedAsRoot).ToList();
+ allowedContentTypes.UnionWith(typesAllowedAtRoot);
+ }
+
//get the files
foreach (var formFile in file)
{
@@ -780,71 +827,82 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var safeFileName = fileName.ToSafeFileName(ShortStringHelper);
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
- if (_contentSettings.IsFileAllowedForUpload(ext))
- {
- var mediaType = Constants.Conventions.MediaTypes.File;
-
- if (contentTypeAlias == Constants.Conventions.MediaTypes.AutoSelect)
- {
- var mediaTypes = _mediaTypeService.GetAll();
- // Look up MediaTypes
- foreach (var mediaTypeItem in mediaTypes)
- {
- var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile");
- if (fileProperty != null)
- {
- var dataTypeKey = fileProperty.DataTypeKey;
- var dataType = _dataTypeService.GetDataType(dataTypeKey);
-
- if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig)
- {
- var fileExtensions = fileExtensionsConfig.FileExtensions;
- if (fileExtensions != null)
- {
- if (fileExtensions.Where(x => x.Value == ext).Count() != 0)
- {
- mediaType = mediaTypeItem.Alias;
- break;
- }
- }
- }
- }
- }
-
- // If media type is still File then let's check if it's an image.
- if (mediaType == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext))
- {
- mediaType = Constants.Conventions.MediaTypes.Image;
- }
- }
- else
- {
- mediaType = contentTypeAlias;
- }
-
- var mediaItemName = fileName.ToFriendlyName();
-
- var f = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
-
-
- await using (var stream = formFile.OpenReadStream())
- {
- f.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
- }
-
-
- var saveResult = _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
- if (saveResult == false)
- {
- AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName);
- }
- }
- else
+ if (!_contentSettings.IsFileAllowedForUpload(ext))
{
tempFiles.Notifications.Add(new BackOfficeNotification(
_localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
_localizedTextService.Localize("media", "disallowedFileType"),
NotificationStyle.Warning));
+ continue;
+ }
+
+ if (string.IsNullOrEmpty(mediaTypeAlias))
+ {
+ mediaTypeAlias = Constants.Conventions.MediaTypes.File;
+
+ if (contentTypeAlias == Constants.Conventions.MediaTypes.AutoSelect)
+ {
+ // Look up MediaTypes
+ foreach (var mediaTypeItem in allMediaTypes)
+ {
+ var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
+ if (fileProperty == null)
+ {
+ continue;
+ }
+
+ var dataTypeKey = fileProperty.DataTypeKey;
+ var dataType = _dataTypeService.GetDataType(dataTypeKey);
+
+ if (dataType == null || dataType.Configuration is not IFileExtensionsConfig fileExtensionsConfig)
+ {
+ continue;
+ }
+
+ var fileExtensions = fileExtensionsConfig.FileExtensions;
+ if (fileExtensions == null || fileExtensions.All(x => x.Value != ext))
+ {
+ continue;
+ }
+
+ mediaTypeAlias = mediaTypeItem.Alias;
+ break;
+ }
+
+ // If media type is still File then let's check if it's an image.
+ if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext))
+ {
+ mediaTypeAlias = Constants.Conventions.MediaTypes.Image;
+ }
+ }
+ else
+ {
+ mediaTypeAlias = contentTypeAlias;
+ }
+ }
+
+ if (allowedContentTypes.Any(x => x.Alias == mediaTypeAlias) == false)
+ {
+ tempFiles.Notifications.Add(new BackOfficeNotification(
+ _localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
+ _localizedTextService.Localize("media", "disallowedMediaType", new[] { mediaTypeAlias }),
+ NotificationStyle.Warning));
+ continue;
+ }
+
+ var mediaItemName = fileName.ToFriendlyName();
+
+ var createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
+
+ await using (var stream = formFile.OpenReadStream())
+ {
+ createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
+ }
+
+ var saveResult = _mediaService.Save(createdMediaItem, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
+ if (saveResult == false)
+ {
+ AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName);
}
}
@@ -861,6 +919,29 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return Ok(tempFiles);
}
+ private bool IsFolderCreationAllowedHere(int parentId)
+ {
+ var allMediaTypes = _mediaTypeService.GetAll().ToList();
+ var isFolderAllowed = false;
+ if (parentId == Constants.System.Root)
+ {
+ var typesAllowedAtRoot = allMediaTypes.Where(ct => ct.AllowedAsRoot).ToList();
+ isFolderAllowed = typesAllowedAtRoot.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
+ }
+ else
+ {
+ var parentMediaType = _mediaService.GetById(parentId);
+ var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == parentMediaType.ContentType.Alias);
+ if (mediaFolderType != null)
+ {
+ isFolderAllowed =
+ mediaFolderType.AllowedContentTypes.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
+ }
+ }
+
+ return isFolderAllowed;
+ }
+
private IMedia FindInChildren(int mediaId, string nameToFind, string contentTypeAlias)
{
const int pageSize = 500;
@@ -1001,7 +1082,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return new ActionResult(toMove);
}
-
+ [Obsolete("Please use TrackedReferencesController.GetPagedRelationsForItem() instead. Scheduled for removal in V11.")]
public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100)
{
if (pageNumber <= 0 || pageSize <= 0)
diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs
index d63f6b0eda..197148a2a3 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs
@@ -148,7 +148,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
relationType.Name.ToSafeAlias(_shortStringHelper, true),
relationType.IsBidirectional,
relationType.ParentObjectType,
- relationType.ChildObjectType);
+ relationType.ChildObjectType,
+ relationType.IsDependency);
try
{
diff --git a/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs
new file mode 100644
index 0000000000..aa1a0ee86e
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs
@@ -0,0 +1,76 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Web.BackOffice.ModelBinders;
+using Umbraco.Cms.Web.Common.Attributes;
+using Umbraco.Cms.Web.Common.Authorization;
+
+namespace Umbraco.Cms.Web.BackOffice.Controllers
+{
+ [PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
+ [Authorize(Policy = AuthorizationPolicies.SectionAccessContentOrMedia)]
+ public class TrackedReferencesController : BackOfficeNotificationsController
+ {
+ private readonly ITrackedReferencesService _relationService;
+
+ public TrackedReferencesController(ITrackedReferencesService relationService)
+ {
+ _relationService = relationService;
+ }
+
+ ///
+ /// Gets a page list of tracked references for the current item, so you can see where an item is being used.
+ ///
+ ///
+ /// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
+ /// This is basically finding parents of relations.
+ ///
+ public ActionResult> GetPagedReferences(int id, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = false)
+ {
+ if (pageNumber <= 0 || pageSize <= 0)
+ {
+ return BadRequest("Both pageNumber and pageSize must be greater than zero");
+ }
+
+ return _relationService.GetPagedRelationsForItem(id, pageNumber - 1, pageSize, filterMustBeIsDependency);
+ }
+
+ ///
+ /// Gets a page list of the child nodes of the current item used in any kind of relation.
+ ///
+ ///
+ /// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any kind of relation.
+ /// This is basically finding the descending items which are children in relations.
+ ///
+ public ActionResult> GetPagedDescendantsInReferences(int parentId, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true)
+ {
+ if (pageNumber <= 0 || pageSize <= 0)
+ {
+ return BadRequest("Both pageNumber and pageSize must be greater than zero");
+ }
+
+ return _relationService.GetPagedDescendantsInReferences(parentId, pageNumber - 1, pageSize, filterMustBeIsDependency);
+ }
+
+ ///
+ /// Gets a page list of the items used in any kind of relation from selected integer ids.
+ ///
+ ///
+ /// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view).
+ /// This is basically finding children of relations.
+ ///
+ [HttpGet]
+ [HttpPost]
+ public ActionResult> GetPagedReferencedItems([FromJsonPath] int[] ids, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true)
+ {
+ if (pageNumber <= 0 || pageSize <= 0)
+ {
+ return BadRequest("Both pageNumber and pageSize must be greater than zero");
+ }
+
+ return _relationService.GetPagedItemsWithRelations(ids, pageNumber - 1, pageSize, filterMustBeIsDependency);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs
index e234fa1115..08cfc49a9d 100644
--- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs
+++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs
@@ -129,7 +129,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping
target.AllowedActions = GetActions(source, parent, context);
target.AllowedTemplates = GetAllowedTemplates(source);
- target.ContentApps = _commonMapper.GetContentApps(source);
+ target.ContentApps = _commonMapper.GetContentAppsForEntity(source);
target.ContentTypeId = source.ContentType.Id;
target.ContentTypeKey = source.ContentType.Key;
target.ContentTypeAlias = source.ContentType.Alias;
diff --git a/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs
index 03bca1ee70..9bff86e90b 100644
--- a/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs
+++ b/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -57,7 +57,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping
// Umbraco.Code.MapAll -Properties -Errors -Edited -Updater -Alias -IsContainer
private void Map(IMedia source, MediaItemDisplay target, MapperContext context)
{
- target.ContentApps = _commonMapper.GetContentApps(source);
+ target.ContentApps = _commonMapper.GetContentAppsForEntity(source);
target.ContentType = _commonMapper.GetContentType(source, context);
target.ContentTypeId = source.ContentType.Id;
target.ContentTypeAlias = source.ContentType.Alias;
diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
index f0647b9efb..8da173ce68 100644
--- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
+++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs
@@ -41,7 +41,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping
// Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture
private void Map(IMember source, MemberDisplay target, MapperContext context)
{
- target.ContentApps = _commonMapper.GetContentApps(source);
+ target.ContentApps = _commonMapper.GetContentAppsForEntity(source);
target.ContentType = _commonMapper.GetContentType(source, context);
target.ContentTypeId = source.ContentType.Id;
target.ContentTypeAlias = source.ContentType.Alias;
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs
index d62edcc1f9..8d4e04d2c0 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs
@@ -58,16 +58,15 @@ namespace Umbraco.Cms.Web.BackOffice.Security
// TODO: We could override and throw NotImplementedException for other methods?
// Ensures that the sign in scheme is always the Umbraco back office external type
- private class EnsureBackOfficeScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions
+ internal class EnsureBackOfficeScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions
{
public void PostConfigure(string name, TOptions options)
{
- if (!name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
+ // ensure logic only applies to backoffice authentication schemes
+ if (name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
{
- return;
+ options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
}
-
- options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
}
}
}
diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
index 58a6862300..916bfb17c0 100644
--- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
+++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
@@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Security
@@ -92,7 +93,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
///
public void Configure(CookieAuthenticationOptions options)
{
- options.SlidingExpiration = true;
+ options.SlidingExpiration = false;
options.ExpireTimeSpan = _globalSettings.TimeOut;
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
options.Cookie.Name = _securitySettings.AuthCookieName;
@@ -150,8 +151,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security
// ensure the thread culture is set
backOfficeIdentity.EnsureCulture();
- await EnsureValidSessionId(ctx);
- await securityStampValidator.ValidateAsync(ctx);
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
// add or update a claim to track when the cookie expires, we use this to track time remaining
@@ -163,6 +162,28 @@ namespace Umbraco.Cms.Web.BackOffice.Security
Constants.Security.BackOfficeAuthenticationType,
backOfficeIdentity));
+ await securityStampValidator.ValidateAsync(ctx);
+
+ // This might have been called from GetRemainingTimeoutSeconds, in this case we don't want to ensure valid session
+ // since that in it self will keep the session valid since we renew the lastVerified date.
+ // Similarly don't renew the token
+ if (IsRemainingSecondsRequest(ctx))
+ {
+ return;
+ }
+
+ // This relies on IssuedUtc, so call it before updating it.
+ await EnsureValidSessionId(ctx);
+
+ // We have to manually specify Issued and Expires,
+ // because the SecurityStampValidator refreshes the principal every 30 minutes,
+ // When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
+ // When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
+ // meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
+ // https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
+ ctx.Properties.IssuedUtc = _systemClock.UtcNow;
+ ctx.Properties.ExpiresUtc = _systemClock.UtcNow.Add(_globalSettings.TimeOut);
+ ctx.ShouldRenew = true;
},
OnSigningIn = ctx =>
{
@@ -226,7 +247,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
}
return Task.CompletedTask;
- }
+ },
};
}
@@ -276,5 +297,21 @@ namespace Umbraco.Cms.Web.BackOffice.Security
}
}
}
+
+ private bool IsRemainingSecondsRequest(CookieValidatePrincipalContext context)
+ {
+ var routeValues = context.HttpContext.Request.RouteValues;
+ if (routeValues.TryGetValue("controller", out var controllerName) &&
+ routeValues.TryGetValue("action", out var action))
+ {
+ if (controllerName?.ToString() == ControllerExtensions.GetControllerName()
+ && action?.ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs
index 03451a60fd..e610ca1ee7 100644
--- a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs
+++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs
@@ -13,15 +13,13 @@ namespace Umbraco.Cms.Web.BackOffice.Services
public class ConflictingRouteService : IConflictingRouteService
{
private readonly TypeLoader _typeLoader;
- private readonly IEnumerable _endpointDataSources;
///
/// Initializes a new instance of the class.
///
- public ConflictingRouteService(TypeLoader typeLoader, IEnumerable endpointDataSources)
+ public ConflictingRouteService(TypeLoader typeLoader)
{
_typeLoader = typeLoader;
- _endpointDataSources = endpointDataSources;
}
///
diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
index db714bb675..ee7d480731 100644
--- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
@@ -349,7 +349,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor);
var proxyControllerContext = new ControllerContext(actionContext);
- var controller = (TreeController)_controllerFactory.CreateController(proxyControllerContext);
+ var controller = (TreeControllerBase)_controllerFactory.CreateController(proxyControllerContext);
// TODO: What about other filters? Will they execute?
var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext);
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs
index 08f6d7b400..42a15cccc8 100644
--- a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs
@@ -64,15 +64,34 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
AddTreeController(controllerType);
}
- public void RemoveTreeController() => RemoveTreeController(typeof(T));
+ public void RemoveTree(Tree treeDefinition)
+ {
+ if (treeDefinition == null)
+ throw new ArgumentNullException(nameof(treeDefinition));
+ _trees.Remove(treeDefinition);
+ }
+ public void RemoveTreeController()
+ where T : TreeControllerBase
+ => RemoveTreeController(typeof(T));
+
+ // TODO: Change parameter name to "controllerType" in a major version to make it consistent with AddTreeController method.
public void RemoveTreeController(Type type)
{
- var tree = _trees.FirstOrDefault(it => it.TreeControllerType == type);
+ if (!typeof(TreeControllerBase).IsAssignableFrom(type))
+ throw new ArgumentException($"Type {type} does not inherit from {typeof(TreeControllerBase).FullName}.");
+
+ var tree = _trees.FirstOrDefault(x => x.TreeControllerType == type);
if (tree != null)
{
_trees.Remove(tree);
}
}
+
+ public void RemoveTreeControllers(IEnumerable controllerTypes)
+ {
+ foreach (var controllerType in controllerTypes)
+ RemoveTreeController(controllerType);
+ }
}
}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs
index b6f2948965..20d0e1a305 100644
--- a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs
@@ -47,6 +47,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
/// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end
/// to the back end to be used in the query for model data.
///
+ [Obsolete("See GetTreeNodesAsync")]
protected abstract ActionResult GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings);
///
@@ -55,8 +56,40 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
///
///
///
+ [Obsolete("See GetMenuForNodeAsync")]
protected abstract ActionResult GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings);
+ ///
+ /// The method called to render the contents of the tree structure
+ ///
+ ///
+ ///
+ /// All of the query string parameters passed from jsTree
+ ///
+ ///
+ /// If overriden, GetTreeNodes will not be called
+ /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end
+ /// to the back end to be used in the query for model data.
+ ///
+ protected virtual async Task> GetTreeNodesAsync(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings)
+ {
+ return GetTreeNodes(id, queryStrings);
+ }
+
+ ///
+ /// Returns the menu structure for the node
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If overriden, GetMenuForNode will not be called
+ ///
+ protected virtual async Task> GetMenuForNodeAsync(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings)
+ {
+ return GetMenuForNode(id, queryStrings);
+ }
+
///