diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 56462fcc40..fce15eb487 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -14,6 +14,7 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco + @@ -44,7 +45,7 @@ - + @@ -55,6 +56,6 @@ - + diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 614a816f3f..4aa354eba2 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -14,6 +14,7 @@ Contains the core assemblies needed to run Umbraco Cms en-US umbraco + @@ -43,7 +44,7 @@ - + @@ -58,7 +59,7 @@ - - + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 5cdacca419..b7bfaaff5b 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -14,6 +14,7 @@ Installs Umbraco Cms in your Visual Studio ASP.NET project en-US umbraco + @@ -25,7 +26,7 @@ not want this to happen as the alpha of the next major is, really, the next major already. --> - + diff --git a/build/NuSpecs/tools/Views.Web.config.install.xdt b/build/NuSpecs/tools/Views.Web.config.install.xdt index 4d660301a8..828bb8612f 100644 --- a/build/NuSpecs/tools/Views.Web.config.install.xdt +++ b/build/NuSpecs/tools/Views.Web.config.install.xdt @@ -8,7 +8,7 @@ - + @@ -18,13 +18,13 @@ - + diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 1411cb5c97..0be28f9467 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -18,9 +18,20 @@ if ($project) { # Copy umbraco and umbraco_files from package to project folder $umbracoFolder = Join-Path $projectPath "Umbraco" - New-Item -ItemType Directory -Force -Path $umbracoFolder + New-Item -ItemType Directory -Force -Path $umbracoFolder $umbracoFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco" - robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log + + Write-Host "copying files to $umbracoFolder ..." + # see https://support.microsoft.com/en-us/help/954404/return-codes-that-are-used-by-the-robocopy-utility-in-windows-server-2 + robocopy $umbracoFolderSource $umbracoFolder /is /it /e + if (($lastexitcode -eq 1) -or ($lastexitcode -eq 3) -or ($lastexitcode -eq 5) -or ($lastexitcode -eq 7)) + { + write-host "Copy succeeded!" + } + else + { + write-host "Copy failed with exit code:" $lastexitcode + } $copyWebconfig = $true $destinationWebConfig = Join-Path $projectPath "Web.config" @@ -40,7 +51,11 @@ if ($project) { } } } - Catch { } + Catch + { + Write-Host "An error occurred:" + Write-Host $_ + } } if($copyWebconfig -eq $true) @@ -74,7 +89,9 @@ if ($project) { } Catch { - # Not a big problem if this fails, let it go + # Not a big problem if this fails, let it go + # Write-Host "An error occurred:" + # Write-Host $_ } } diff --git a/build/build.ps1 b/build/build.ps1 index 55b686c98e..e4994d2c4a 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -15,7 +15,7 @@ [Parameter(Mandatory=$false)] [Alias("doc")] [switch] $docfx = $false, - + # keep the build directories, don't clear them [Parameter(Mandatory=$false)] [Alias("c")] @@ -392,13 +392,13 @@ &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Core.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` - -Symbols -SymbolPackageFormat snupkg -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmscore.log" + -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmscore.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Core." } &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.Web.nuspec" ` -Properties BuildTmp="$($this.BuildTemp)" ` -Version "$($this.Version.Semver.ToString())" ` - -Symbols -SymbolPackageFormat snupkg -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsweb.log" + -Verbosity detailed -outputDirectory "$($this.BuildOutput)" > "$($this.BuildTemp)\nupack.cmsweb.log" if (-not $?) { throw "Failed to pack NuGet UmbracoCms.Web." } &$this.BuildEnv.NuGet Pack "$nuspecs\UmbracoCms.nuspec" ` @@ -429,37 +429,37 @@ Write-Host "Prepare Azure Gallery" $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput) }) - + $ubuild.DefineMethod("PrepareCSharpDocs", { Write-Host "Prepare C# Documentation" - + $src = "$($this.SolutionRoot)\src" $tmp = $this.BuildTemp $out = $this.BuildOutput $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - + #restore nuget packages $this.RestoreNuGet() # run DocFx $DocFx = $this.BuildEnv.DocFx - + & $DocFx metadata $DocFxJson & $DocFx build $DocFxJson # zip it & $this.BuildEnv.Zip a -tzip -r "$out\csharp-docs.zip" $DocFxSiteOutput }) - + $ubuild.DefineMethod("PrepareAngularDocs", { Write-Host "Prepare Angular Documentation" - + $src = "$($this.SolutionRoot)\src" $out = $this.BuildOutput - + $this.CompileBelle() "Moving to Umbraco.Web.UI.Client folder" diff --git a/src/Umbraco.Abstractions/EnumerableExtensions.cs b/src/Umbraco.Abstractions/EnumerableExtensions.cs index cee8199a6c..a055cdb08e 100644 --- a/src/Umbraco.Abstractions/EnumerableExtensions.cs +++ b/src/Umbraco.Abstractions/EnumerableExtensions.cs @@ -9,44 +9,7 @@ namespace Umbraco.Core /// public static class EnumerableExtensions { - // based upon the original Zip method - public static IEnumerable Zip(this IEnumerable e1, IEnumerable e2, IEnumerable e3, - Func resultSelector) - { - if (e1 == null) throw new ArgumentNullException("e1"); - if (e2 == null) throw new ArgumentNullException("e2"); - if (e3 == null) throw new ArgumentNullException("e3"); - if (resultSelector == null) throw new ArgumentNullException("resultSelector"); - return ZipIterator(e1, e2, e3, resultSelector); - } - - private static IEnumerable ZipIterator(IEnumerable ie1, IEnumerable ie2, IEnumerable ie3, - Func resultSelector) - { - var e1 = ie1.GetEnumerator(); - try - { - var e2 = ie2.GetEnumerator(); - var e3 = ie3.GetEnumerator(); - try - { - while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext()) - yield return resultSelector(e1.Current, e2.Current, e3.Current); - } - finally - { - if (e2 != null) - e2.Dispose(); - if (e3 != null) - e3.Dispose(); - } - } - finally - { - if (e1 != null) - e1.Dispose(); - } - } + public static bool IsCollectionEmpty(this IReadOnlyCollection list) => list == null || list.Count == 0; internal static bool HasDuplicates(this IEnumerable items, bool includeNull) { diff --git a/src/Umbraco.Abstractions/Models/Entities/EntityBase.cs b/src/Umbraco.Abstractions/Models/Entities/EntityBase.cs index 967bf9adfc..3d312eea4f 100644 --- a/src/Umbraco.Abstractions/Models/Entities/EntityBase.cs +++ b/src/Umbraco.Abstractions/Models/Entities/EntityBase.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Models.Entities /// /// Updates the entity when it is being saved for the first time. /// - public virtual void AddingEntity() + internal virtual void AddingEntity() { var now = DateTime.Now; @@ -99,7 +99,7 @@ namespace Umbraco.Core.Models.Entities /// /// Updates the entity when it is being saved. /// - public virtual void UpdatingEntity() + internal virtual void UpdatingEntity() { var now = DateTime.Now; diff --git a/src/Umbraco.Abstractions/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Abstractions/Models/PublishedContent/PublishedCultureInfos.cs index 749b37a41a..47c81ab9a1 100644 --- a/src/Umbraco.Abstractions/Models/PublishedContent/PublishedCultureInfos.cs +++ b/src/Umbraco.Abstractions/Models/PublishedContent/PublishedCultureInfos.cs @@ -13,10 +13,9 @@ namespace Umbraco.Core.Models.PublishedContent /// public PublishedCultureInfo(string culture, string name, string urlSegment, DateTime date) { - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentNullOrEmptyException(nameof(culture)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Culture = culture; + Culture = culture ?? throw new ArgumentNullException(nameof(culture)); Name = name; UrlSegment = urlSegment; Date = date; diff --git a/src/Umbraco.Abstractions/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Abstractions/Services/Changes/ContentTypeChangeTypes.cs index 497f7d47a9..bf7f87fd1a 100644 --- a/src/Umbraco.Abstractions/Services/Changes/ContentTypeChangeTypes.cs +++ b/src/Umbraco.Abstractions/Services/Changes/ContentTypeChangeTypes.cs @@ -6,9 +6,25 @@ namespace Umbraco.Core.Services.Changes public enum ContentTypeChangeTypes : byte { None = 0, - Create = 1, // item type has been created, no impact - RefreshMain = 2, // changed, impacts content (adding property or composition does NOT) - RefreshOther = 4, // changed, other changes - Remove = 8 // item type has been removed + + /// + /// Item type has been created, no impact + /// + Create = 1, + + /// + /// Content type changes impact only the Content type being saved + /// + RefreshMain = 2, + + /// + /// Content type changes impacts the content type being saved and others used that are composed of it + /// + RefreshOther = 4, // changed, other change + + /// + /// Content type was removed + /// + Remove = 8 } } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index af9277fce9..fe7a561eca 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -42,8 +42,8 @@ namespace Umbraco.Core.Composing private string _currentAssembliesHash; private IEnumerable _assemblies; private bool _reportedChange; - private static string _localTempPath; - private static string _fileBasePath; + private readonly string _localTempPath; + private string _fileBasePath; /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index 828a577c34..5dd33c2a60 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; using Umbraco.Core.IO; using Umbraco.Core.Logging.Viewer; +using Umbraco.Core.Manifest; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PackageActions; using Umbraco.Core.Persistence.Mappers; @@ -66,9 +67,16 @@ namespace Umbraco.Core /// Gets the validators collection builder. /// /// The composition. - internal static ManifestValueValidatorCollectionBuilder Validators(this Composition composition) + internal static ManifestValueValidatorCollectionBuilder ManifestValueValidators(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the manifest filter collection builder. + /// + /// The composition. + public static ManifestFilterCollectionBuilder ManifestFilters(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the components collection builder. /// diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 6fdf7ea3b9..d1672c6c7f 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; namespace Umbraco.Core { @@ -41,7 +42,12 @@ namespace Umbraco.Core configs.Add(() => new CoreDebug()); // GridConfig depends on runtime caches, manifest parsers... and cannot be available during composition - configs.Add(factory => new GridConfig(factory.GetInstance(), factory.GetInstance(), configDir, factory.GetInstance().Debug)); + configs.Add(factory => new GridConfig( + factory.GetInstance(), + factory.GetInstance(), + configDir, + factory.GetInstance(), + factory.GetInstance().Debug)); } } } diff --git a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index b2dae09fc9..9aead74886 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -1,14 +1,15 @@ using System.IO; using Umbraco.Core.Cache; using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; namespace Umbraco.Core.Configuration.Grid { class GridConfig : IGridConfig { - public GridConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, bool isDebug) + public GridConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, ManifestParser manifestParser, bool isDebug) { - EditorsConfig = new GridEditorsConfig(logger, appCaches, configFolder, isDebug); + EditorsConfig = new GridEditorsConfig(logger, appCaches, configFolder, manifestParser, isDebug); } public IGridEditorsConfig EditorsConfig { get; } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 0e7ef62c58..d434da8c70 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using Newtonsoft.Json.Linq; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -15,13 +13,15 @@ namespace Umbraco.Core.Configuration.Grid private readonly ILogger _logger; private readonly AppCaches _appCaches; private readonly DirectoryInfo _configFolder; + private readonly ManifestParser _manifestParser; private readonly bool _isDebug; - public GridEditorsConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, bool isDebug) + public GridEditorsConfig(ILogger logger, AppCaches appCaches, DirectoryInfo configFolder, ManifestParser manifestParser, bool isDebug) { _logger = logger; _appCaches = appCaches; _configFolder = configFolder; + _manifestParser = manifestParser; _isDebug = isDebug; } @@ -31,9 +31,6 @@ namespace Umbraco.Core.Configuration.Grid { List GetResult() { - // TODO: should use the common one somehow! + ignoring _appPlugins here! - var parser = new ManifestParser(_appCaches, Current.ManifestValidators, _logger); - var editors = new List(); var gridConfig = Path.Combine(_configFolder.FullName, "grid.editors.config.js"); if (File.Exists(gridConfig)) @@ -42,7 +39,7 @@ namespace Umbraco.Core.Configuration.Grid try { - editors.AddRange(parser.ParseGridEditors(sourceString)); + editors.AddRange(_manifestParser.ParseGridEditors(sourceString)); } catch (Exception ex) { @@ -51,7 +48,7 @@ namespace Umbraco.Core.Configuration.Grid } // add manifest editors, skip duplicates - foreach (var gridEditor in parser.Manifest.GridEditors) + foreach (var gridEditor in _manifestParser.Manifest.GridEditors) { if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); } diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index f2b31be28f..673da8f9a3 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -1,27 +1,375 @@ -namespace Umbraco.Core +using System; + +namespace Umbraco.Core { public static partial class Constants { public static class DataTypes { - public const int LabelString = -92; + //NOTE: unfortunately due to backwards compat we can't move/rename these, with the addition of the GUID + //constants, it would make more sense to have these suffixed with "ID" or in a Subclass called "INT", for + //now all we can do is make a subclass called Guids to put the GUID IDs. + + public const int LabelString = System.DefaultLabelDataTypeId; public const int LabelInt = -91; public const int LabelBigint = -93; public const int LabelDateTime = -94; public const int LabelTime = -98; public const int LabelDecimal = -99; + public const int Textarea = -89; public const int Textbox = -88; + public const int RichtextEditor = -87; public const int Boolean = -49; public const int DateTime = -36; public const int DropDownSingle = -39; public const int DropDownMultiple = -42; + public const int Upload = -90; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; public const int DefaultMembersListView = -97; + public const int ImageCropper = 1043; public const int Tags = 1041; + + public static class ReservedPreValueKeys + { + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; + } + + /// + /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. + /// + public static class Guids + { + + /// + /// Guid for Content Picker as string + /// + public const string ContentPicker = "FD1E0DA5-5606-4862-B679-5D0CF3A52A59"; + + /// + /// Guid for Content Picker + /// + public static readonly Guid ContentPickerGuid = new Guid(ContentPicker); + + + /// + /// Guid for Member Picker as string + /// + public const string MemberPicker = "1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"; + + /// + /// Guid for Member Picker + /// + public static readonly Guid MemberPickerGuid = new Guid(MemberPicker); + + + /// + /// Guid for Media Picker as string + /// + public const string MediaPicker = "135D60E0-64D9-49ED-AB08-893C9BA44AE5"; + + /// + /// Guid for Media Picker + /// + public static readonly Guid MediaPickerGuid = new Guid(MediaPicker); + + + /// + /// Guid for Multiple Media Picker as string + /// + public const string MultipleMediaPicker = "9DBBCBBB-2327-434A-B355-AF1B84E5010A"; + + /// + /// Guid for Multiple Media Picker + /// + public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + + + /// + /// Guid for Related Links as string + /// + public const string RelatedLinks = "B4E3535A-1753-47E2-8568-602CF8CFEE6F"; + + /// + /// Guid for Related Links + /// + public static readonly Guid RelatedLinksGuid = new Guid(RelatedLinks); + + + /// + /// Guid for Member as string + /// + public const string Member = "d59be02f-1df9-4228-aa1e-01917d806cda"; + + /// + /// Guid for Member + /// + public static readonly Guid MemberGuid = new Guid(Member); + + + /// + /// Guid for Image Cropper as string + /// + public const string ImageCropper = "1df9f033-e6d4-451f-b8d2-e0cbc50a836f"; + + /// + /// Guid for Image Cropper + /// + public static readonly Guid ImageCropperGuid = new Guid(ImageCropper); + + + /// + /// Guid for Tags as string + /// + public const string Tags = "b6b73142-b9c1-4bf8-a16d-e1c23320b549"; + + /// + /// Guid for Tags + /// + public static readonly Guid TagsGuid = new Guid(Tags); + + + /// + /// Guid for List View - Content as string + /// + public const string ListViewContent = "C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"; + + /// + /// Guid for List View - Content + /// + public static readonly Guid ListViewContentGuid = new Guid(ListViewContent); + + + /// + /// Guid for List View - Media as string + /// + public const string ListViewMedia = "3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"; + + /// + /// Guid for List View - Media + /// + public static readonly Guid ListViewMediaGuid = new Guid(ListViewMedia); + + + /// + /// Guid for List View - Members as string + /// + public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D"; + + /// + /// Guid for List View - Members + /// + public static readonly Guid ListViewMembersGuid = new Guid(ListViewMembers); + + + /// + /// Guid for Date Picker with time as string + /// + public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a"; + + /// + /// Guid for Date Picker with time + /// + public static readonly Guid DatePickerWithTimeGuid = new Guid(DatePickerWithTime); + + + /// + /// Guid for Approved Color as string + /// + public const string ApprovedColor = "0225af17-b302-49cb-9176-b9f35cab9c17"; + + /// + /// Guid for Approved Color + /// + public static readonly Guid ApprovedColorGuid = new Guid(ApprovedColor); + + + /// + /// Guid for Dropdown multiple as string + /// + public const string DropdownMultiple = "f38f0ac7-1d27-439c-9f3f-089cd8825a53"; + + /// + /// Guid for Dropdown multiple + /// + public static readonly Guid DropdownMultipleGuid = new Guid(DropdownMultiple); + + + /// + /// Guid for Radiobox as string + /// + public const string Radiobox = "bb5f57c9-ce2b-4bb9-b697-4caca783a805"; + + /// + /// Guid for Radiobox + /// + public static readonly Guid RadioboxGuid = new Guid(Radiobox); + + + /// + /// Guid for Date Picker as string + /// + public const string DatePicker = "5046194e-4237-453c-a547-15db3a07c4e1"; + + /// + /// Guid for Date Picker + /// + public static readonly Guid DatePickerGuid = new Guid(DatePicker); + + + /// + /// Guid for Dropdown as string + /// + public const string Dropdown = "0b6a45e7-44ba-430d-9da5-4e46060b9e03"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid DropdownGuid = new Guid(Dropdown); + + + /// + /// Guid for Checkbox list as string + /// + public const string CheckboxList = "fbaf13a8-4036-41f2-93a3-974f678c312a"; + + /// + /// Guid for Checkbox list + /// + public static readonly Guid CheckboxListGuid = new Guid(CheckboxList); + + + /// + /// Guid for Checkbox as string + /// + public const string Checkbox = "92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"; + + /// + /// Guid for Checkbox + /// + public static readonly Guid CheckboxGuid = new Guid(Checkbox); + + + /// + /// Guid for Numeric as string + /// + public const string Numeric = "2e6d3631-066e-44b8-aec4-96f09099b2b5"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid NumericGuid = new Guid(Numeric); + + + /// + /// Guid for Richtext editor as string + /// + public const string RichtextEditor = "ca90c950-0aff-4e72-b976-a30b1ac57dad"; + + /// + /// Guid for Richtext editor + /// + public static readonly Guid RichtextEditorGuid = new Guid(RichtextEditor); + + + /// + /// Guid for Textstring as string + /// + public const string Textstring = "0cc0eba1-9960-42c9-bf9b-60e150b429ae"; + + /// + /// Guid for Textstring + /// + public static readonly Guid TextstringGuid = new Guid(Textstring); + + + /// + /// Guid for Textarea as string + /// + public const string Textarea = "c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"; + + /// + /// Guid for Dropdown + /// + public static readonly Guid TextareaGuid = new Guid(Textarea); + + + /// + /// Guid for Upload as string + /// + public const string Upload = "84c6b441-31df-4ffe-b67e-67d5bc3ae65a"; + + /// + /// Guid for Upload + /// + public static readonly Guid UploadGuid = new Guid(Upload); + + + /// + /// Guid for Label as string + /// + public const string LabelString = "f0bc4bfb-b499-40d6-ba86-058885a5178c"; + + /// + /// Guid for Label string + /// + public static readonly Guid LabelStringGuid = new Guid(LabelString); + + /// + /// Guid for Label as int + /// + public const string LabelInt = "8e7f995c-bd81-4627-9932-c40e568ec788"; + + /// + /// Guid for Label int + /// + public static readonly Guid LabelIntGuid = new Guid(LabelInt); + + /// + /// Guid for Label as big int + /// + public const string LabelBigInt = "930861bf-e262-4ead-a704-f99453565708"; + + /// + /// Guid for Label big int + /// + public static readonly Guid LabelBigIntGuid = new Guid(LabelBigInt); + + /// + /// Guid for Label as date time + /// + public const string LabelDateTime = "0e9794eb-f9b5-4f20-a788-93acd233a7e4"; + + /// + /// Guid for Label date time + /// + public static readonly Guid LabelDateTimeGuid = new Guid(LabelDateTime); + + /// + /// Guid for Label as time + /// + public const string LabelTime = "a97cec69-9b71-4c30-8b12-ec398860d7e8"; + + /// + /// Guid for Label time + /// + public static readonly Guid LabelTimeGuid = new Guid(LabelTime); + + /// + /// Guid for Label as decimal + /// + public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; + + /// + /// Guid for Label decimal + /// + public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); + + + } } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index d3e8b4ad3b..05213ed1c4 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -5,39 +5,89 @@ public static class Icons { /// - /// System contenttype icon + /// System default icon /// - public const string ContentType = "icon-arrangement"; + public const string DefaultIcon = Content; /// - /// System datatype icon + /// System content icon + /// + public const string Content = "icon-document"; + + /// + /// System content type icon + /// + public const string ContentType = "icon-item-arrangement"; + + /// + /// System data type icon /// public const string DataType = "icon-autofill"; /// - /// System property editor icon + /// System list view icon /// - public const string PropertyEditor = "icon-autofill"; + public const string ListView = "icon-thumbnail-list"; /// /// System macro icon /// public const string Macro = "icon-settings-alt"; + /// + /// System media file icon + /// + public const string MediaFile = "icon-document"; + + /// + /// System media folder icon + /// + public const string MediaFolder = "icon-folder"; + + /// + /// System media image icon + /// + public const string MediaImage = "icon-picture"; + + /// + /// System media type icon + /// + public const string MediaType = "icon-thumbnails"; + /// /// System member icon /// public const string Member = "icon-user"; /// - /// System member icon + /// System member group icon + /// + public const string MemberGroup = "icon-users-alt"; + + /// + /// System member type icon /// public const string MemberType = "icon-users"; + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; + /// /// System member icon /// public const string Template = "icon-layout"; + + /// + /// System user icon + /// + public const string User = "icon-user"; + + /// + /// System user group icon + /// + public const string UserGroup = "icon-users"; } } } diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index d25997b5f0..9fdc5f0b90 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -66,44 +66,44 @@ namespace Umbraco.Core /// /// Determines whether the content type is invariant. /// - public static bool VariesByNothing(this PublishedContentType contentType) => contentType.Variations.VariesByNothing(); + public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// /// And then it could also vary by segment. - public static bool VariesByCulture(this PublishedContentType contentType) => contentType.Variations.VariesByCulture(); + public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type varies by segment. /// /// And then it could also vary by culture. - public static bool VariesBySegment(this PublishedContentType contentType) => contentType.Variations.VariesBySegment(); + public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); /// /// Determines whether the content type varies by culture and segment. /// - public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the property type is invariant. /// - public static bool VariesByNothing(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); + public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// - public static bool VariesByCulture(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); + public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// - public static bool VariesBySegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); + public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// Determines whether the property type varies by culture and segment. /// - public static bool VariesByCultureAndSegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); /// /// Determines whether a variation is invariant. diff --git a/src/Umbraco.Core/Manifest/IManifestFilter.cs b/src/Umbraco.Core/Manifest/IManifestFilter.cs new file mode 100644 index 0000000000..505f13d385 --- /dev/null +++ b/src/Umbraco.Core/Manifest/IManifestFilter.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Manifest +{ + /// + /// Provides filtering for package manifests. + /// + public interface IManifestFilter + { + /// + /// Filters package manifests. + /// + /// The package manifests. + /// + /// It is possible to remove, change, or add manifests. + /// + void Filter(List manifests); + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs new file mode 100644 index 0000000000..febdb7e356 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Manifest +{ + /// + /// Contains the manifest filters. + /// + public class ManifestFilterCollection : BuilderCollectionBase + { + /// + /// Initializes a new instance of the class. + /// + public ManifestFilterCollection(IEnumerable items) + : base(items) + { } + + /// + /// Filters package manifests. + /// + /// The package manifests. + public void Filter(List manifests) + { + foreach (var filter in this) + filter.Filter(manifests); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs new file mode 100644 index 0000000000..4d98700f93 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollectionBuilder.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Manifest +{ + public class ManifestFilterCollectionBuilder : OrderedCollectionBuilderBase + { + protected override ManifestFilterCollectionBuilder This => this; + + // do NOT cache this, it's only used once + protected override Lifetime CollectionLifetime => Lifetime.Transient; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index dc40cd90a2..efd9e92b1f 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -22,24 +22,26 @@ namespace Umbraco.Core.Manifest private readonly IAppPolicyCache _cache; private readonly ILogger _logger; private readonly ManifestValueValidatorCollection _validators; + private readonly ManifestFilterCollection _filters; private string _path; /// /// Initializes a new instance of the class. /// - public ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ILogger logger) - : this(appCaches, validators, "~/App_Plugins", logger) + public ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, ILogger logger) + : this(appCaches, validators, filters, "~/App_Plugins", logger) { } /// /// Initializes a new instance of the class. /// - private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, string path, ILogger logger) + private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string path, ILogger logger) { if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); _cache = appCaches.RuntimeCache; _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + _filters = filters ?? throw new ArgumentNullException(nameof(filters)); if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); Path = path; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -78,6 +80,7 @@ namespace Umbraco.Core.Manifest if (string.IsNullOrWhiteSpace(text)) continue; var manifest = ParseManifest(text); + manifest.Source = path; manifests.Add(manifest); } catch (Exception e) @@ -86,6 +89,8 @@ namespace Umbraco.Core.Manifest } } + _filters.Filter(manifests); + return manifests; } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 475ee8a7f8..e50eb69467 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -9,6 +9,16 @@ namespace Umbraco.Core.Manifest /// public class PackageManifest { + /// + /// Gets the source path of the manifest. + /// + /// + /// Gets the full absolute file path of the manifest, + /// using system directory separators. + /// + [JsonIgnore] + public string Source { get; set; } + /// /// Gets or sets the scripts listed in the manifest. /// diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 2d495b38b5..8915ebcf74 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -191,7 +191,7 @@ namespace Umbraco.Core.Mapping private TTarget Map(object source, Type sourceType, MapperContext context) { if (source == null) - throw new ArgumentNullException(nameof(source)); + return default; var targetType = typeof(TTarget); diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 1de983636b..dd5c17713a 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -101,33 +101,33 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = Constants.ObjectTypes.SystemRoot, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = Constants.ObjectTypes.ContentRecycleBin, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = Constants.ObjectTypes.MediaRecycleBin, CreateDate = DateTime.Now }); - InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, "f0bc4bfb-b499-40d6-ba86-058885a5178c", "Label"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", "Label (datetime)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); - InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -90, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-90", SortOrder = 34, UniqueId = new Guid("84c6b441-31df-4ffe-b67e-67d5bc3ae65a"), Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -89, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-89", SortOrder = 33, UniqueId = new Guid("c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"), Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -88, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-88", SortOrder = 32, UniqueId = new Guid("0cc0eba1-9960-42c9-bf9b-60e150b429ae"), Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -87, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-87", SortOrder = 4, UniqueId = new Guid("ca90c950-0aff-4e72-b976-a30b1ac57dad"), Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = new Guid("2e6d3631-066e-44b8-aec4-96f09099b2b5"), Text = "Numeric", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -49, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-49", SortOrder = 2, UniqueId = new Guid("92897bc6-a5f3-4ffe-ae27-f2e7e33dda49"), Text = "True/false", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = new Guid("fbaf13a8-4036-41f2-93a3-974f678c312a"), Text = "Checkbox list", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = new Guid("0b6a45e7-44ba-430d-9da5-4e46060b9e03"), Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = new Guid("5046194e-4237-453c-a547-15db3a07c4e1"), Text = "Date Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = new Guid("bb5f57c9-ce2b-4bb9-b697-4caca783a805"), Text = "Radiobox", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = new Guid("0225af17-b302-49cb-9176-b9f35cab9c17"), Text = "Approved Color", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -36, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-36", SortOrder = 2, UniqueId = new Guid("e4d66c0f-b935-4200-81f0-025f7256b89a"), Text = "Date Picker with time", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = new Guid("3A0156C4-3B8C-4803-BDC1-6871FAA83FFF"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, Constants.DataTypes.Guids.LabelString, "Label (string)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, Constants.DataTypes.Guids.LabelInt, "Label (integer)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, Constants.DataTypes.Guids.LabelBigInt, "Label (bigint)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.NumericGuid, Text = "Numeric", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Boolean, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Boolean}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.CheckboxGuid, Text = "True/false", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.CheckboxListGuid, Text = "Checkbox list", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DropdownGuid, Text = "Dropdown", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DatePickerGuid, Text = "Date Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.RadioboxGuid, Text = "Radiobox", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DropdownMultipleGuid, Text = "Dropdown multiple", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ApprovedColorGuid, Text = "Approved Color", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DateTime, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DateTime}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.DatePickerWithTimeGuid, Text = "Date Picker with time", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewContentGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewMediaGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.ListViewMembersGuid, Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1041, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1041", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); //New UDI pickers with newer Ids @@ -155,10 +155,10 @@ namespace Umbraco.Core.Migrations.Install private void CreateContentTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } private void CreateUserData() @@ -210,19 +210,19 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); //membership property types - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); @@ -266,30 +266,30 @@ namespace Umbraco.Core.Migrations.Install const string layouts = "[" + cardLayout + "," + listLayout + "]"; // TODO: Check which of the DataTypeIds below doesn't exist in umbracoNode, which results in a foreign key constraint errors. - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -49, EditorAlias = Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Boolean, EditorAlias = Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -51, EditorAlias = Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -87, EditorAlias = Constants.PropertyEditors.Aliases.TinyMce, DbType = "Ntext", Configuration = "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -88, EditorAlias = Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -89, EditorAlias = Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -90, EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Textbox, EditorAlias = Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Textarea, EditorAlias = Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Upload, EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.Label, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.Label, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -36, EditorAlias = Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DateTime, EditorAlias = Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" }); InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1041, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext", + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.Tags, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext", Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1043, EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.ImageCropper, EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultContentListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Constants.DataTypes.DefaultMediaListView, EditorAlias = Constants.PropertyEditors.Aliases.ListView, DbType = "Nvarchar", diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs index 9e2dec9b70..4c3f8534e7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -2,24 +2,24 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { - [RuntimeLevel(MinLevel = RuntimeLevel.Upgrade, MaxLevel = RuntimeLevel.Upgrade)] // only on upgrades - public class PreValueMigratorComposer : ICoreComposer +[RuntimeLevel(MinLevel = RuntimeLevel.Upgrade, MaxLevel = RuntimeLevel.Upgrade)] // only on upgrades +public class PreValueMigratorComposer : ICoreComposer +{ + public void Compose(Composition composition) { - public void Compose(Composition composition) - { - // do NOT add DefaultPreValueMigrator to this list! - // it will be automatically used if nothing matches + // do NOT add DefaultPreValueMigrator to this list! + // it will be automatically used if nothing matches - composition.WithCollectionBuilder() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - } + composition.WithCollectionBuilder() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); } } +} diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index df8a3caea8..f460edbde7 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -35,5 +35,58 @@ namespace Umbraco.Core.Models throw new InvalidCastException($"Cannot cast dataType configuration, of type {configuration.GetType().Name}, to {typeof(T).Name}."); } + + private static readonly ISet IdsOfBuildInDataTypes = new HashSet() + { + Constants.DataTypes.Guids.ContentPickerGuid, + Constants.DataTypes.Guids.MemberPickerGuid, + Constants.DataTypes.Guids.MediaPickerGuid, + Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Constants.DataTypes.Guids.RelatedLinksGuid, + Constants.DataTypes.Guids.MemberGuid, + Constants.DataTypes.Guids.ImageCropperGuid, + Constants.DataTypes.Guids.TagsGuid, + Constants.DataTypes.Guids.ListViewContentGuid, + Constants.DataTypes.Guids.ListViewMediaGuid, + Constants.DataTypes.Guids.ListViewMembersGuid, + Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Constants.DataTypes.Guids.ApprovedColorGuid, + Constants.DataTypes.Guids.DropdownMultipleGuid, + Constants.DataTypes.Guids.RadioboxGuid, + Constants.DataTypes.Guids.DatePickerGuid, + Constants.DataTypes.Guids.DropdownGuid, + Constants.DataTypes.Guids.CheckboxListGuid, + Constants.DataTypes.Guids.CheckboxGuid, + Constants.DataTypes.Guids.NumericGuid, + Constants.DataTypes.Guids.RichtextEditorGuid, + Constants.DataTypes.Guids.TextstringGuid, + Constants.DataTypes.Guids.TextareaGuid, + Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.LabelStringGuid, + Constants.DataTypes.Guids.LabelDecimalGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + Constants.DataTypes.Guids.LabelBigIntGuid, + Constants.DataTypes.Guids.LabelTimeGuid, + Constants.DataTypes.Guids.LabelDateTimeGuid, + }; + + /// + /// Returns true if this date type is build-in/default. + /// + /// The data type definition. + /// + internal static bool IsBuildInDataType(this IDataType dataType) + { + return IsBuildInDataType(dataType.Key); + } + + /// + /// Returns true if this date type is build-in/default. + /// + internal static bool IsBuildInDataType(Guid key) + { + return IdsOfBuildInDataTypes.Contains(key); + } + } } diff --git a/src/Umbraco.Core/Models/Entities/EntityExtensions.cs b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs new file mode 100644 index 0000000000..2ee6a2d5ed --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/EntityExtensions.cs @@ -0,0 +1,46 @@ +using System; + +namespace Umbraco.Core.Models.Entities +{ + internal static class EntityExtensions + { + /// + /// Updates the entity when it is being saved. + /// + internal static void UpdatingEntity(this IEntity entity) + { + var now = DateTime.Now; + + // just in case + if (entity.CreateDate == default) + { + entity.CreateDate = now; + } + + // set the update date if not already set + if (entity.UpdateDate == default || (entity is ICanBeDirty canBeDirty && canBeDirty.IsPropertyDirty("UpdateDate") == false)) + { + entity.UpdateDate = now; + } + } + + /// + /// Updates the entity when it is being saved for the first time. + /// + internal static void AddingEntity(this IEntity entity) + { + var now = DateTime.Now; + var canBeDirty = entity as ICanBeDirty; + + // set the create and update dates, if not already set + if (entity.CreateDate == default || canBeDirty?.IsPropertyDirty("CreateDate") == false) + { + entity.CreateDate = now; + } + if (entity.UpdateDate == default || canBeDirty?.IsPropertyDirty("UpdateDate") == false) + { + entity.UpdateDate = now; + } + } + } +} diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 537a830bca..b473a154f1 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -478,19 +478,6 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _providerUserKey, nameof(ProviderUserKey)); } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - public override void AddingEntity() - { - base.AddingEntity(); - - if (ProviderUserKey == null) - ProviderUserKey = Key; - } - /* Internal experiment - only used for mapping queries. * Adding these to have first level properties instead of the Properties collection. */ diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 908491fbb2..e7a9ae2dfd 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -20,6 +20,7 @@ namespace Umbraco.Core.Models private string _alias; private string _description; private int _dataTypeId; + private Guid _dataTypeKey; private Lazy _propertyGroupId; private string _propertyEditorAlias; private ValueStorageType _valueStorageType; @@ -138,6 +139,13 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId)); } + [DataMember] + public Guid DataTypeKey + { + get => _dataTypeKey; + set => SetPropertyValueAndDetectChanges(value, ref _dataTypeKey, nameof(DataTypeKey)); + } + /// /// Gets or sets the alias of the property editor for this property type. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs index 4027184f3c..0810f2207b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs @@ -13,6 +13,10 @@ /// /// Refreshes the factory. /// + /// + /// This will typically re-compiled models/classes into a new DLL that are used to populate the cache. + /// This is called prior to refreshing the cache. + /// void Refresh(); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index d38c8eb721..1c0d39a8b8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent { - /// /// /// Represents a published content item. @@ -27,21 +26,13 @@ namespace Umbraco.Core.Models.PublishedContent int Id { get; } /// - /// Gets the name of the content item. + /// Gets the name of the content item for the current culture. /// - /// - /// The value of this property is contextual. When the content type is multi-lingual, - /// this is the name for the 'current' culture. Otherwise, it is the invariant name. - /// string Name { get; } /// - /// Gets the url segment of the content item. + /// Gets the url segment of the content item for the current culture. /// - /// - /// The value of this property is contextual. When the content type is multi-lingual, - /// this is the name for the 'current' culture. Otherwise, it is the invariant url segment. - /// string UrlSegment { get; } /// @@ -94,43 +85,28 @@ namespace Umbraco.Core.Models.PublishedContent /// /// /// For published content items, this is also the date the item was published. - /// This date is always global to the content item, see GetCulture().Date for the + /// This date is always global to the content item, see CultureDate() for the /// date each culture was published. /// DateTime UpdateDate { get; } /// - /// Gets the url of the content item. + /// Gets the url of the content item for the current culture. /// /// /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// 'current' culture. Otherwise, it is the invariant url. + /// if any. /// string Url { get; } /// - /// Gets the url of the content item. - /// - /// - /// The value of this property is contextual. It depends on the 'current' request uri, - /// if any. In addition, when the content type is multi-lingual, this is the url for the - /// specified culture. Otherwise, it is the invariant url. - /// - string GetUrl(string culture = null); - - /// - /// Gets culture infos for a culture. - /// - PublishedCultureInfo GetCulture(string culture = null); - - /// - /// Gets culture infos. + /// Gets available culture infos. /// /// /// Contains only those culture that are available. For a published content, these are /// the cultures that are published. For a draft content, those that are 'available' ie /// have a non-empty content name. + /// Does not contain the invariant culture. // fixme? /// IReadOnlyDictionary Cultures { get; } @@ -178,11 +154,15 @@ namespace Umbraco.Core.Models.PublishedContent IPublishedContent Parent { get; } /// - /// Gets the children of the content item. + /// Gets the children of the content item that are available for the current culture. /// - /// Children are sorted by their sortOrder. IEnumerable Children { get; } + /// + /// Gets all the children of the content item, regardless of whether they are available for the current culture. + /// + IEnumerable ChildrenForAllCultures { get; } + #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs new file mode 100644 index 0000000000..ab6920377c --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents an type. + /// + /// Instances implementing the interface should be + /// immutable, ie if the content type changes, then a new instance needs to be created. + public interface IPublishedContentType + { + /// + /// Gets the content type identifier. + /// + int Id { get; } + + /// + /// Gets the content type alias. + /// + string Alias { get; } + + /// + /// Gets the content item type. + /// + PublishedItemType ItemType { get; } + + /// + /// Gets the aliases of the content types participating in the composition. + /// + HashSet CompositionAliases { get; } + + /// + /// Gets the content variations of the content type. + /// + ContentVariation Variations { get; } + + /// + /// Gets a value indicating whether this content type is for an element. + /// + bool IsElement { get; } + + /// + /// Gets the content type properties. + /// + IEnumerable PropertyTypes { get; } + + /// + /// Gets a property type index. + /// + /// The alias is case-insensitive. This is the only place where alias strings are compared. + int GetPropertyIndex(string alias); + + /// + /// Gets a property type. + /// + IPublishedPropertyType GetPropertyType(string alias); + + /// + /// Gets a property type. + /// + IPublishedPropertyType GetPropertyType(int index); + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index e75e8a4eb9..816bfdbb01 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -10,7 +10,7 @@ /// /// An content type. /// A published content type corresponding to the item type and content type. - PublishedContentType CreateContentType(IContentTypeComposition contentType); + IPublishedContentType CreateContentType(IContentTypeComposition contentType); /// /// Creates a published property type. @@ -18,7 +18,7 @@ /// The published content type owning the property. /// A property type. /// Is used by constructor to create property types. - PublishedPropertyType CreatePropertyType(PublishedContentType contentType, PropertyType propertyType); + IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, PropertyType propertyType); /// /// Creates a published property type. @@ -28,7 +28,7 @@ /// The datatype identifier. /// The variations. /// Is used by constructor to create special property types. - PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); /// /// Gets a published datatype. diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs index 4b579d824b..4c72dc914a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedElement.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the content type. /// - PublishedContentType ContentType { get; } + IPublishedContentType ContentType { get; } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs index 9a00e94d3e..2ee7dcb28f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedProperty.cs @@ -5,7 +5,7 @@ /// public interface IPublishedProperty { - PublishedPropertyType PropertyType { get; } + IPublishedPropertyType PropertyType { get; } /// /// Gets the alias of the property. diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs new file mode 100644 index 0000000000..40f2bf3df2 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs @@ -0,0 +1,108 @@ +using System; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents a published property type. + /// + /// Instances implementing the interface should be + /// immutable, ie if the property type changes, then a new instance needs to be created. + public interface IPublishedPropertyType + { + /// + /// Gets the published content type containing the property type. + /// + IPublishedContentType ContentType { get; } + + /// + /// Gets the data type. + /// + PublishedDataType DataType { get; } + + /// + /// Gets property type alias. + /// + string Alias { get; } + + /// + /// Gets the property editor alias. + /// + string EditorAlias { get; } + + /// + /// Gets a value indicating whether the property is a user content property. + /// + /// A non-user content property is a property that has been added to a + /// published content type by Umbraco but does not corresponds to a user-defined + /// published property. + bool IsUserProperty { get; } + + /// + /// Gets the content variations of the property type. + /// + ContentVariation Variations { get; } + + /// + /// Determines whether a value is an actual value, or not a value. + /// + /// Used by property.HasValue and, for instance, in fallback scenarios. + bool? IsValue(object value, PropertyValueLevel level); + + /// + /// Gets the property cache level. + /// + PropertyCacheLevel CacheLevel { get; } + + /// + /// Converts the source value into the intermediate value. + /// + /// The published element owning the property. + /// The source value. + /// A value indicating whether content should be considered draft. + /// The intermediate value. + object ConvertSourceToInter(IPublishedElement owner, object source, bool preview); + + /// + /// Converts the intermediate value into the object value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The object value. + object ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + + /// + /// Converts the intermediate value into the XPath value. + /// + /// The published element owning the property. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether content should be considered draft. + /// The XPath value. + /// + /// The XPath value can be either a string or an XPathNavigator. + /// + object ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + + /// + /// Gets the property model CLR type. + /// + /// + /// The model CLR type may be a type, or may contain types. + /// For the actual CLR type, see . + /// + Type ModelClrType { get; } + + /// + /// Gets the property CLR type. + /// + /// + /// Returns the actual CLR type which does not contain types. + /// Mapping from may throw if some instances + /// could not be mapped to actual CLR types. + /// + Type ClrType { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 0798e9a4e0..3b03cfc9ea 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -5,13 +5,13 @@ using System.Linq; namespace Umbraco.Core.Models.PublishedContent { /// - /// Represents an type. + /// Represents an type. /// /// Instances of the class are immutable, ie /// if the content type changes, then a new class needs to be created. - public class PublishedContentType + public class PublishedContentType : IPublishedContentType { - private readonly PublishedPropertyType[] _propertyTypes; + private readonly IPublishedPropertyType[] _propertyTypes; // fast alias-to-index xref containing both the raw alias and its lowercase version private readonly Dictionary _indexes = new Dictionary(); @@ -35,11 +35,10 @@ namespace Umbraco.Core.Models.PublishedContent } /// - /// Initializes a new instance of the with specific values. + /// This constructor is for tests and is not intended to be used directly from application code. /// /// - /// This constructor is for tests and is not intended to be used directly from application code. - /// Values are assumed to be consisted and are not checked. + /// Values are assumed to be consistent and are not checked. /// public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) : this (id, alias, itemType, compositionAliases, variations, isElement) @@ -52,6 +51,20 @@ namespace Umbraco.Core.Models.PublishedContent InitializeIndexes(); } + /// + /// This constructor is for tests and is not intended to be used directly from application code. + /// + /// + /// Values are assumed to be consistent and are not checked. + /// + public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false) + : this(id, alias, itemType, compositionAliases, variations, isElement) + { + _propertyTypes = propertyTypes(this).ToArray(); + + InitializeIndexes(); + } + private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement) { Id = id; @@ -75,7 +88,7 @@ namespace Umbraco.Core.Models.PublishedContent // Members have properties such as IMember LastLoginDate which are plain C# properties and not content // properties; they are exposed as pseudo content properties, as long as a content property with the // same alias does not exist already. - private void EnsureMemberProperties(List propertyTypes, IPublishedContentTypeFactory factory) + private void EnsureMemberProperties(List propertyTypes, IPublishedContentTypeFactory factory) { var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase); @@ -103,44 +116,29 @@ namespace Umbraco.Core.Models.PublishedContent #region Content type - /// - /// Gets the content type identifier. - /// + /// public int Id { get; } - /// - /// Gets the content type alias. - /// + /// public string Alias { get; } - /// - /// Gets the content item type. - /// + /// public PublishedItemType ItemType { get; } - /// - /// Gets the aliases of the content types participating in the composition. - /// + /// public HashSet CompositionAliases { get; } - /// - /// Gets the content variations of the content type. - /// + /// public ContentVariation Variations { get; } #endregion #region Properties - /// - /// Gets the content type properties. - /// - public IEnumerable PropertyTypes => _propertyTypes; + /// + public IEnumerable PropertyTypes => _propertyTypes; - /// - /// Gets a property type index. - /// - /// The alias is case-insensitive. This is the only place where alias strings are compared. + /// public int GetPropertyIndex(string alias) { if (_indexes.TryGetValue(alias, out var index)) return index; // fastest @@ -150,10 +148,8 @@ namespace Umbraco.Core.Models.PublishedContent // virtual for unit tests // TODO: explain why - /// - /// Gets a property type. - /// - public virtual PublishedPropertyType GetPropertyType(string alias) + /// + public virtual IPublishedPropertyType GetPropertyType(string alias) { var index = GetPropertyIndex(alias); return GetPropertyType(index); @@ -161,17 +157,13 @@ namespace Umbraco.Core.Models.PublishedContent // virtual for unit tests // TODO: explain why - /// - /// Gets a property type. - /// - public virtual PublishedPropertyType GetPropertyType(int index) + /// + public virtual IPublishedPropertyType GetPropertyType(int index) { return index >= 0 && index < _propertyTypes.Length ? _propertyTypes[index] : null; } - /// - /// Gets a value indicating whether this content type is for an element. - /// + /// public bool IsElement { get; } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 2ca3593b55..17a15a2536 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -26,37 +26,46 @@ namespace Umbraco.Core.Models.PublishedContent } /// - public PublishedContentType CreateContentType(IContentTypeComposition contentType) + public IPublishedContentType CreateContentType(IContentTypeComposition contentType) { return new PublishedContentType(contentType, this); } - // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType(int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); } - // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, PropertyType propertyType) + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, PropertyType propertyType) { return new PublishedPropertyType(contentType, propertyType, _propertyValueConverters, _publishedModelFactory, this); } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) + public IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } - // for tests - internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) + /// + /// This method is for tests and is not intended to be used directly from application code. + /// + /// Values are assumed to be consisted and are not checked. + internal IPublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 8bf8cec244..fb41c95419 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models.PublishedContent #region ContentType /// - public virtual PublishedContentType ContentType => _content.ContentType; + public virtual IPublishedContentType ContentType => _content.ContentType; #endregion @@ -96,12 +96,6 @@ namespace Umbraco.Core.Models.PublishedContent /// public virtual string Url => _content.Url; - /// - public virtual string GetUrl(string culture = null) => _content.GetUrl(culture); - - /// - public PublishedCultureInfo GetCulture(string culture = null) => _content.GetCulture(culture); - /// public IReadOnlyDictionary Cultures => _content.Cultures; @@ -124,6 +118,9 @@ namespace Umbraco.Core.Models.PublishedContent /// public virtual IEnumerable Children => _content.Children; + /// + public virtual IEnumerable ChildrenForAllCultures => _content.ChildrenForAllCultures; + #endregion #region Properties diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs index 1989ac2caf..481b9bd5d2 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementWrapped.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Models.PublishedContent public IPublishedElement Unwrap() => _content; /// - public PublishedContentType ContentType => _content.ContentType; + public IPublishedContentType ContentType => _content.ContentType; /// public Guid Key => _content.Key; diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index 5f374f8bc8..e11d2391ec 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Initializes a new instance of the class. /// - protected PublishedPropertyBase(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) + protected PublishedPropertyBase(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel) { PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); ReferenceCacheLevel = referenceCacheLevel; @@ -42,7 +42,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the property type. /// - public PublishedPropertyType PropertyType { get; } + public IPublishedPropertyType PropertyType { get; } /// /// Gets the property reference cache level. diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 68892fd79a..0c2e62770e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -10,10 +10,8 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Instances of the class are immutable, ie /// if the property type changes, then a new class needs to be created. - public class PublishedPropertyType + public class PublishedPropertyType : IPublishedPropertyType { - // TODO: API design review, should this be an interface? - private readonly IPublishedModelFactory _publishedModelFactory; private readonly PropertyValueConverterCollection _propertyValueConverters; private readonly object _locker = new object(); @@ -32,7 +30,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// The new published property type belongs to the published content type. /// - public PublishedPropertyType(PublishedContentType contentType, PropertyType propertyType, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) + public PublishedPropertyType(IPublishedContentType contentType, PropertyType propertyType, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) : this(propertyType.Alias, propertyType.DataTypeId, true, propertyType.Variations, propertyValueConverters, publishedModelFactory, factory) { ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); @@ -45,7 +43,7 @@ namespace Umbraco.Core.Models.PublishedContent /// Values are assumed to be consisted and are not checked. /// The new published property type belongs to the published content type. /// - public PublishedPropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, bool isUserProperty, ContentVariation variations, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) + public PublishedPropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, bool isUserProperty, ContentVariation variations, PropertyValueConverterCollection propertyValueConverters, IPublishedModelFactory publishedModelFactory, IPublishedContentTypeFactory factory) : this(propertyTypeAlias, dataTypeId, isUserProperty, variations, propertyValueConverters, publishedModelFactory, factory) { ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); @@ -75,37 +73,22 @@ namespace Umbraco.Core.Models.PublishedContent #region Property type - /// - /// Gets the published content type containing the property type. - /// - public PublishedContentType ContentType { get; internal set; } // internally set by PublishedContentType constructor + /// + public IPublishedContentType ContentType { get; internal set; } // internally set by PublishedContentType constructor - /// - /// Gets the data type. - /// + /// public PublishedDataType DataType { get; } - /// - /// Gets property type alias. - /// + /// public string Alias { get; } - /// - /// Gets the property editor alias. - /// + /// public string EditorAlias => DataType.EditorAlias; - /// - /// Gets a value indicating whether the property is a user content property. - /// - /// A non-user content property is a property that has been added to a - /// published content type by Umbraco but does not corresponds to a user-defined - /// published property. + /// public bool IsUserProperty { get; } - /// - /// Gets the content variations of the property type. - /// + /// public ContentVariation Variations { get; } #endregion @@ -193,10 +176,7 @@ namespace Umbraco.Core.Models.PublishedContent _modelClrType = _converter == null ? typeof (object) : _converter.GetPropertyValueType(this); } - /// - /// Determines whether a value is an actual value, or not a value. - /// - /// Used by property.HasValue and, for instance, in fallback scenarios. + /// public bool? IsValue(object value, PropertyValueLevel level) { if (!_initialized) Initialize(); @@ -209,9 +189,7 @@ namespace Umbraco.Core.Models.PublishedContent return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } - /// - /// Gets the property cache level. - /// + /// public PropertyCacheLevel CacheLevel { get @@ -221,13 +199,7 @@ namespace Umbraco.Core.Models.PublishedContent } } - /// - /// Converts the source value into the intermediate value. - /// - /// The published element owning the property. - /// The source value. - /// A value indicating whether content should be considered draft. - /// The intermediate value. + /// public object ConvertSourceToInter(IPublishedElement owner, object source, bool preview) { if (!_initialized) Initialize(); @@ -238,14 +210,7 @@ namespace Umbraco.Core.Models.PublishedContent : source; } - /// - /// Converts the intermediate value into the object value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The object value. + /// public object ConvertInterToObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (!_initialized) Initialize(); @@ -256,17 +221,7 @@ namespace Umbraco.Core.Models.PublishedContent : inter; } - /// - /// Converts the intermediate value into the XPath value. - /// - /// The published element owning the property. - /// The reference cache level. - /// The intermediate value. - /// A value indicating whether content should be considered draft. - /// The XPath value. - /// - /// The XPath value can be either a string or an XPathNavigator. - /// + /// public object ConvertInterToXPath(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (!_initialized) Initialize(); @@ -282,13 +237,7 @@ namespace Umbraco.Core.Models.PublishedContent return inter.ToString().Trim(); } - /// - /// Gets the property model CLR type. - /// - /// - /// The model CLR type may be a type, or may contain types. - /// For the actual CLR type, see . - /// + /// public Type ModelClrType { get @@ -298,14 +247,7 @@ namespace Umbraco.Core.Models.PublishedContent } } - /// - /// Gets the property CLR type. - /// - /// - /// Returns the actual CLR type which does not contain types. - /// Mapping from may throw if some instances - /// could not be mapped to actual CLR types. - /// + /// public Type ClrType { get diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 7469222ab0..10f999532f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -38,7 +38,7 @@ namespace Umbraco.Core.Models.PublishedContent public override object GetXPathValue(string culture = null, string segment = null) => string.IsNullOrEmpty(culture) & string.IsNullOrEmpty(segment) ? _xpathValue.Value : null; - public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) + public RawValueProperty(IPublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { if (propertyType.Variations != ContentVariation.Nothing) diff --git a/src/Umbraco.Web/Routing/UrlProviderMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs similarity index 63% rename from src/Umbraco.Web/Routing/UrlProviderMode.cs rename to src/Umbraco.Core/Models/PublishedContent/UrlMode.cs index ac29aed3fb..4cd6a680f4 100644 --- a/src/Umbraco.Web/Routing/UrlProviderMode.cs +++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs @@ -1,14 +1,14 @@ -namespace Umbraco.Web.Routing +namespace Umbraco.Core.Models.PublishedContent { /// - /// Specifies the type of urls that the url provider should produce, Auto is the default + /// Specifies the type of urls that the url provider should produce, Auto is the default. /// - /// - /// The Relative option can lead to invalid results when combined with hostnames, but it is the only way to reproduce - /// the true, pre-4.10, always-relative behavior of Umbraco. - /// - public enum UrlProviderMode + public enum UrlMode { + /// + /// Indicates that the url provider should do what it has been configured to do. + /// + Default = 0, /// /// Indicates that the url provider should produce relative urls exclusively. diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 0f83cf78a4..cf7df4fb86 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -193,19 +193,6 @@ namespace Umbraco.Core.Models return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } - internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) - { - switch (recycleBinId) - { - case Constants.System.RecycleBinMedia: - return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); - case Constants.System.RecycleBinContent: - return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); - default: - throw new NotSupportedException("Path access is only determined on content or media"); - } - } - /// /// Determines whether this user has access to view sensitive data /// diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs index c134748047..db8e2b20d9 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -54,6 +54,7 @@ namespace Umbraco.Core.Persistence.Factories propertyType.Alias = typeDto.Alias; propertyType.DataTypeId = typeDto.DataTypeId; + propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; propertyType.Description = typeDto.Description; propertyType.Id = typeDto.Id; propertyType.Key = typeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs b/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs index 853cd9f99e..48e7afdc7e 100644 --- a/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/AuditItemMapper.cs @@ -18,7 +18,8 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); - DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); + // we cannot map that one - because AuditType is an enum but Header is a string + //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); } } diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs index 03d82a345f..c87937e41e 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -85,6 +85,17 @@ namespace Umbraco.Core.Persistence.Querying // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've // only just discovered what it is actually doing. + // TODO + // in most cases we want to convert the value to a plain object, + // but for in some rare cases, we may want to do it differently, + // for instance a Models.AuditType (an enum) may in some cases + // need to be converted to its string value. + // but - we cannot have specific code here, really - and how would + // we configure this? is it even possible? + /* + var toString = typeof(object).GetMethod("ToString"); + var member = Expression.Call(m, toString); + */ var member = Expression.Convert(m, typeof(object)); var lambda = Expression.Lambda>(member); var getter = lambda.Compile(); diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs index 7c8a82bb85..b2dd6a3297 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs @@ -33,5 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories Direction orderDirection, AuditType[] auditTypeFilter, IQuery customFilter); + + IEnumerable Get(AuditType type, IQuery query); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs index 1486935e2a..c3d34cc3e9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -100,7 +100,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(IAuditEntry entity) { - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); var dto = AuditEntryFactory.BuildDto(entity); Database.Insert(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index cda89fd89a..c25328b10c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -74,6 +74,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); } + public IEnumerable Get(AuditType type, IQuery query) + { + var sqlClause = GetBaseQuery(false) + .Where(x => x.Header == type.ToString()); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + protected override Sql GetBaseQuery(bool isCount) { var sql = SqlContext.Sql(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs index 8df9bf686d..57d5dfa864 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs @@ -69,7 +69,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(IConsent entity) { - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); var dto = ConsentFactory.BuildDto(entity); Database.Insert(dto); @@ -80,7 +80,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistUpdatedItem(IConsent entity) { - ((EntityBase) entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ConsentFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index ccafb9f771..593f1b4f32 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -143,7 +143,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement templateDtoIx++; if (!templates.TryGetValue(allowedDto.TemplateNodeId, out var template)) continue; allowedTemplates.Add(template); - + if (allowedDto.IsDefault) defaultTemplateId = template.Id; } @@ -188,10 +188,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var groupDtos = Database.Fetch(sql1); var sql2 = Sql() - .Select(r => r.Select(x => x.DataTypeDto)) + .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) .AndSelect() .From() .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) .InnerJoin().On((pt, ct) => pt.ContentTypeId == ct.NodeId) .LeftJoin().On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) .LeftJoin().On((pt, mpt) => pt.Id == mpt.PropertyTypeId) @@ -290,6 +291,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { Description = dto.Description, DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, Id = dto.Id, Key = dto.UniqueId, Mandatory = dto.Mandatory, diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 98ddcdcb17..9d77eb0990 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -230,7 +231,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw ex; } - ((ContentType)entity).AddingEntity(); + entity.AddingEntity(); PersistNewBaseContentType(entity); PersistTemplates(entity, false); @@ -270,7 +271,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((ContentType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 591fa2b660..22c9244d8f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1013,8 +1013,9 @@ AND umbracoNode.id <> @id", if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) { var sql = Sql() - .SelectAll() + .Select(dt => dt.Select(x => x.NodeDto)) .From() + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) .Where("propertyEditorAlias = @propertyEditorAlias", new { propertyEditorAlias = propertyType.PropertyEditorAlias }) .OrderBy(typeDto => typeDto.NodeId); var datatype = Database.FirstOrDefault(sql); @@ -1022,6 +1023,7 @@ AND umbracoNode.id <> @id", if (datatype != null) { propertyType.DataTypeId = datatype.NodeId; + propertyType.DataTypeKey = datatype.NodeDto.UniqueId; } else { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs index 6dca479a92..45aa7603a3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -106,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IDataType entity) { - ((DataType)entity).AddingEntity(); + entity.AddingEntity(); //ensure a datatype has a unique name before creating it entity.Name = EnsureUniqueNodeName(entity.Name); @@ -174,7 +175,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } //Updates Modified date - ((DataType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs index be1e28fcc1..0b58663952 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -148,7 +148,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IDictionaryItem entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); foreach (var translation in entity.Translations) translation.Value = translation.Value.ToValidXmlString(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 6c08e05995..30a2927cc8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; @@ -205,7 +206,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" }; return list; @@ -260,21 +264,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { - // TODO: https://github.com/umbraco/Umbraco-CMS/issues/4234 - sort out IContent vs Content - // however, it's not just so we have access to AddingEntity - // there are tons of things at the end of the methods, that can only work with a true Content - // and basically, the repository requires a Content, not an IContent - var content = (Content)entity; + entity.AddingEntity(); - content.AddingEntity(); - var publishing = content.PublishedState == PublishedState.Publishing; + var publishing = entity.PublishedState == PublishedState.Publishing; // ensure that the default template is assigned if (entity.TemplateId.HasValue == false) entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; // sanitize names - SanitizeNames(content, publishing); + SanitizeNames(entity, publishing); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? @@ -324,11 +323,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; Database.Insert(contentVersionDto); - content.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the document version dto var documentVersionDto = dto.DocumentVersionDto; - documentVersionDto.Id = content.VersionId; + documentVersionDto.Id = entity.VersionId; if (publishing) documentVersionDto.Published = true; Database.Insert(documentVersionDto); @@ -336,62 +335,62 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // and again in case we're publishing immediately if (publishing) { - content.PublishedVersionId = content.VersionId; + entity.PublishedVersionId = entity.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.Name; + contentVersionDto.Text = entity.Name; Database.Insert(contentVersionDto); - content.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; - documentVersionDto.Id = content.VersionId; + documentVersionDto.Id = entity.VersionId; documentVersionDto.Published = false; Database.Insert(documentVersionDto); } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, content.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' - if (!publishing && content.PublishName != content.Name) + if (!publishing && entity.PublishName != entity.Name) edited = true; // persist the document dto // at that point, when publishing, the entity still has its old Published value // so we need to explicitly update the dto to persist the correct value - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) dto.Published = true; dto.NodeId = nodeDto.NodeId; - content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); //insert the schedule - PersistContentSchedule(content, false); + PersistContentSchedule(entity, false); // persist the variations - if (content.ContentType.VariesByCulture()) + if (entity.ContentType.VariesByCulture()) { // bump dates to align cultures to version if (publishing) - content.AdjustDates(contentVersionDto.VersionDate); + entity.AdjustDates(contentVersionDto.VersionDate); // names also impact 'edited' // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(content, publishing)); + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(content, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); } // refresh content - content.SetCultureEdited(editedCultures); + entity.SetCultureEdited(editedCultures); // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -399,23 +398,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // flip the entity's published property // this also flips its published state // note: what depends on variations (eg PublishNames) is managed directly by the content - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) { - content.Published = true; - content.PublishTemplateId = content.TemplateId; - content.PublisherId = content.WriterId; - content.PublishName = content.Name; - content.PublishDate = content.UpdateDate; + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; SetEntityTags(entity, _tagRepository); } - else if (content.PublishedState == PublishedState.Unpublishing) + else if (entity.PublishedState == PublishedState.Unpublishing) { - content.Published = false; - content.PublishTemplateId = null; - content.PublisherId = null; - content.PublishName = null; - content.PublishDate = null; + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; ClearEntityTags(entity, _tagRepository); } @@ -437,34 +436,33 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { - // however, it's not just so we have access to AddingEntity - // there are tons of things at the end of the methods, that can only work with a true Content - // and basically, the repository requires a Content, not an IContent - var content = (Content)entity; + var entityBase = entity as EntityBase; + var isEntityDirty = entityBase != null && entityBase.IsDirty(); // check if we need to make any database changes at all - if ((content.PublishedState == PublishedState.Published || content.PublishedState == PublishedState.Unpublished) - && !content.IsEntityDirty() && !content.IsAnyUserPropertyDirty()) + if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) + && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) return; // no change to save, do nothing, don't even update dates // whatever we do, we must check that we are saving the current version - var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == content.VersionId)).FirstOrDefault(); + var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); if (version == null || !version.Current) throw new InvalidOperationException("Cannot save a non-current version."); // update - content.UpdatingEntity(); - var publishing = content.PublishedState == PublishedState.Publishing; + entity.UpdatingEntity(); + + var publishing = entity.PublishedState == PublishedState.Publishing; // check if we need to create a new version - if (publishing && content.PublishedVersionId > 0) + if (publishing && entity.PublishedVersionId > 0) { // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); } // sanitize names - SanitizeNames(content, publishing); + SanitizeNames(entity, publishing); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? @@ -504,13 +502,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // and, if publishing, insert new content & document version dtos if (publishing) { - content.PublishedVersionId = content.VersionId; + entity.PublishedVersionId = entity.VersionId; contentVersionDto.Id = 0; // want a new id contentVersionDto.Current = true; // current version - contentVersionDto.Text = content.Name; + contentVersionDto.Text = entity.Name; Database.Insert(contentVersionDto); - content.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id documentVersionDto.Published = false; // non-published version Database.Insert(documentVersionDto); @@ -518,31 +516,31 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? content.PublishedVersionId : content.VersionId; + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); Database.Execute(deletePropertyDataSql); // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(content.ContentType.Variations, content.VersionId, publishing ? content.PublishedVersionId : 0, + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, entity.Properties, LanguageRepository, out var edited, out var editedCultures); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' - if (!publishing && content.PublishName != content.Name) + if (!publishing && entity.PublishName != entity.Name) edited = true; - if (content.ContentType.VariesByCulture()) + if (entity.ContentType.VariesByCulture()) { // bump dates to align cultures to version if (publishing) - content.AdjustDates(contentVersionDto.VersionDate); + entity.AdjustDates(contentVersionDto.VersionDate); // names also impact 'edited' // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - if (cultureInfo.Name != content.GetPublishName(cultureInfo.Culture)) + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) { edited = true; (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); @@ -560,7 +558,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(deleteContentVariations); // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); Database.Execute(deleteDocumentVariations); // TODO: NPoco InsertBulk issue? @@ -570,32 +568,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // (same in PersistNewItem above) // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(content, publishing)); + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(content, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); } // refresh content - content.SetCultureEdited(editedCultures); + entity.SetCultureEdited(editedCultures); // update the document dto // at that point, when un/publishing, the entity still has its old Published value // so we need to explicitly update the dto to persist the correct value - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) dto.Published = true; - else if (content.PublishedState == PublishedState.Unpublishing) + else if (entity.PublishedState == PublishedState.Unpublishing) dto.Published = false; - content.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); //update the schedule - if (content.IsPropertyDirty("ContentSchedule")) - PersistContentSchedule(content, true); + if (entity.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(entity, true); // if entity is publishing, update tags, else leave tags there // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) SetEntityTags(entity, _tagRepository); // trigger here, before we reset Published etc @@ -603,23 +601,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // flip the entity's published property // this also flips its published state - if (content.PublishedState == PublishedState.Publishing) + if (entity.PublishedState == PublishedState.Publishing) { - content.Published = true; - content.PublishTemplateId = content.TemplateId; - content.PublisherId = content.WriterId; - content.PublishName = content.Name; - content.PublishDate = content.UpdateDate; + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; SetEntityTags(entity, _tagRepository); } - else if (content.PublishedState == PublishedState.Unpublishing) + else if (entity.PublishedState == PublishedState.Unpublishing) { - content.Published = false; - content.PublishTemplateId = null; - content.PublisherId = null; - content.PublishName = null; - content.PublishDate = null; + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; ClearEntityTags(entity, _tagRepository); } @@ -1335,7 +1333,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities - private void SanitizeNames(Content content, bool publishing) + private void SanitizeNames(IContent content, bool publishing) { // a content item *must* have an invariant name, and invariant published name // else we just cannot write the invariant rows (node, content version...) to the database @@ -1400,7 +1398,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureVariantNamesAreUnique(Content content, bool publishing) + private void EnsureVariantNamesAreUnique(IContent content, bool publishing) { if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos.Count == 0) return; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs index 69523a860a..9aa28fb18a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DomainRepository.cs @@ -6,6 +6,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; @@ -101,7 +102,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - ((UmbracoDomain)entity).AddingEntity(); + entity.AddingEntity(); var factory = new DomainModelFactory(); var dto = factory.BuildDto(entity); @@ -120,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IDomain entity) { - ((UmbracoDomain)entity).UpdatingEntity(); + entity.UpdatingEntity(); var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", new { domainName = entity.DomainName, id = entity.Id }); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 0fa48e5521..f708590ea8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -139,7 +139,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IIdentityUserLogin entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = ExternalLoginFactory.BuildDto(entity); @@ -151,7 +151,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IIdentityUserLogin entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ExternalLoginFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 5a62c25df7..8429532b01 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - ((EntityBase) entity).AddingEntity(); + entity.AddingEntity(); // deal with entity becoming the new default entity if (entity.IsDefault) @@ -156,7 +156,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - ((EntityBase) entity).UpdatingEntity(); + entity.UpdatingEntity(); if (entity.IsDefault) { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index 565917e078..f0044e225d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMacro entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = MacroFactory.BuildDto(entity); @@ -152,7 +152,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMacro entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); ; var dto = MacroFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index c679a3cbd8..19706f0c03 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -216,7 +217,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMedia entity) { var media = (Models.Media) entity; - media.AddingEntity(); + entity.AddingEntity(); // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 281255e755..1abc75cf3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; @@ -102,7 +103,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMediaType entity) { - ((MediaType)entity).AddingEntity(); + entity.AddingEntity(); PersistNewBaseContentType(entity); @@ -114,7 +115,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((MediaType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs index ff7a79f98e..c138550de5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -91,8 +92,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMemberGroup entity) { //Save to db + entity.AddingEntity(); var group = (MemberGroup)entity; - group.AddingEntity(); var dto = MemberGroupFactory.BuildDto(group); var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index fdf75d8a4c..1605f4f672 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -6,6 +6,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -232,8 +233,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMember entity) { + if (entity.ProviderUserKey == null) + { + entity.ProviderUserKey = entity.Key; + } + entity.AddingEntity(); + var member = (Member) entity; - member.AddingEntity(); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index a32ec1b422..d96854743e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -131,12 +132,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { ValidateAlias(entity); - ((MemberType)entity).AddingEntity(); + entity.AddingEntity(); //set a default icon if one is not specified if (entity.Icon.IsNullOrWhiteSpace()) { - entity.Icon = "icon-user"; + entity.Icon = Constants.Icons.Member; } //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType @@ -165,7 +166,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ValidateAlias(entity); //Updates Modified date - ((MemberType)entity).UpdatingEntity(); + entity.UpdatingEntity(); //Look up parent to get and set the correct Path if ParentId has changed if (entity.IsPropertyDirty("ParentId")) @@ -224,6 +225,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line propertyType.DataTypeId = 0; + propertyType.DataTypeKey = default; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs index bd2580b38f..1dc7aa478d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index c5ba24f385..4b4af505b8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -134,7 +134,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IRelation entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var factory = new RelationFactory(entity.RelationType); var dto = factory.BuildDto(entity); @@ -147,7 +147,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IRelation entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var factory = new RelationFactory(entity.RelationType); var dto = factory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs index 4faf78bd0a..075d4aa769 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IRelationType entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = RelationTypeFactory.BuildDto(entity); @@ -145,7 +145,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IRelationType entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = RelationTypeFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 6b2dfddaeb..1497c2857c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -5,6 +5,7 @@ using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -96,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IServerRegistration entity) { - ((ServerRegistration)entity).AddingEntity(); + entity.AddingEntity(); var dto = ServerRegistrationFactory.BuildDto(entity); @@ -108,7 +109,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IServerRegistration entity) { - ((ServerRegistration)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = ServerRegistrationFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs index 9f27b6b9e3..99e824757d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (uniqueing) { - if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + if (name.NumPos > 0 && name.Name.InvariantStartsWith(nodeName) && name.NumVal == uniqueNumber) uniqueNumber++; else break; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs index fb836dccac..87564b9ac9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TagRepository.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistNewItem(ITag entity) { - ((EntityBase)entity).AddingEntity(); + entity.AddingEntity(); var dto = TagFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -97,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected override void PersistUpdatedItem(ITag entity) { - ((EntityBase)entity).UpdatingEntity(); + entity.UpdatingEntity(); var dto = TagFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TupleExtensions.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TupleExtensions.cs index eae9251552..c40060456c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TupleExtensions.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TupleExtensions.cs @@ -11,9 +11,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return t.Item1.Zip(t.Item2, relator); } - public static IEnumerable Map(this Tuple, List, List> t, Func relator) - { - return t.Item1.Zip(t.Item2, t.Item3, relator); - } +// public static IEnumerable Map(this Tuple, List, List> t, Func relator) +// { +// return t.Item1.Zip(t.Item2, t.Item3, relator); +// } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs index 3b247950e4..0701a0996e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -290,7 +290,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IUserGroup entity) { - ((UserGroup) entity).AddingEntity(); + entity.AddingEntity(); var userGroupDto = UserGroupFactory.BuildDto(entity); @@ -304,7 +304,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IUserGroup entity) { - ((UserGroup) entity).UpdatingEntity(); + entity.UpdatingEntity(); var userGroupDto = UserGroupFactory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 91a20c5bdd..96abc37662 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -445,7 +445,7 @@ ORDER BY colName"; return; } - ((User) entity).AddingEntity(); + entity.AddingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) @@ -495,7 +495,7 @@ ORDER BY colName"; protected override void PersistUpdatedItem(IUser entity) { // updates Modified date - ((User) entity).UpdatingEntity(); + entity.UpdatingEntity(); // ensure security stamp if missing if (entity.SecurityStamp.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs new file mode 100644 index 0000000000..bef3f42f46 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IIgnoreUserStartNodesConfig.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Marker interface for any editor configuration that supports Ignoring user start nodes + /// + internal interface IIgnoreUserStartNodesConfig + { + bool IgnoreUserStartNodes { get; set; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index f6a7cbf32f..0a9cf632bc 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.PropertyEditors /// /// The property type. /// A value indicating whether the converter supports a property type. - bool IsConverter(PublishedPropertyType propertyType); + bool IsConverter(IPublishedPropertyType propertyType); /// /// Determines whether a value is an actual value, or not a value. @@ -36,14 +36,14 @@ namespace Umbraco.Core.PropertyEditors /// The CLR type of values returned by the converter. /// Some of the CLR types may be generated, therefore this method cannot directly return /// a Type object (which may not exist yet). In which case it needs to return a ModelType instance. - Type GetPropertyValueType(PublishedPropertyType propertyType); + Type GetPropertyValueType(IPublishedPropertyType propertyType); /// /// Gets the property cache level. /// /// The property type. /// The property cache level. - PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType); + PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType); /// /// Converts a property source value to an intermediate value. @@ -64,7 +64,7 @@ namespace Umbraco.Core.PropertyEditors /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve /// white spaces. /// - object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview); + object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview); /// /// Converts a property intermediate value to an Object value. @@ -83,7 +83,7 @@ namespace Umbraco.Core.PropertyEditors /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage /// the cache levels of property values. It is not meant to be used by the converter. /// - object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); /// /// Converts a property intermediate value to an XPath value. @@ -107,6 +107,6 @@ namespace Umbraco.Core.PropertyEditors /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage /// the cache levels of property values. It is not meant to be used by the converter. /// - object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 48bfc49ed9..3b6ebc610c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.PropertyEditors /// public abstract class PropertyValueConverterBase : IPropertyValueConverter { - public virtual bool IsConverter(PublishedPropertyType propertyType) + public virtual bool IsConverter(IPublishedPropertyType propertyType) => false; public virtual bool? IsValue(object value, PropertyValueLevel level) @@ -30,19 +30,19 @@ namespace Umbraco.Core.PropertyEditors return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } - public virtual Type GetPropertyValueType(PublishedPropertyType propertyType) + public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (object); - public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public virtual object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public virtual object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source; - public virtual object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public virtual object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter; - public virtual object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public virtual object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter?.ToString() ?? string.Empty; } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs index 3d69c37b8b..dd2dfb49e7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/CheckboxListValueConverter.cs @@ -9,16 +9,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class CheckboxListValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.CheckBoxList); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { var sourceString = source?.ToString() ?? string.Empty; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index 9f260fc973..46dae3e4f0 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -8,16 +8,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class ColorPickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => UseLabel(propertyType) ? typeof(PickedColor) : typeof(string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var useLabel = UseLabel(propertyType); @@ -39,7 +39,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return ssource; } - private bool UseLabel(PublishedPropertyType propertyType) + private bool UseLabel(IPublishedPropertyType propertyType) { return ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration).UseLabel; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index ffe9feb653..0206528bf7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -8,16 +8,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class DatePickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (DateTime); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return DateTime.MinValue; @@ -39,7 +39,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // default ConvertSourceToObject just returns source ie a DateTime value - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a DateTime already return XmlConvert.ToString((DateTime) inter, XmlDateTimeSerializationMode.Unspecified); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 6f7888aee3..7d6e7c0ce9 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -7,16 +7,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class DecimalValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Decimal.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (decimal); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs index e4ef3a50a3..88061a559e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/EmailAddressValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class EmailAddressValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.EmailAddress); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 29f6de0271..b3685457ec 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -28,16 +28,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _config = config; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (JToken); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 79cb748960..6f5bd571b7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -14,19 +14,19 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public class ImageCropperValueConverter : PropertyValueConverterBase { /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ImageCropper); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (ImageCropperValue); /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs index e0abf17a7e..ca8f23bca2 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class IntegerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Integer.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (int); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source.TryConvertTo().Result; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index e04893716a..12e6238705 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -31,19 +31,19 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// /// /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor) && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json); } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (JToken); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 05a5f15aaf..84baf226cf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -16,10 +16,10 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class LabelValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); switch (valueType.ValueType) @@ -40,10 +40,10 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var valueType = ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration); switch (valueType.ValueType) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index aeacf33eef..a062561ab1 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -7,17 +7,17 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -25,13 +25,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return new HtmlString(inter == null ? string.Empty : (string) inter); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter?.ToString() ?? string.Empty; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs index bdd09ea33b..cd7f48f510 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MemberGroupPickerValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberGroupPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 1d5f0b1ca3..15e7ce4caf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -9,18 +9,18 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MultipleTextStringValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultipleTextstring.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // data is (both in database and xml): // @@ -58,7 +58,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters : values.ToArray(); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { var d = new XmlDocument(); var e = d.CreateElement("values"); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs index c9528c3e8b..b9c61bb169 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MustBeStringValueConverter.cs @@ -22,16 +22,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.MultiNodeTreePicker }; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Aliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs index b99cc7e0e3..61adc9a93e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/RadioButtonListValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class RadioButtonListValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.RadioButtonList); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var attempt = source.TryConvertTo(); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 31ab47223f..11502687b7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -18,16 +18,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => IsRangeDataType(propertyType.DataType.Id) ? typeof (Range) : typeof (decimal); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) return null; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index 9b857c2dff..b54c693c14 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -19,16 +19,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return Array.Empty(); @@ -43,7 +43,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return (string[]) source; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2368a1d034..7caa9a90cc 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -13,16 +13,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextArea }; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -30,13 +30,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter ?? string.Empty; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs index 46f660d829..9938af671d 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs @@ -10,17 +10,17 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TinyMceValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); // PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a string is: string // in the database a string is: string @@ -28,13 +28,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return source; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return new HtmlString(inter == null ? string.Empty : (string)inter); } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs index cfa247edaa..407ed13ddf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/UploadPropertyConverter.cs @@ -9,16 +9,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class UploadPropertyConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.UploadField); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { return source?.ToString() ?? ""; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 8ad09733f8..153462ccf5 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -6,16 +6,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class YesNoValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.Boolean; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (bool); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { // in xml a boolean is: string // in the database a boolean is: string "1" or "0" or empty @@ -49,7 +49,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // default ConvertSourceToObject just returns source ie a boolean value - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a boolean already return (bool)inter ? "1" : "0"; diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs new file mode 100644 index 0000000000..f220f307d6 --- /dev/null +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core +{ + public static class PublishedContentExtensions + { + private static IVariationContextAccessor VariationContextAccessor => Current.VariationContextAccessor; + + /// + /// Determines whether the content has a culture. + /// + /// Culture is case-insensitive. + public static bool HasCulture(this IPublishedContent content, string culture) + => content.Cultures.ContainsKey(culture ?? string.Empty); + + /// + /// Determines whether the content is invariant, or has a culture. + /// + /// Culture is case-insensitive. + public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) + => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? ""); + + /// + /// Filters a sequence of to return invariant items, and items that are published for the specified culture. + /// + /// The content items. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null). + internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, string culture = null) + where T : class, IPublishedContent + { + if (contents == null) throw new ArgumentNullException(nameof(contents)); + + culture = culture ?? Current.VariationContextAccessor.VariationContext?.Culture ?? ""; + + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); + } + + /// + /// Gets the name of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static string Name(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.Name : null; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Name : null; + } + + + /// + /// Gets the url segment of the content item. + /// + /// The content item. + /// The specific culture to get the url segment for. If null is used the current culture is used (Default is null). + public static string UrlSegment(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.Cultures.TryGetValue("", out var invariantInfos) ? invariantInfos.UrlSegment : null; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.UrlSegment : null; + } + + /// + /// Gets the culture date of the content item. + /// + /// The content item. + /// The specific culture to get the name for. If null is used the current culture is used (Default is null). + public static DateTime CultureDate(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture()) + return content.UpdateDate; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + // get + return culture != "" && content.Cultures.TryGetValue(culture, out var infos) ? infos.Date : DateTime.MinValue; + } + + + /// + /// Gets the children of the content item. + /// + /// The content item. + /// The specific culture to get the url children for. If null is used the current culture is used (Default is null). + /// + /// Gets children that are available for the specified culture. + /// Children are sorted by their sortOrder. + /// The '*' culture and supported and returns everything. + /// + public static IEnumerable Children(this IPublishedContent content, string culture = null) + { + // invariant has invariant value (whatever the requested culture) + if (!content.ContentType.VariesByCulture() && culture != "*") + culture = ""; + + // handle context culture for variant + if (culture == null) + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; + + var children = content.ChildrenForAllCultures; + return culture == "*" + ? children + : children.Where(x => x.IsInvariantOrHasCulture(culture)); + } + } +} diff --git a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs index 4e026490a4..de6eeb6a42 100644 --- a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs @@ -9,7 +9,14 @@ namespace Umbraco.Core public static class PublishedModelFactoryExtensions { /// - /// Executes an action with a safe live factory/ + /// Returns true if the current is an implementation of + /// + /// + /// + public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory; + + /// + /// Executes an action with a safe live factory /// /// /// If the factory is a live factory, ensures it is refreshed and locked while executing the action. @@ -20,6 +27,7 @@ namespace Umbraco.Core { lock (liveFactory.SyncRoot) { + //Call refresh on the live factory to re-compile the models liveFactory.Refresh(); action(); } diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index f32a46f3f8..1f004846d0 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -58,7 +58,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); // register our predefined validators - composition.WithCollectionBuilder() + composition.ManifestValueValidators() .Add() .Add() .Add() @@ -66,6 +66,9 @@ namespace Umbraco.Core.Runtime .Add() .Add(); + // register the manifest filter collection builder (collection is empty by default) + composition.ManifestFilters(); + // properties and parameters derive from data editors composition.WithCollectionBuilder() .Add(() => composition.TypeLoader.GetDataEditors()); diff --git a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs index b3d15bd612..a5f5efdba9 100644 --- a/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs +++ b/src/Umbraco.Core/Services/Changes/TreeChangeExtensions.cs @@ -2,9 +2,9 @@ namespace Umbraco.Core.Services.Changes { - internal static class TreeChangeExtensions + public static class TreeChangeExtensions { - public static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) + internal static TreeChange.EventArgs ToEventArgs(this IEnumerable> changes) { return new TreeChange.EventArgs(changes); } diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 1175df81dc..dfe02ba690 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -11,6 +12,42 @@ namespace Umbraco.Core.Services /// public static class ContentServiceExtensions { + #region RTE Anchor values + + private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled); + + internal static IEnumerable GetAnchorValuesFromRTEs(this IContentService contentService, int id, string culture = "*") + { + var result = new List(); + var content = contentService.GetById(id); + + foreach (var contentProperty in content.Properties) + { + if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce)) + { + var value = contentProperty.GetValue(culture)?.ToString(); + if (!string.IsNullOrEmpty(value)) + { + result.AddRange(contentService.GetAnchorValuesFromRTEContent(value)); + } + } + } + return result; + } + + + internal static IEnumerable GetAnchorValuesFromRTEContent(this IContentService contentService, string rteContent) + { + var result = new List(); + var matches = AnchorRegex.Matches(rteContent); + foreach (Match match in matches) + { + result.Add(match.Value.Split('\"')[1]); + } + return result; + } + #endregion + public static IEnumerable GetByIds(this IContentService contentService, IEnumerable ids) { var guids = new List(); diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs new file mode 100644 index 0000000000..3b72a6f258 --- /dev/null +++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Services +{ + internal static class DateTypeServiceExtensions + { + public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key) + { + if (DataTypeExtensions.IsBuildInDataType(key)) return false; //built in ones can never be ignoring start nodes + + var dataType = dataTypeService.GetDataType(key); + + if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig) + return ignoreStartNodesConfig.IgnoreUserStartNodes; + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 48e577a8f0..6f9ca58821 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -526,5 +526,6 @@ namespace Umbraco.Core.Services OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId); #endregion + } } diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 3dc530e250..bb56e110cd 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Services { + /// /// Defines the DataType Service, which is an easy access to operations involving /// diff --git a/src/Umbraco.Core/Services/Implement/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs index 46c851a789..5eb08f2dea 100644 --- a/src/Umbraco.Core/Services/Implement/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -51,8 +51,8 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.UserId == userId && x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(type, Query().Where(x => x.UserId == userId)) + : _auditRepository.Get(type, Query().Where(x => x.UserId == userId && x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } @@ -63,8 +63,8 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var result = sinceDate.HasValue == false - ? _auditRepository.Get(Query().Where(x => x.AuditType == type)) - : _auditRepository.Get(Query().Where(x => x.AuditType == type && x.CreateDate >= sinceDate.Value)); + ? _auditRepository.Get(type, Query()) + : _auditRepository.Get(type, Query().Where(x => x.CreateDate >= sinceDate.Value)); scope.Complete(); return result; } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5cc1a584b1..e49dcf4a12 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; @@ -31,6 +32,7 @@ namespace Umbraco.Core.Services.Implement private IQuery _queryNotTrashed; //TODO: The non-lazy object should be injected private readonly Lazy _propertyValidationService = new Lazy(() => new PropertyValidationService()); + #region Constructors @@ -1695,12 +1697,11 @@ namespace Umbraco.Core.Services.Implement } const int pageSize = 500; - var page = 0; var total = long.MaxValue; - while (page * pageSize < total) + while (total > 0) { //get descendants - ordered from deepest to shallowest - var descendants = GetPagedDescendants(content.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); + var descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); foreach (var c in descendants) DoDelete(c); } @@ -1926,11 +1927,10 @@ namespace Umbraco.Core.Services.Implement paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id; const int pageSize = 500; - var page = 0; var total = long.MaxValue; - while (page * pageSize < total) + while (total > 0) { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); foreach (var descendant in descendants) { moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path @@ -3026,5 +3026,8 @@ namespace Umbraco.Core.Services.Implement } #endregion + + + } } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 2ff39f7f7d..ab075c4ade 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -201,7 +201,7 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); @@ -227,13 +227,13 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback - var parent = parentId > 0 ? GetById(parentId) : null; // + locks // + locks + var parent = parentId > 0 ? GetById(parentId) : null; // + locks if (parentId > 0 && parent == null) - throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback // causes rollback + throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback var media = parentId > 0 ? new Models.Media(name, parent, mediaType) : new Models.Media(name, parentId, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -261,9 +261,9 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -645,8 +645,6 @@ namespace Umbraco.Core.Services.Implement } // poor man's validation? - // poor man's validation? - if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); @@ -934,7 +932,7 @@ namespace Umbraco.Core.Services.Implement var parent = parentId == Constants.System.Root ? null : GetById(parentId); if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) - throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback // causes rollback + throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); @@ -947,12 +945,6 @@ namespace Umbraco.Core.Services.Implement // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged - // if media was trashed, and since we're not moving to the recycle bin, - - // indicate that the trashed status should be changed to false, else just - - // leave it unchanged - var trashed = media.Trashed ? false : (bool?) null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); @@ -1042,17 +1034,11 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MediaTree); + // no idea what those events are for, keep a simplified version + // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since // each deleted items will have its own deleting/deleted events. so, files and such // are managed by Delete, and not here. - - // no idea what those events are for, keep a simplified version - // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since - // each deleted items will have its own deleting/deleted events. so, files and such - - // emptying the recycle bin means deleting whatever is in there - do it properly! - // are managed by Delete, and not here. - // no idea what those events are for, keep a simplified version var args = new RecycleBinEventArgs(nodeObjectType, evtMsgs); if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, args)) @@ -1113,11 +1099,6 @@ namespace Umbraco.Core.Services.Implement { // if the current sort order equals that of the media we don't // need to update it, so just increment the sort order and continue. - // if the current sort order equals that of the media we don't - - // else update - // need to update it, so just increment the sort order and continue. - // save if (media.SortOrder == sortOrder) { sortOrder++; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 65962dae7c..59f691251b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -13,7 +13,7 @@ true - full + portable false bin\Debug\ TRACE;DEBUG @@ -23,7 +23,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -55,6 +55,11 @@ + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + 1.3.0 @@ -216,6 +221,9 @@ + + + @@ -231,6 +239,7 @@ + @@ -246,7 +255,6 @@ - @@ -288,6 +296,11 @@ + + + + + @@ -316,6 +329,7 @@ + @@ -447,12 +461,6 @@ - - - - - - @@ -1229,4 +1237,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 166f2f1025..2b4f518259 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -13,7 +13,7 @@ true - full + portable false bin\Debug\ DEBUG;TRACE @@ -23,7 +23,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -49,6 +49,11 @@ + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj index b9a5890d57..60ef944a8c 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj @@ -14,7 +14,7 @@ true - full + portable false bin\Debug\ DEBUG;TRACE @@ -22,7 +22,7 @@ 4 - pdbonly + portable true bin\Release\ TRACE @@ -103,6 +103,11 @@ 2.8.0 + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index fbf828ad20..be160a483c 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -68,9 +68,9 @@ namespace Umbraco.Tests.Cache.PublishedCache var appCache = new DictionaryAppCache(); var domainCache = new DomainCache(ServiceContext.DomainService, DefaultCultureAccessor); var publishedShapshot = new PublishedSnapshot( - new PublishedContentCache(xmlStore, domainCache, appCache, globalSettings, new SiteDomainHelper(), umbracoContextAccessor, ContentTypesCache, null, null), + new PublishedContentCache(xmlStore, domainCache, appCache, globalSettings, ContentTypesCache, null, null), new PublishedMediaCache(xmlStore, ServiceContext.MediaService, ServiceContext.UserService, appCache, ContentTypesCache, Factory.GetInstance(), umbracoContextAccessor), - new PublishedMemberCache(null, appCache, Current.Services.MemberService, ContentTypesCache, umbracoContextAccessor), + new PublishedMemberCache(null, appCache, Current.Services.MemberService, ContentTypesCache), domainCache); var publishedSnapshotService = new Mock(); publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedShapshot); @@ -85,7 +85,7 @@ namespace Umbraco.Tests.Cache.PublishedCache globalSettings, new TestVariationContextAccessor()); - _cache = _umbracoContext.ContentCache; + _cache = _umbracoContext.Content; } [Test] diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 08eeb8ef4d..f3d9f895ef 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -323,8 +323,7 @@ namespace Umbraco.Tests.Cache.PublishedCache // no xpath null, // not from examine - false, - _umbracoContextAccessor), + false), //callback to get the children (dd, n) => children, // callback to get a property @@ -334,8 +333,7 @@ namespace Umbraco.Tests.Cache.PublishedCache // no xpath null, // not from examine - false, - _umbracoContextAccessor); + false); return dicDoc; } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/DictionaryPublishedContent.cs index d3cbf1f183..db8dc38d6d 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/DictionaryPublishedContent.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/DictionaryPublishedContent.cs @@ -8,7 +8,6 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; using Umbraco.Web.Composing; using Umbraco.Web.Models; using Umbraco.Web.PublishedCache; @@ -40,9 +39,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache IAppCache appCache, PublishedContentTypeCache contentTypeCache, XPathNavigator nav, - bool fromExamine, - IUmbracoContextAccessor umbracoContextAccessor) - :base(umbracoContextAccessor) + bool fromExamine) { if (valueDictionary == null) throw new ArgumentNullException(nameof(valueDictionary)); if (getParent == null) throw new ArgumentNullException(nameof(getParent)); @@ -158,8 +155,6 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache public override string Name => _name; - public override PublishedCultureInfo GetCulture(string culture = null) => null; - private static readonly Lazy> NoCultures = new Lazy>(() => new Dictionary()); public override IReadOnlyDictionary Cultures => NoCultures.Value; @@ -189,12 +184,14 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache public override IEnumerable Children => _getChildren.Value; + public override IEnumerable ChildrenForAllCultures => Children; + public override IPublishedProperty GetProperty(string alias) { return _getProperty(this, alias); } - public override PublishedContentType ContentType => _contentType; + public override IPublishedContentType ContentType => _contentType; private readonly List _keysAdded = new List(); private int _id; @@ -215,7 +212,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache //private Guid _version; private int _level; private readonly ICollection _properties; - private readonly PublishedContentType _contentType; + private readonly IPublishedContentType _contentType; private void ValidateAndSetProperty(IReadOnlyDictionary valueDictionary, Action setProperty, params string[] potentialKeys) { diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/DomainCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/DomainCache.cs index cde2077551..abaa239598 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/DomainCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/DomainCache.cs @@ -27,13 +27,18 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } /// - public IEnumerable GetAssigned(int contentId, bool includeWildcards) + public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) { - return _domainService.GetAssignedDomains(contentId, includeWildcards) + return _domainService.GetAssignedDomains(documentId, includeWildcards) .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)); } + /// + public bool HasAssigned(int documentId, bool includeWildcards = false) + => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + + /// public string DefaultCulture { get; } } } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 2a144f3aaa..8ce6b10983 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -9,7 +9,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; -using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -19,10 +18,8 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache { private readonly IAppCache _appCache; private readonly IGlobalSettings _globalSettings; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly RoutesCache _routesCache; private readonly IDomainCache _domainCache; - private readonly DomainHelper _domainHelper; private readonly PublishedContentTypeCache _contentTypeCache; // initialize a PublishedContentCache instance with @@ -35,8 +32,6 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache IDomainCache domainCache, // an IDomainCache implementation IAppCache appCache, // an IAppCache that should be at request-level IGlobalSettings globalSettings, - ISiteDomainHelper siteDomainHelper, - IUmbracoContextAccessor umbracoContextAccessor, PublishedContentTypeCache contentTypeCache, // a PublishedContentType cache RoutesCache routesCache, // a RoutesCache string previewToken) // a preview token string (or null if not previewing) @@ -44,11 +39,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache { _appCache = appCache; _globalSettings = globalSettings; - _umbracoContextAccessor = umbracoContextAccessor; _routesCache = routesCache; // may be null for unit-testing _contentTypeCache = contentTypeCache; _domainCache = domainCache; - _domainHelper = new DomainHelper(_domainCache, siteDomainHelper); _xmlStore = xmlStore; _xml = _xmlStore.Xml; // capture - because the cache has to remain consistent @@ -107,7 +100,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache // that would be returned - the "deepest" route - and that is the route we want to cache, *not* the // longer one - so make sure we don't cache the wrong route - var deepest = DomainHelper.ExistsDomainInPath(_domainCache.GetAll(false), content.Path, domainRootNodeId) == false; + var deepest = DomainUtilities.ExistsDomainInPath(_domainCache.GetAll(false), content.Path, domainRootNodeId) == false; if (deepest) _routesCache.Store(content.Id, route, true); // trusted route @@ -267,16 +260,16 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache // or we reach the content root, collecting urls in the way var pathParts = new List(); var n = node; - var hasDomains = _domainHelper.NodeHasDomains(n.Id); + var hasDomains = _domainCache.HasAssigned(n.Id); while (hasDomains == false && n != null) // n is null at root { // get the url - var urlName = n.UrlSegment; + var urlName = n.UrlSegment(); pathParts.Add(urlName); // move to parent node n = n.Parent; - hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id); + hasDomains = n != null && _domainCache.HasAssigned(n.Id); } // no domain, respect HideTopLevelNodeFromPath for legacy purposes @@ -320,13 +313,13 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache private IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing) { - return xmlNode == null ? null : XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache,_umbracoContextAccessor); + return xmlNode == null ? null : XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache); } private IEnumerable ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing) { return xmlNodes.Cast() - .Select(xmlNode => XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache, _umbracoContextAccessor)); + .Select(xmlNode => XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache)); } #endregion @@ -389,7 +382,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture)); } - public override IEnumerable GetAtRoot(bool preview) + public override IEnumerable GetAtRoot(bool preview, string culture = null) { return ConvertToDocuments(GetXml(preview).SelectNodes(XPathStrings.RootDocuments), preview); } @@ -539,12 +532,12 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _contentTypeCache.Get(PublishedItemType.Content, id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _contentTypeCache.Get(PublishedItemType.Content, alias); } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index 0c7ee98c6d..999d7f040d 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -105,7 +105,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache return GetUmbracoMedia(contentId) != null; } - public override IEnumerable GetAtRoot(bool preview) + public override IEnumerable GetAtRoot(bool preview, string culture = null) { var searchProvider = GetSearchProviderSafe(); @@ -612,17 +612,17 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _contentTypeCache.Get(PublishedItemType.Media, id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _contentTypeCache.Get(PublishedItemType.Media, alias); } - public override IEnumerable GetByContentType(PublishedContentType contentType) + public override IEnumerable GetByContentType(IPublishedContentType contentType) { throw new NotSupportedException(); } @@ -674,8 +674,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache _appCache, _contentTypeCache, cacheValues.XPath, // though, outside of tests, that should be null - cacheValues.FromExamine, - _umbracoContextAccessor + cacheValues.FromExamine ); return content.CreateModel(); } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMemberCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMemberCache.cs index c882488f20..19328c241e 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMemberCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMemberCache.cs @@ -6,7 +6,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Security; @@ -18,16 +17,13 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache private readonly IAppCache _requestCache; private readonly XmlStore _xmlStore; private readonly PublishedContentTypeCache _contentTypeCache; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - public PublishedMemberCache(XmlStore xmlStore, IAppCache requestCache, IMemberService memberService, - PublishedContentTypeCache contentTypeCache, IUmbracoContextAccessor umbracoContextAccessor) + public PublishedMemberCache(XmlStore xmlStore, IAppCache requestCache, IMemberService memberService, PublishedContentTypeCache contentTypeCache) { _requestCache = requestCache; _memberService = memberService; _xmlStore = xmlStore; _contentTypeCache = contentTypeCache; - _umbracoContextAccessor = umbracoContextAccessor; } public IPublishedContent GetByProviderKey(object key) @@ -44,7 +40,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var result = _memberService.GetByProviderKey(key); if (result == null) return null; var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); - return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel(); + return new PublishedMember(result, type).CreateModel(); }); } @@ -62,7 +58,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var result = _memberService.GetById(memberId); if (result == null) return null; var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); - return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel(); + return new PublishedMember(result, type).CreateModel(); }); } @@ -80,7 +76,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var result = _memberService.GetByUsername(username); if (result == null) return null; var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); - return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel(); + return new PublishedMember(result, type).CreateModel(); }); } @@ -98,14 +94,14 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var result = _memberService.GetByEmail(email); if (result == null) return null; var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId); - return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel(); + return new PublishedMember(result, type).CreateModel(); }); } public IPublishedContent GetByMember(IMember member) { var type = _contentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId); - return new PublishedMember(member, type, _umbracoContextAccessor).CreateModel(); + return new PublishedMember(member, type).CreateModel(); } public XPathNavigator CreateNavigator() @@ -138,12 +134,12 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #region Content types - public PublishedContentType GetContentType(int id) + public IPublishedContentType GetContentType(int id) { return _contentTypeCache.Get(PublishedItemType.Member, id); } - public PublishedContentType GetContentType(string alias) + public IPublishedContentType GetContentType(string alias) { return _contentTypeCache.Get(PublishedItemType.Member, alias); } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedSnapshotService.cs index 4a201ae44c..394a33d777 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedSnapshotService.cs @@ -145,9 +145,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var domainCache = new DomainCache(_domainService, _defaultCultureAccessor); return new PublishedSnapshot( - new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _siteDomainHelper,_umbracoContextAccessor, _contentTypeCache, _routesCache, previewToken), + new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _contentTypeCache, _routesCache, previewToken), new PublishedMediaCache(_xmlStore, _mediaService, _userService, _requestCache, _contentTypeCache, _entitySerializer, _umbracoContextAccessor), - new PublishedMemberCache(_xmlStore, _requestCache, _memberService, _contentTypeCache, _umbracoContextAccessor), + new PublishedMemberCache(_xmlStore, _requestCache, _memberService, _contentTypeCache), domainCache); } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedContent.cs index e1819bf0be..3697863cb4 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedContent.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedContent.cs @@ -26,23 +26,19 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache XmlNode xmlNode, bool isPreviewing, IAppCache appCache, - PublishedContentTypeCache contentTypeCache, - IUmbracoContextAccessor umbracoContextAccessor) - :base(umbracoContextAccessor) + PublishedContentTypeCache contentTypeCache) { _xmlNode = xmlNode; _isPreviewing = isPreviewing; _appCache = appCache; _contentTypeCache = contentTypeCache; - _umbracoContextAccessor = umbracoContextAccessor; } private readonly XmlNode _xmlNode; private readonly bool _isPreviewing; private readonly IAppCache _appCache; // at snapshot/request level (see PublishedContentCache) private readonly PublishedContentTypeCache _contentTypeCache; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly object _initializeLock = new object(); @@ -53,7 +49,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache private IEnumerable _children = Enumerable.Empty(); private IPublishedContent _parent; - private PublishedContentType _contentType; + private IPublishedContentType _contentType; private Dictionary _properties; private int _id; @@ -84,6 +80,8 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } + public override IEnumerable ChildrenForAllCultures => Children; + public override IPublishedProperty GetProperty(string alias) { EnsureNodeInitialized(); @@ -147,10 +145,15 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } - public override PublishedCultureInfo GetCulture(string culture = null) => null; + private Dictionary _cultures; - private static readonly Lazy> NoCultures = new Lazy>(() => new Dictionary()); - public override IReadOnlyDictionary Cultures => NoCultures.Value; + private Dictionary GetCultures() + { + EnsureNodeInitialized(); + return new Dictionary { { "", new PublishedCultureInfo("", _name, _urlName, _updateDate) } }; + } + + public override IReadOnlyDictionary Cultures => _cultures ?? (_cultures = GetCultures()); public override string WriterName { @@ -254,7 +257,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } - public override PublishedContentType ContentType + public override IPublishedContentType ContentType { get { @@ -269,7 +272,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache if (parent == null) return; if (parent.Attributes?.GetNamedItem("isDoc") != null) - _parent = Get(parent, _isPreviewing, _appCache, _contentTypeCache, _umbracoContextAccessor); + _parent = Get(parent, _isPreviewing, _appCache, _contentTypeCache); _parentInitialized = true; } @@ -308,8 +311,8 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache out int id, out Guid key, out int template, out int sortOrder, out string name, out string writerName, out string urlName, out string creatorName, out int creatorId, out int writerId, out string docTypeAlias, out int docTypeId, out string path, out DateTime createDate, out DateTime updateDate, out int level, out bool isDraft, - out PublishedContentType contentType, out Dictionary properties, - Func getPublishedContentType) + out IPublishedContentType contentType, out Dictionary properties, + Func getPublishedContentType) { //initialize the out params with defaults: writerName = null; @@ -426,7 +429,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var iterator = nav.Select(expr); _children = iterator.Cast() - .Select(n => Get(((IHasXmlNode) n).GetNode(), _isPreviewing, _appCache, _contentTypeCache, _umbracoContextAccessor)) + .Select(n => Get(((IHasXmlNode) n).GetNode(), _isPreviewing, _appCache, _contentTypeCache)) .OrderBy(x => x.SortOrder) .ToList(); @@ -446,7 +449,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache /// sure that we create only one instance of each for the duration of a request. The /// returned IPublishedContent is a model, if models are enabled. public static IPublishedContent Get(XmlNode node, bool isPreviewing, IAppCache appCache, - PublishedContentTypeCache contentTypeCache, IUmbracoContextAccessor umbracoContextAccessor) + PublishedContentTypeCache contentTypeCache) { // only 1 per request @@ -454,7 +457,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var id = attrs?.GetNamedItem("id").Value; if (id.IsNullOrWhiteSpace()) throw new InvalidOperationException("Node has no ID attribute."); var key = CacheKeyPrefix + id; // dont bother with preview, wont change during request in Xml cache - return (IPublishedContent) appCache.Get(key, () => (new XmlPublishedContent(node, isPreviewing, appCache, contentTypeCache, umbracoContextAccessor)).CreateModel()); + return (IPublishedContent) appCache.Get(key, () => (new XmlPublishedContent(node, isPreviewing, appCache, contentTypeCache)).CreateModel()); } public static void ClearRequest() diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedProperty.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedProperty.cs index 0c90c8d1ff..7d2fa74aa6 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedProperty.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedProperty.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache public override object GetXPathValue(string culture = null, string segment = null) { throw new NotImplementedException(); } - public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, XmlNode propertyXmlData) + public XmlPublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, XmlNode propertyXmlData) : this(propertyType, content, isPreviewing) { if (propertyXmlData == null) @@ -58,7 +58,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache _sourceValue = XmlHelper.GetNodeValue(propertyXmlData); } - public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, string propertyData) + public XmlPublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, string propertyData) : this(propertyType, content, isPreviewing) { if (propertyData == null) @@ -66,7 +66,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache _sourceValue = propertyData; } - public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing) + public XmlPublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { _sourceValue = string.Empty; diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index 6605bc4546..1c90f68d62 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -44,7 +44,7 @@ namespace Umbraco.Tests.Manifest new RequiredValidator(Mock.Of()), new RegexValidator(Mock.Of(), null) }; - _parser = new ManifestParser(AppCaches.Disabled, new ManifestValueValidatorCollection(validators), Mock.Of()); + _parser = new ManifestParser(AppCaches.Disabled, new ManifestValueValidatorCollection(validators), new ManifestFilterCollection(Array.Empty()), Mock.Of()); } [Test] diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index 79d383857a..e6a382692c 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -172,6 +172,48 @@ namespace Umbraco.Tests.Mapping } } + [Test] + public void EnumMap() + { + var definitions = new MapDefinitionCollection(new IMapDefinition[] + { + new MapperDefinition4(), + }); + var mapper = new UmbracoMapper(definitions); + + var thing5 = new Thing5() + { + Fruit1 = Thing5Enum.Apple, + Fruit2 = Thing5Enum.Banana, + Fruit3= Thing5Enum.Cherry + }; + + var thing6 = mapper.Map(thing5); + + Assert.IsNotNull(thing6); + Assert.AreEqual(Thing6Enum.Apple, thing6.Fruit1); + Assert.AreEqual(Thing6Enum.Banana, thing6.Fruit2); + Assert.AreEqual(Thing6Enum.Cherry, thing6.Fruit3); + } + + [Test] + public void NullPropertyMap() + { + var definitions = new MapDefinitionCollection(new IMapDefinition[] + { + new MapperDefinition5(), + }); + var mapper = new UmbracoMapper(definitions); + + var thing7 = new Thing7(); + + var thing8 = mapper.Map(thing7); + + Assert.IsNotNull(thing8); + Assert.IsNull(thing8.Things); + } + + private class Thing1 { public string Value { get; set; } @@ -188,6 +230,44 @@ namespace Umbraco.Tests.Mapping private class Thing4 { } + private class Thing5 + { + public Thing5Enum Fruit1 { get; set; } + public Thing5Enum Fruit2 { get; set; } + public Thing5Enum Fruit3 { get; set; } + } + + private enum Thing5Enum + { + Apple = 0, + Banana = 1, + Cherry = 2 + } + + private class Thing6 + { + public Thing6Enum Fruit1 { get; set; } + public Thing6Enum Fruit2 { get; set; } + public Thing6Enum Fruit3 { get; set; } + } + + private enum Thing6Enum + { + Apple = 0, + Banana = 1, + Cherry = 2 + } + + private class Thing7 + { + public IEnumerable Things { get; set; } + } + + private class Thing8 + { + public IEnumerable Things { get; set; } + } + private class MapperDefinition1 : IMapDefinition { public void DefineMaps(UmbracoMapper mapper) @@ -224,5 +304,41 @@ namespace Umbraco.Tests.Mapping mapper.Define(); } } + + private class MapperDefinition4 : IMapDefinition + { + public void DefineMaps(UmbracoMapper mapper) + { + mapper.Define((source, context) => new Thing6(), Map); + mapper.Define( + (source, context) => (Thing6Enum)source); + } + + private void Map(Thing5 source, Thing6 target, MapperContext context) + { + target.Fruit1 = context.Map(source.Fruit1); + target.Fruit2 = context.Map(source.Fruit2); + target.Fruit3 = context.Map(source.Fruit3); + } + } + + private class MapperDefinition5 : IMapDefinition + { + public void DefineMaps(UmbracoMapper mapper) + { + mapper.Define((source, context) => new Thing2(), Map1); + mapper.Define((source, context) => new Thing8(), Map2); + } + + private void Map1(Thing1 source, Thing2 target, MapperContext context) + { + target.Value = source.Value; + } + + private void Map2(Thing7 source, Thing8 target, MapperContext context) + { + target.Things = context.Map>(source.Things); + } + } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs index 3c23223c9f..582e5a4815 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -36,6 +36,8 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.IsTrue(result > 0, "Expected >0 but was " + result); } + + [Test] public void OrderByTest() { @@ -75,6 +77,7 @@ namespace Umbraco.Tests.Persistence.Repositories [TestCase(0, "Alpha", "Alpha (3)")] [TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)" [TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id + [TestCase(0, "alpha", "alpha (3)")] [TestCase(0, "", " (1)")] [TestCase(0, null, " (1)")] public void Test(int nodeId, string nodeName, string expected) @@ -95,5 +98,22 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName)); } + + [Test] + [Explicit("This test fails! We need to fix up the logic")] + public void TestMany() + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Test" }, + new SimilarNodeName { Id = 3, Name = "Test (1)" }, + new SimilarNodeName { Id = 4, Name = "Test (2)" }, + new SimilarNodeName { Id = 22, Name = "Test (1) (1)" }, + }; + + //fixme - this will yield "Test (2)" which is already in use + Assert.AreEqual("Test (3)", SimilarNodeName.GetUniqueName(names, 0, "Test")); + } } } diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 809354bd74..44879eae2f 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -327,7 +327,7 @@ AnotherContentFinder public void Resolves_RestExtensions() { var types = _manager.ResolveRestExtensions(); - Assert.AreEqual(3, types.Count()); + Assert.AreEqual(2, types.Count()); } [Test] diff --git a/src/Umbraco.Tests/Published/ConvertersTests.cs b/src/Umbraco.Tests/Published/ConvertersTests.cs index 0fce8ebfc3..671129848c 100644 --- a/src/Umbraco.Tests/Published/ConvertersTests.cs +++ b/src/Umbraco.Tests/Published/ConvertersTests.cs @@ -4,16 +4,14 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Tests.Components; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; using Umbraco.Web.PublishedCache; @@ -37,10 +35,12 @@ namespace Umbraco.Tests.Published var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", new[] + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - contentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1); + } + + var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "1234" } }, false); @@ -70,22 +70,22 @@ namespace Umbraco.Tests.Published } } - public bool IsConverter(PublishedPropertyType propertyType) + public bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); - public Type GetPropertyValueType(PublishedPropertyType propertyType) + public Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (int); - public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; - public object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => int.TryParse(source as string, out int i) ? i : 0; - public object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => (int) inter; - public object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => ((int) inter).ToString(); } @@ -115,15 +115,17 @@ namespace Umbraco.Tests.Published var contentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", new[] + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - contentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1); + } + + var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "1234" } }, false); - var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", Array.Empty()); - var cnt1 = new TestPublishedContent(cntType1, 1234, Guid.NewGuid(), new Dictionary(), false); + var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", t => Enumerable.Empty()); + var cnt1 = new SolidPublishedContent(cntType1) { Id = 1234 }; cacheContent[cnt1.Id] = cnt1; Assert.AreSame(cnt1, element1.Value("prop1")); @@ -143,26 +145,26 @@ namespace Umbraco.Tests.Published public bool? IsValue(object value, PropertyValueLevel level) => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); - public bool IsConverter(PublishedPropertyType propertyType) + public bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); - public Type GetPropertyValueType(PublishedPropertyType propertyType) + public Type GetPropertyValueType(IPublishedPropertyType propertyType) // the first version would be the "generic" version, but say we want to be more precise // and return: whatever Clr type is generated for content type with alias "cnt1" -- which // we cannot really typeof() at the moment because it has not been generated, hence ModelType. // => typeof (IPublishedContent); => ModelType.For("cnt1"); - public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => _cacheLevel; - public object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => int.TryParse(source as string, out int i) ? i : -1; - public object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int) inter); - public object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => ((int) inter).ToString(); } @@ -208,30 +210,28 @@ namespace Umbraco.Tests.Published var contentTypeFactory = new PublishedContentTypeFactory(factory, converters, dataTypeService); - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", new[] + IEnumerable CreatePropertyTypes(IPublishedContentType contentType, int i) { - contentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return contentTypeFactory.CreatePropertyType(contentType, "prop" + i, i); + } - var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", new[] - { - contentTypeFactory.CreatePropertyType("prop2", 2), - }); - - var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", new[] - { - contentTypeFactory.CreatePropertyType("prop1", 1), - }); - - var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", new[] - { - contentTypeFactory.CreatePropertyType("prop2", 2), - }); + var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", t => CreatePropertyTypes(t, 1)); + var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", t => CreatePropertyTypes(t, 2)); + var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", t => CreatePropertyTypes(t, 1)); + var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", t => CreatePropertyTypes(t, 2)); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "val1" } }, false); var element2 = new PublishedElement(elementType2, Guid.NewGuid(), new Dictionary { { "prop2", "1003" } }, false); - var cnt1 = new TestPublishedContent(contentType1, 1003, Guid.NewGuid(), new Dictionary { { "prop1", "val1" } }, false); - var cnt2 = new TestPublishedContent(contentType2, 1004, Guid.NewGuid(), new Dictionary { { "prop2", "1003" } }, false); + var cnt1 = new SolidPublishedContent(contentType1) + { + Id = 1003, + Properties = new[] { new SolidPublishedProperty { Alias = "prop1", SolidHasValue = true, SolidValue = "val1" } } + }; + var cnt2 = new SolidPublishedContent(contentType1) + { + Id = 1004, + Properties = new[] { new SolidPublishedProperty { Alias = "prop2", SolidHasValue = true, SolidValue = "1003" } } + }; cacheContent[cnt1.Id] = cnt1.CreateModel(); cacheContent[cnt2.Id] = cnt2.CreateModel(); @@ -267,13 +267,13 @@ namespace Umbraco.Tests.Published public class SimpleConverter3A : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == "Umbraco.Void"; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; } @@ -286,22 +286,22 @@ namespace Umbraco.Tests.Published _publishedSnapshotAccessor = publishedSnapshotAccessor; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == "Umbraco.Void.2"; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var s = source as string; return s?.Split(',').Select(int.Parse).ToArray() ?? Array.Empty(); } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { return ((int[]) inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1) _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray(); } diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 8f3b9a1df9..9385b8955a 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.Models; @@ -23,7 +24,7 @@ namespace Umbraco.Tests.Published [TestFixture] public class NestedContentTests { - private (PublishedContentType, PublishedContentType) CreateContentTypes() + private (IPublishedContentType, IPublishedContentType) CreateContentTypes() { Current.Reset(); @@ -125,13 +126,24 @@ namespace Umbraco.Tests.Published var factory = new PublishedContentTypeFactory(publishedModelFactory.Object, converters, dataTypeService); - var propertyType1 = factory.CreatePropertyType("property1", 1); - var propertyType2 = factory.CreatePropertyType("property2", 2); - var propertyTypeN1 = factory.CreatePropertyType("propertyN1", 3); + IEnumerable CreatePropertyTypes1(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "property1", 1); + } - var contentType1 = factory.CreateContentType(1, "content1", new[] { propertyType1 }); - var contentType2 = factory.CreateContentType(2, "content2", new[] { propertyType2 }); - var contentTypeN1 = factory.CreateContentType(2, "contentN1", new[] { propertyTypeN1 }, isElement: true); + IEnumerable CreatePropertyTypes2(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "property2", 2); + } + + IEnumerable CreatePropertyTypesN1(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "propertyN1", 3); + } + + var contentType1 = factory.CreateContentType(1, "content1", CreatePropertyTypes1); + var contentType2 = factory.CreateContentType(2, "content2", CreatePropertyTypes2); + var contentTypeN1 = factory.CreateContentType(2, "contentN1", CreatePropertyTypesN1, isElement: true); // mocked content cache returns content types contentCache @@ -156,12 +168,16 @@ namespace Umbraco.Tests.Published var key = Guid.NewGuid(); var keyA = Guid.NewGuid(); - var content = new TestPublishedContent(contentType1, key, new[] + var content = new SolidPublishedContent(contentType1) { - new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[ + Key = key, + Properties = new [] + { + new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[ {{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }} ]") - }, Mock.Of()); + } + }; var value = content.Value("property1"); // nested single converter returns proper TestModel value @@ -183,14 +199,17 @@ namespace Umbraco.Tests.Published var key = Guid.NewGuid(); var keyA = Guid.NewGuid(); var keyB = Guid.NewGuid(); - var content = new TestPublishedContent(contentType2, key, new[] + var content = new SolidPublishedContent(contentType2) { - new TestPublishedProperty(contentType2.GetPropertyType("property2"), $@"[ + Key = key, + Properties = new[] + { + new TestPublishedProperty(contentType2.GetPropertyType("property2"), $@"[ {{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }}, {{ ""key"": ""{keyB}"", ""propertyN1"": ""bar"", ""ncContentTypeAlias"": ""contentN1"" }} ]") - }, - Mock.Of()); + } + }; var value = content.Value("property2"); // nested many converter returns proper IEnumerable value @@ -219,14 +238,14 @@ namespace Umbraco.Tests.Published private readonly bool _hasValue; private IPublishedElement _owner; - public TestPublishedProperty(PublishedPropertyType propertyType, object source) + public TestPublishedProperty(IPublishedPropertyType propertyType, object source) : base(propertyType, PropertyCacheLevel.Element) // initial reference cache level always is .Content { _sourceValue = source; _hasValue = source != null && (!(source is string ssource) || !string.IsNullOrWhiteSpace(ssource)); } - public TestPublishedProperty(PublishedPropertyType propertyType, IPublishedElement element, bool preview, PropertyCacheLevel referenceCacheLevel, object source) + public TestPublishedProperty(IPublishedPropertyType propertyType, IPublishedElement element, bool preview, PropertyCacheLevel referenceCacheLevel, object source) : base(propertyType, referenceCacheLevel) { _sourceValue = source; @@ -247,52 +266,5 @@ namespace Umbraco.Tests.Published public override object GetValue(string culture = null, string segment = null) => PropertyType.ConvertInterToObject(_owner, ReferenceCacheLevel, InterValue, _preview); public override object GetXPathValue(string culture = null, string segment = null) => throw new WontImplementException(); } - - class TestPublishedContent : PublishedContentBase - { - public TestPublishedContent(PublishedContentType contentType, Guid key, IEnumerable properties, IUmbracoContextAccessor umbracoContextAccessor): base(umbracoContextAccessor) - { - ContentType = contentType; - Key = key; - var propertiesA = properties.ToArray(); - Properties = propertiesA; - foreach (var property in propertiesA) - property.SetOwner(this); - } - - // ReSharper disable UnassignedGetOnlyAutoProperty - public override PublishedItemType ItemType { get; } - public override bool IsDraft(string culture = null) => false; - public override bool IsPublished(string culture = null) => true; - public override IPublishedContent Parent { get; } - public override IEnumerable Children { get; } - public override PublishedContentType ContentType { get; } - // ReSharper restore UnassignedGetOnlyAutoProperty - - // ReSharper disable UnassignedGetOnlyAutoProperty - public override int Id { get; } - public override int? TemplateId { get; } - public override int SortOrder { get; } - public override string Name { get; } - public override PublishedCultureInfo GetCulture(string culture = ".") => throw new NotSupportedException(); - public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); - public override string UrlSegment { get; } - public override string WriterName { get; } - public override string CreatorName { get; } - public override int WriterId { get; } - public override int CreatorId { get; } - public override string Path { get; } - public override DateTime CreateDate { get; } - public override DateTime UpdateDate { get; } - public override int Level { get; } - public override Guid Key { get; } - // ReSharper restore UnassignedGetOnlyAutoProperty - - public override IEnumerable Properties { get; } - public override IPublishedProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - } } } diff --git a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs index 76fdd81ec2..9db539d142 100644 --- a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs +++ b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs @@ -35,10 +35,13 @@ namespace Umbraco.Tests.Published new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", new[] + + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - publishedContentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); + } + + var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); // PublishedElementPropertyBase.GetCacheLevels: // @@ -113,10 +116,13 @@ namespace Umbraco.Tests.Published new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", new[] + + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - publishedContentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); + } + + var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); var elementsCache = new FastDictionaryAppCache(); var snapshotCache = new FastDictionaryAppCache(); @@ -187,10 +193,13 @@ namespace Umbraco.Tests.Published new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", new[] + + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - publishedContentTypeFactory.CreatePropertyType("prop1", 1), - }); + yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); + } + + var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); Assert.Throws(() => { @@ -213,28 +222,28 @@ namespace Umbraco.Tests.Published public bool? IsValue(object value, PropertyValueLevel level) => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); - public bool IsConverter(PublishedPropertyType propertyType) + public bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); - public Type GetPropertyValueType(PublishedPropertyType propertyType) + public Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(int); - public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => _cacheLevel; - public object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { SourceConverts++; return int.TryParse(source as string, out int i) ? i : 0; } - public object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { InterConverts++; return (int) inter; } - public object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => ((int) inter).ToString(); } } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs new file mode 100644 index 0000000000..f3a520ead1 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -0,0 +1,851 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Core.Strings; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web; +using Umbraco.Web.Cache; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + public class NuCacheChildrenTests + { + private IPublishedSnapshotService _snapshotService; + private IVariationContextAccessor _variationAccesor; + private IPublishedSnapshotAccessor _snapshotAccessor; + private ContentType _contentTypeInvariant; + private ContentType _contentTypeVariant; + private TestDataSource _source; + + private void Init(IEnumerable kits) + { + Current.Reset(); + + var factory = Mock.Of(); + Current.Factory = factory; + + var configs = new Configs(); + Mock.Get(factory).Setup(x => x.GetInstance(typeof(Configs))).Returns(configs); + var globalSettings = new GlobalSettings(); + configs.Add(SettingsForTests.GenerateMockUmbracoSettings); + configs.Add(() => globalSettings); + + var publishedModelFactory = new NoopPublishedModelFactory(); + Mock.Get(factory).Setup(x => x.GetInstance(typeof(IPublishedModelFactory))).Returns(publishedModelFactory); + + var runtime = Mock.Of(); + Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); + + // create data types, property types and content types + var dataType = new DataType(new VoidEditor("Editor", Mock.Of())) { Id = 3 }; + + var dataTypes = new[] + { + dataType + }; + + var propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Nothing }; + _contentTypeInvariant = new ContentType(-1) { Id = 2, Alias = "itype", Variations = ContentVariation.Nothing }; + _contentTypeInvariant.AddPropertyType(propertyType); + + propertyType = new PropertyType("Umbraco.Void.Editor", ValueStorageType.Nvarchar) { Alias = "prop", DataTypeId = 3, Variations = ContentVariation.Culture }; + _contentTypeVariant = new ContentType(-1) { Id = 3, Alias = "vtype", Variations = ContentVariation.Culture }; + _contentTypeVariant.AddPropertyType(propertyType); + + var contentTypes = new[] + { + _contentTypeInvariant, + _contentTypeVariant + }; + + var contentTypeService = Mock.Of(); + Mock.Get(contentTypeService).Setup(x => x.GetAll()).Returns(contentTypes); + Mock.Get(contentTypeService).Setup(x => x.GetAll(It.IsAny())).Returns(contentTypes); + + var contentTypeServiceBaseFactory = Mock.Of(); + Mock.Get(contentTypeServiceBaseFactory).Setup(x => x.For(It.IsAny())).Returns(contentTypeService); + + var dataTypeService = Mock.Of(); + Mock.Get(dataTypeService).Setup(x => x.GetAll()).Returns(dataTypes); + + // create a service context + var serviceContext = ServiceContext.CreatePartial( + dataTypeService: dataTypeService, + memberTypeService: Mock.Of(), + memberService: Mock.Of(), + contentTypeService: contentTypeService, + localizationService: Mock.Of() + ); + + // create a scope provider + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + + // create a published content type factory + var contentTypeFactory = new PublishedContentTypeFactory( + Mock.Of(), + new PropertyValueConverterCollection(Array.Empty()), + dataTypeService); + + // create accessors + _variationAccesor = new TestVariationContextAccessor(); + _snapshotAccessor = new TestPublishedSnapshotAccessor(); + + // create a data source for NuCache + _source = new TestDataSource(kits); + + // at last, create the complete NuCache snapshot service! + var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + _snapshotService = new PublishedSnapshotService(options, + null, + runtime, + serviceContext, + contentTypeFactory, + null, + _snapshotAccessor, + _variationAccesor, + Mock.Of(), + scopeProvider, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestDefaultCultureAccessor(), + _source, + globalSettings, + Mock.Of(), + Mock.Of(), + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + + // invariant is the current default + _variationAccesor.VariationContext = new VariationContext(); + + Mock.Get(factory).Setup(x => x.GetInstance(typeof(IVariationContextAccessor))).Returns(_variationAccesor); + } + + private IEnumerable GetInvariantKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + ContentNodeKit CreateKit(int id, int parentId, int sortOrder) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit + { + ContentTypeId = _contentTypeInvariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + } + + yield return CreateKit(1, -1, 1); + yield return CreateKit(2, -1, 2); + yield return CreateKit(3, -1, 3); + + yield return CreateKit(4, 1, 1); + yield return CreateKit(5, 1, 2); + yield return CreateKit(6, 1, 3); + + yield return CreateKit(7, 2, 3); + yield return CreateKit(8, 2, 2); + yield return CreateKit(9, 2, 1); + + yield return CreateKit(10, 3, 1); + + yield return CreateKit(11, 4, 1); + yield return CreateKit(12, 4, 2); + } + + private IEnumerable GetVariantKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + Dictionary GetCultureInfos(int id, DateTime now) + { + var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; + + var infos = new Dictionary(); + if (en.Contains(id)) + infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; + if (fr.Contains(id)) + infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; + return infos; + } + + ContentNodeKit CreateKit(int id, int parentId, int sortOrder) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit + { + ContentTypeId = _contentTypeVariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = GetCultureInfos(id, now) + } + }; + } + + yield return CreateKit(1, -1, 1); + yield return CreateKit(2, -1, 2); + yield return CreateKit(3, -1, 3); + + yield return CreateKit(4, 1, 1); + yield return CreateKit(5, 1, 2); + yield return CreateKit(6, 1, 3); + + yield return CreateKit(7, 2, 3); + yield return CreateKit(8, 2, 2); + yield return CreateKit(9, 2, 1); + + yield return CreateKit(10, 3, 1); + + yield return CreateKit(11, 4, 1); + yield return CreateKit(12, 4, 2); + } + + private IEnumerable GetVariantWithDraftKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + Dictionary GetCultureInfos(int id, DateTime now) + { + var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; + + var infos = new Dictionary(); + if (en.Contains(id)) + infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; + if (fr.Contains(id)) + infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; + return infos; + } + + ContentNodeKit CreateKit(int id, int parentId, int sortOrder) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + ContentData CreateContentData(bool published) => new ContentData + { + Name = "N" + id, + Published = published, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = GetCultureInfos(id, now) + }; + + var withDraft = id%2==0; + var withPublished = !withDraft; + + return new ContentNodeKit + { + ContentTypeId = _contentTypeVariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = withDraft ? CreateContentData(false) : null, + PublishedData = withPublished ? CreateContentData(true) : null + }; + } + + yield return CreateKit(1, -1, 1); + yield return CreateKit(2, -1, 2); + yield return CreateKit(3, -1, 3); + + yield return CreateKit(4, 1, 1); + yield return CreateKit(5, 1, 2); + yield return CreateKit(6, 1, 3); + + yield return CreateKit(7, 2, 3); + yield return CreateKit(8, 2, 2); + yield return CreateKit(9, 2, 1); + + yield return CreateKit(10, 3, 1); + + yield return CreateKit(11, 4, 1); + yield return CreateKit(12, 4, 2); + } + + [Test] + public void EmptyTest() + { + Init(Enumerable.Empty()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + Assert.AreEqual(0, documents.Length); + } + + [Test] + public void ChildrenTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + + documents = snapshot.Content.GetById(3).Children().ToArray(); + AssertDocuments(documents, "N10"); + + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N11", "N12"); + + documents = snapshot.Content.GetById(10).Children().ToArray(); + AssertDocuments(documents); + } + + [Test] + public void ParentTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + Assert.IsNull(snapshot.Content.GetById(1).Parent); + Assert.IsNull(snapshot.Content.GetById(2).Parent); + Assert.IsNull(snapshot.Content.GetById(3).Parent); + + Assert.AreEqual(1, snapshot.Content.GetById(4).Parent?.Id); + Assert.AreEqual(1, snapshot.Content.GetById(5).Parent?.Id); + Assert.AreEqual(1, snapshot.Content.GetById(6).Parent?.Id); + + Assert.AreEqual(2, snapshot.Content.GetById(7).Parent?.Id); + Assert.AreEqual(2, snapshot.Content.GetById(8).Parent?.Id); + Assert.AreEqual(2, snapshot.Content.GetById(9).Parent?.Id); + + Assert.AreEqual(3, snapshot.Content.GetById(10).Parent?.Id); + + Assert.AreEqual(4, snapshot.Content.GetById(11).Parent?.Id); + Assert.AreEqual(4, snapshot.Content.GetById(12).Parent?.Id); + } + + [Test] + public void MoveToRootTest() + { + Init(GetInvariantKits()); + + // get snapshot + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + // do some changes + var kit = _source.Kits[10]; + _source.Kits[10] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), 1, "-1,10", 4, -1, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + // notify + _snapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(10, TreeChangeTypes.RefreshBranch) }, out _, out _); + + // changes that *I* make are immediately visible on the current snapshot + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3", "N10"); + + documents = snapshot.Content.GetById(3).Children().ToArray(); + AssertDocuments(documents); + + Assert.IsNull(snapshot.Content.GetById(10).Parent); + } + + [Test] + public void MoveFromRootTest() + { + Init(GetInvariantKits()); + + // get snapshot + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + // do some changes + var kit = _source.Kits[1]; + _source.Kits[1] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), 1, "-1,3,10,1", 1, 10, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + // notify + _snapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.RefreshBranch) }, out _, out _); + + // changes that *I* make are immediately visible on the current snapshot + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N2", "N3"); + + documents = snapshot.Content.GetById(10).Children().ToArray(); + AssertDocuments(documents, "N1"); + + Assert.AreEqual(10, snapshot.Content.GetById(1).Parent?.Id); + } + + [Test] + public void ReOrderTest() + { + Init(GetInvariantKits()); + + // get snapshot + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + // do some changes + var kit = _source.Kits[7]; + _source.Kits[7] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 1, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + kit = _source.Kits[8]; + _source.Kits[8] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 3, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + kit = _source.Kits[9]; + _source.Kits[9] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 2, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + // notify + _snapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(kit.Node.ParentContentId, TreeChangeTypes.RefreshBranch) }, out _, out _); + + // changes that *I* make are immediately visible on the current snapshot + var documents = snapshot.Content.GetById(kit.Node.ParentContentId).Children().ToArray(); + AssertDocuments(documents, "N7", "N9", "N8"); + } + + [Test] + public void MoveTest() + { + Init(GetInvariantKits()); + + // get snapshot + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + // do some changes + var kit = _source.Kits[4]; + _source.Kits[4] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 2, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + kit = _source.Kits[5]; + _source.Kits[5] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 3, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + kit = _source.Kits[6]; + _source.Kits[6] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, kit.Node.Path, 4, kit.Node.ParentContentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + kit = _source.Kits[7]; + _source.Kits[7] = new ContentNodeKit + { + ContentTypeId = 2, + Node = new ContentNode(kit.Node.Id, Guid.NewGuid(), kit.Node.Level, "-1,1,7", 1, 1, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData + { + Name = kit.PublishedData.Name, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = DateTime.Now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; + + // notify + _snapshotService.Notify(new[] + { + // removal must come first + new ContentCacheRefresher.JsonPayload(2, TreeChangeTypes.RefreshBranch), + new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.RefreshBranch) + }, out _, out _); + + // changes that *I* make are immediately visible on the current snapshot + var documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N7", "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8"); + + Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); + } + + [Test] + public void VariantChildrenTest() + { + Init(GetVariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + _variationAccesor.VariationContext = new VariationContext("en-US"); + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4-en-US", "N5-en-US", "N6-en-US"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9-en-US", "N8-en-US", "N7-en-US"); + + documents = snapshot.Content.GetById(3).Children().ToArray(); + AssertDocuments(documents, "N10-en-US"); + + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N11-en-US", "N12-en-US"); + + documents = snapshot.Content.GetById(10).Children().ToArray(); + AssertDocuments(documents); + + + _variationAccesor.VariationContext = new VariationContext("fr-FR"); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-fr-FR", "N3-fr-FR"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4-fr-FR", "N6-fr-FR"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9-fr-FR", "N7-fr-FR"); + + documents = snapshot.Content.GetById(3).Children().ToArray(); + AssertDocuments(documents, "N10-fr-FR"); + + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N12-fr-FR"); + + documents = snapshot.Content.GetById(10).Children().ToArray(); + AssertDocuments(documents); + + documents = snapshot.Content.GetById(1).Children("*").ToArray(); + AssertDocuments(documents, "N4-fr-FR", null, "N6-fr-FR"); + AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); + + documents = snapshot.Content.GetById(1).Children("en-US").ToArray(); + AssertDocuments(documents, "N4-fr-FR", null, "N6-fr-FR"); + AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); + + documents = snapshot.Content.GetById(1).ChildrenForAllCultures.ToArray(); + AssertDocuments(documents, "N4-fr-FR", null, "N6-fr-FR"); + AssertDocuments("en-US", documents, "N4-en-US", "N5-en-US", "N6-en-US"); + + + documents = snapshot.Content.GetAtRoot("*").ToArray(); + AssertDocuments(documents, "N1-fr-FR", null, "N3-fr-FR"); + + documents = snapshot.Content.GetById(1).DescendantsOrSelf().ToArray(); + AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", "N12-fr-FR", "N6-fr-FR"); + + documents = snapshot.Content.GetById(1).DescendantsOrSelf("*").ToArray(); + AssertDocuments(documents, "N1-fr-FR", "N4-fr-FR", null /*11*/, "N12-fr-FR", null /*5*/, "N6-fr-FR"); + } + + [Test] + public void RemoveTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(3, TreeChangeTypes.Remove), // remove last + new ContentCacheRefresher.JsonPayload(5, TreeChangeTypes.Remove), // remove middle + new ContentCacheRefresher.JsonPayload(9, TreeChangeTypes.Remove), // remove first + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.Remove), // remove first + new ContentCacheRefresher.JsonPayload(8, TreeChangeTypes.Remove), // remove + new ContentCacheRefresher.JsonPayload(7, TreeChangeTypes.Remove), // remove + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N2"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents); + } + + [Test] + public void UpdateTest() + { + Init(GetInvariantKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + + // notify + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(1, TreeChangeTypes.RefreshBranch), + new ContentCacheRefresher.JsonPayload(2, TreeChangeTypes.RefreshNode), + }, out _, out _); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1", "N2", "N3"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N5", "N6"); + + documents = snapshot.Content.GetById(2).Children().ToArray(); + AssertDocuments(documents, "N9", "N8", "N7"); + } + + [Test] + public void AtRootTest() + { + Init(GetVariantWithDraftKits()); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + _variationAccesor.VariationContext = new VariationContext("en-US"); + + // N2 is draft only + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-en-US", /*"N2-en-US",*/ "N3-en-US"); + + documents = snapshot.Content.GetAtRoot(true).ToArray(); + AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); + } + + private void AssertDocuments(IPublishedContent[] documents, params string[] names) + { + Assert.AreEqual(names.Length, documents.Length); + for (var i = 0; i < names.Length; i++) + Assert.AreEqual(names[i], documents[i].Name); + } + + private void AssertDocuments(string culture, IPublishedContent[] documents, params string[] names) + { + Assert.AreEqual(names.Length, documents.Length); + for (var i = 0; i < names.Length; i++) + Assert.AreEqual(names[i], documents[i].Name(culture)); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index ad2b0220bb..b66404c954 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -24,7 +24,6 @@ using Umbraco.Web.Cache; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.PublishedCache.NuCache.DataSource; -using Umbraco.Web.Routing; namespace Umbraco.Tests.PublishedContent { @@ -39,10 +38,18 @@ namespace Umbraco.Tests.PublishedContent private void Init() { Current.Reset(); - Current.UnlockConfigs(); - Current.Configs.Add(SettingsForTests.GenerateMockUmbracoSettings); - Current.Configs.Add(() => new GlobalSettings()); - var globalSettings = Current.Configs.Global(); + + var factory = Mock.Of(); + Current.Factory = factory; + + var configs = new Configs(); + Mock.Get(factory).Setup(x => x.GetInstance(typeof(Configs))).Returns(configs); + var globalSettings = new GlobalSettings(); + configs.Add(SettingsForTests.GenerateMockUmbracoSettings); + configs.Add(() => globalSettings); + + var publishedModelFactory = new NoopPublishedModelFactory(); + Mock.Get(factory).Setup(x => x.GetInstance(typeof(IPublishedModelFactory))).Returns(publishedModelFactory); // create a content node kit var kit = new ContentNodeKit @@ -171,7 +178,6 @@ namespace Umbraco.Tests.PublishedContent null, new TestPublishedSnapshotAccessor(), _variationAccesor, - Mock.Of(), Mock.Of(), scopeProvider, Mock.Of(), @@ -180,13 +186,14 @@ namespace Umbraco.Tests.PublishedContent new TestDefaultCultureAccessor(), dataSource, globalSettings, - new SiteDomainHelper(), Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); + + Mock.Get(factory).Setup(x => x.GetInstance(typeof(IVariationContextAccessor))).Returns(_variationAccesor); } [Test] @@ -202,36 +209,34 @@ namespace Umbraco.Tests.PublishedContent var publishedContent = snapshot.Content.GetById(1); Assert.IsNotNull(publishedContent); - Assert.AreEqual("It Works1!", publishedContent.Name); Assert.AreEqual("val1", publishedContent.Value("prop")); Assert.AreEqual("val-fr1", publishedContent.Value("prop", "fr-FR")); Assert.AreEqual("val-uk1", publishedContent.Value("prop", "en-UK")); - Assert.AreEqual("name-fr1", publishedContent.GetCulture("fr-FR").Name); - Assert.AreEqual("name-uk1", publishedContent.GetCulture("en-UK").Name); + Assert.IsNull(publishedContent.Name()); // no invariant name for varying content + Assert.AreEqual("name-fr1", publishedContent.Name("fr-FR")); + Assert.AreEqual("name-uk1", publishedContent.Name("en-UK")); var draftContent = snapshot.Content.GetById(true, 1); - Assert.AreEqual("It Works2!", draftContent.Name); Assert.AreEqual("val2", draftContent.Value("prop")); Assert.AreEqual("val-fr2", draftContent.Value("prop", "fr-FR")); Assert.AreEqual("val-uk2", draftContent.Value("prop", "en-UK")); - Assert.AreEqual("name-fr2", draftContent.GetCulture("fr-FR").Name); - Assert.AreEqual("name-uk2", draftContent.GetCulture("en-UK").Name); + Assert.IsNull(draftContent.Name()); // no invariant name for varying content + Assert.AreEqual("name-fr2", draftContent.Name("fr-FR")); + Assert.AreEqual("name-uk2", draftContent.Name("en-UK")); // now french is default _variationAccesor.VariationContext = new VariationContext("fr-FR"); Assert.AreEqual("val-fr1", publishedContent.Value("prop")); - Assert.AreEqual("name-fr1", publishedContent.GetCulture().Name); - Assert.AreEqual("name-fr1", publishedContent.Name); - Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.GetCulture().Date); + Assert.AreEqual("name-fr1", publishedContent.Name()); + Assert.AreEqual(new DateTime(2018, 01, 01, 01, 00, 00), publishedContent.CultureDate()); // now uk is default _variationAccesor.VariationContext = new VariationContext("en-UK"); Assert.AreEqual("val-uk1", publishedContent.Value("prop")); - Assert.AreEqual("name-uk1", publishedContent.GetCulture().Name); - Assert.AreEqual("name-uk1", publishedContent.Name); - Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.GetCulture().Date); + Assert.AreEqual("name-uk1", publishedContent.Name()); + Assert.AreEqual(new DateTime(2018, 01, 02, 01, 00, 00), publishedContent.CultureDate()); // invariant needs to be retrieved explicitly, when it's not default Assert.AreEqual("val1", publishedContent.Value("prop", culture: "")); @@ -251,7 +256,7 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(ContentVariation.Nothing, againContent.ContentType.GetPropertyType("prop").Variations); // now, "no culture" means "invariant" - Assert.AreEqual("It Works1!", againContent.Name); + Assert.AreEqual("It Works1!", againContent.Name()); Assert.AreEqual("val1", againContent.Value("prop")); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 283ed1edd9..cc455b8e5d 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -10,9 +10,9 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Web; +using PublishedContentExtensions = Umbraco.Web.PublishedContentExtensions; namespace Umbraco.Tests.PublishedContent { @@ -97,7 +97,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetContent(true, 1); //change a doc type alias - var c = (TestPublishedContent)doc.Children.ElementAt(0); + var c = (SolidPublishedContent)doc.Children.ElementAt(0); c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var dt = doc.ChildrenAsTable(Current.Services, "Child"); @@ -129,7 +129,8 @@ namespace Umbraco.Tests.PublishedContent var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); var contentTypeAlias = createChildren ? "Parent" : "Child"; - var d = new TestPublishedContent + var contentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var d = new SolidPublishedContent(contentType) { CreateDate = DateTime.Now, CreatorId = 1, @@ -140,7 +141,7 @@ namespace Umbraco.Tests.PublishedContent UpdateDate = DateTime.Now, Path = "-1,3", UrlSegment = "home-page", - Name = "Page" + Guid.NewGuid().ToString(), + Name = "Page" + Guid.NewGuid(), Version = Guid.NewGuid(), WriterId = 1, WriterName = "Shannon", @@ -175,75 +176,7 @@ namespace Umbraco.Tests.PublishedContent new RawValueProperty(factory.CreatePropertyType("property3", 1), d, "value" + (indexVals + 2))); } - d.ContentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); return d; } - - // note - could probably rewrite those tests using SolidPublishedContentCache - // l8tr... - private class TestPublishedContent : IPublishedContent - { - public string Url { get; set; } - public string GetUrl(string culture = null) => throw new NotSupportedException(); - - public PublishedItemType ItemType { get; set; } - - IPublishedContent IPublishedContent.Parent - { - get { return Parent; } - } - - IEnumerable IPublishedContent.Children - { - get { return Children; } - } - - public IPublishedContent Parent { get; set; } - public int Id { get; set; } - public Guid Key { get; set; } - public int? TemplateId { get; set; } - public int SortOrder { get; set; } - public string Name { get; set; } - public PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); - public IReadOnlyDictionary Cultures => throw new NotSupportedException(); - public string UrlSegment { get; set; } - public string WriterName { get; set; } - public string CreatorName { get; set; } - public int WriterId { get; set; } - public int CreatorId { get; set; } - public string Path { get; set; } - public DateTime CreateDate { get; set; } - public DateTime UpdateDate { get; set; } - public Guid Version { get; set; } - public int Level { get; set; } - public bool IsDraft(string culture = null) => false; - public bool IsPublished(string culture = null) => true; - - public IEnumerable Properties { get; set; } - - public IEnumerable Children { get; set; } - - public IPublishedProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - - public IPublishedProperty GetProperty(string alias, bool recurse) - { - var property = GetProperty(alias); - if (recurse == false) return property; - - IPublishedContent content = this; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content == null ? null : content.GetProperty(alias); - } - - return property; - } - - public PublishedContentType ContentType { get; set; } - } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs index acbad002ee..ced4c012f8 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs @@ -27,7 +27,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = _ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.Content.GetById(1100); Assert.That(publishedContent.IsDocumentType("inherited", false)); } @@ -36,7 +36,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = _ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.Content.GetById(1100); Assert.That(publishedContent.IsDocumentType("base", false), Is.False); } @@ -45,7 +45,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = _ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.Content.GetById(1100); Assert.That(publishedContent.IsDocumentType("inherited", true)); } @@ -55,7 +55,7 @@ namespace Umbraco.Tests.PublishedContent InitializeInheritedContentTypes(); ContentTypesCache.GetPublishedContentTypeByAlias = null; - var publishedContent = _ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.Content.GetById(1100); Assert.That(publishedContent.IsDocumentType("base", true)); } @@ -64,7 +64,7 @@ namespace Umbraco.Tests.PublishedContent { InitializeInheritedContentTypes(); - var publishedContent = _ctx.ContentCache.GetById(1100); + var publishedContent = _ctx.Content.GetById(1100); Assert.That(publishedContent.IsDocumentType("invalidbase", true), Is.False); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 108bfb9f18..62447742ff 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -65,17 +65,22 @@ namespace Umbraco.Tests.PublishedContent var welcome2Type = factory.CreatePropertyType("welcomeText2", 1, variations: ContentVariation.Culture); var nopropType = factory.CreatePropertyType("noprop", 1, variations: ContentVariation.Culture); - var props = new[] - { - prop1Type, - welcomeType, - welcome2Type, - nopropType - }; - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); + IEnumerable CreatePropertyTypes1(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "prop1", 1, variations: ContentVariation.Culture); + yield return factory.CreatePropertyType(contentType, "welcomeText", 1, variations: ContentVariation.Culture); + yield return factory.CreatePropertyType(contentType, "welcomeText2", 1, variations: ContentVariation.Culture); + yield return factory.CreatePropertyType(contentType, "noprop", 1, variations: ContentVariation.Culture); + } - var prop3Type = factory.CreatePropertyType("prop3", 1, variations: ContentVariation.Culture); - var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty(), new[] { prop3Type }); + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes1); + + IEnumerable CreatePropertyTypes2(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "prop3", 1, variations: ContentVariation.Culture); + } + + var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty(), CreatePropertyTypes2); var prop1 = new SolidPublishedPropertyWithLanguageVariants { @@ -150,7 +155,7 @@ namespace Umbraco.Tests.PublishedContent var prop4 = new SolidPublishedPropertyWithLanguageVariants { Alias = "prop3", - PropertyType = prop3Type + PropertyType = contentType2.GetPropertyType("prop3") }; prop4.SetSourceValue("en-US", "Oxxo", true); prop4.SetValue("en-US", "Oxxo", true); @@ -186,7 +191,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_For_Populated_Requested_Language() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "en-US"); Assert.AreEqual("Welcome", value); } @@ -194,7 +199,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "de"); Assert.AreEqual("Willkommen", value); } @@ -202,7 +207,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "fr"); Assert.IsNull(value); } @@ -210,7 +215,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "es"); Assert.IsNull(value); } @@ -218,7 +223,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "es", fallback: Fallback.ToLanguage); Assert.AreEqual("Welcome", value); } @@ -226,7 +231,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors)); Assert.AreEqual("Welcome", value); } @@ -234,7 +239,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); var value = content.Value("welcomeText", "no", fallback: Fallback.ToLanguage); Assert.IsNull(value); } @@ -242,7 +247,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_Get_Content_Recursively_Unless_Requested() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); var value = content.Value("welcomeText2"); Assert.IsNull(value); } @@ -250,7 +255,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_Recursively() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); var value = content.Value("welcomeText2", fallback: Fallback.ToAncestors); Assert.AreEqual("Welcome", value); } @@ -258,7 +263,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_Get_Content_Recursively_Unless_Requested2() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First().Children().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First().Children.First(); Assert.IsNull(content.GetProperty("welcomeText2")); var value = content.Value("welcomeText2"); Assert.IsNull(value); @@ -267,7 +272,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_Recursively2() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First().Children().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First().Children.First(); Assert.IsNull(content.GetProperty("welcomeText2")); var value = content.Value("welcomeText2", fallback: Fallback.ToAncestors); Assert.AreEqual("Welcome", value); @@ -276,7 +281,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_Recursively3() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First().Children().First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First().Children.First(); Assert.IsNull(content.GetProperty("noprop")); var value = content.Value("noprop", fallback: Fallback.ToAncestors); // property has no value but we still get the value (ie, the converter would do something) @@ -287,7 +292,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_With_Recursive_Priority() { Current.VariationContextAccessor.VariationContext = new VariationContext("nl"); - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); var value = content.Value("welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language)); @@ -298,7 +303,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Get_Content_With_Fallback_Language_Priority() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); var value = content.Value("welcomeText", "nl", fallback: Fallback.ToLanguage); // No Dutch value is directly assigned. Check has fallen back to English value from language variant. @@ -308,14 +313,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Throws_For_Non_Supported_Fallback() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); Assert.Throws(() => content.Value("welcomeText", "nl", fallback: Fallback.To(999))); } [Test] public void Can_Fallback_To_Default_Value() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); // no Dutch value is assigned, so getting null var value = content.Value("welcomeText", "nl"); @@ -333,7 +338,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Can_Have_Custom_Default_Value() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First().Children.First(); + var content = Current.UmbracoContext.Content.GetAtRoot().First().Children.First(); // HACK: the value, pretend the converter would return something var prop = content.GetProperty("welcomeText") as SolidPublishedPropertyWithLanguageVariants; diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index b2f1f311c3..440474ae74 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; @@ -15,15 +16,16 @@ namespace Umbraco.Tests.PublishedContent { internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) { - var props = new[] - { - factory.CreatePropertyType("prop1", 1), - }; - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); - var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), props); - var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), props); + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "prop1", 1); + } - cache.Add(new SolidPublishedContent(contentType1) + var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes); + var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), CreatePropertyTypes); + var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), CreatePropertyTypes); + + var content = new SolidPublishedContent(contentType1) { Id = 1, SortOrder = 0, @@ -35,18 +37,19 @@ namespace Umbraco.Tests.PublishedContent ParentId = -1, ChildIds = new int[] { }, Properties = new Collection + { + new SolidPublishedProperty { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" } - }); + } + }; + cache.Add(content); - cache.Add(new SolidPublishedContent(contentType2) + content = new SolidPublishedContent(contentType2) { Id = 2, SortOrder = 1, @@ -58,18 +61,19 @@ namespace Umbraco.Tests.PublishedContent ParentId = -1, ChildIds = new int[] { }, Properties = new Collection + { + new SolidPublishedProperty { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" } - }); + } + }; + cache.Add(content); - cache.Add(new SolidPublishedContent(contentType2Sub) + content = new SolidPublishedContent(contentType2Sub) { Id = 3, SortOrder = 2, @@ -90,20 +94,21 @@ namespace Umbraco.Tests.PublishedContent SolidSourceValue = "1234" } } - }); + }; + cache.Add(content); } [Test] public void First() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot().First(); - Assert.AreEqual("Content 1", content.Name); + var content = Current.UmbracoContext.Content.GetAtRoot().First(); + Assert.AreEqual("Content 1", content.Name()); } [Test] public void Distinct() { - var items = Current.UmbracoContext.ContentCache.GetAtRoot() + var items = Current.UmbracoContext.Content.GetAtRoot() .Distinct() .Distinct() .ToIndexedArray(); @@ -127,7 +132,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void OfType1() { - var items = Current.UmbracoContext.ContentCache.GetAtRoot() + var items = Current.UmbracoContext.Content.GetAtRoot() .OfType() .Distinct() .ToIndexedArray(); @@ -138,7 +143,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void OfType2() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot() + var content = Current.UmbracoContext.Content.GetAtRoot() .OfType() .Distinct() .ToIndexedArray(); @@ -149,7 +154,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void OfType() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot() + var content = Current.UmbracoContext.Content.GetAtRoot() .OfType() .First(x => x.Prop1 == 1234); Assert.AreEqual("Content 2", content.Name); @@ -159,7 +164,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Position() { - var items = Current.UmbracoContext.ContentCache.GetAtRoot() + var items = Current.UmbracoContext.Content.GetAtRoot() .Where(x => x.Value("prop1") == 1234) .ToIndexedArray(); @@ -174,7 +179,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Issue() { - var content = Current.UmbracoContext.ContentCache.GetAtRoot() + var content = Current.UmbracoContext.Content.GetAtRoot() .Distinct() .OfType(); @@ -182,12 +187,12 @@ namespace Umbraco.Tests.PublishedContent var first = where.First(); Assert.AreEqual(1234, first.Prop1); - var content2 = Current.UmbracoContext.ContentCache.GetAtRoot() + var content2 = Current.UmbracoContext.Content.GetAtRoot() .OfType() .First(x => x.Prop1 == 1234); Assert.AreEqual(1234, content2.Prop1); - var content3 = Current.UmbracoContext.ContentCache.GetAtRoot() + var content3 = Current.UmbracoContext.Content.GetAtRoot() .OfType() .First(); Assert.AreEqual(1234, content3.Prop1); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index c5bcd29589..6907f6936c 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -1,4 +1,5 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; @@ -41,13 +42,12 @@ namespace Umbraco.Tests.PublishedContent var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); - // need to specify a custom callback for unit tests - var propertyTypes = new[] + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { - // AutoPublishedContentType will auto-generate other properties - publishedContentTypeFactory.CreatePropertyType("content", 1), - }; - var type = new AutoPublishedContentType(0, "anything", propertyTypes); + yield return publishedContentTypeFactory.CreatePropertyType(contentType, "content", 1); + } + + var type = new AutoPublishedContentType(0, "anything", CreatePropertyTypes); ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; var umbracoContext = GetUmbracoContext("/test"); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index de641a99a2..f54971a197 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Web; @@ -62,18 +63,19 @@ namespace Umbraco.Tests.PublishedContent // when they are requested, but we must declare those that we // explicitely want to be here... - var propertyTypes = new[] + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) { // AutoPublishedContentType will auto-generate other properties - factory.CreatePropertyType("umbracoNaviHide", 1001), - factory.CreatePropertyType("selectedNodes", 1), - factory.CreatePropertyType("umbracoUrlAlias", 1), - factory.CreatePropertyType("content", 1002), - factory.CreatePropertyType("testRecursive", 1), - }; + yield return factory.CreatePropertyType(contentType, "umbracoNaviHide", 1001); + yield return factory.CreatePropertyType(contentType, "selectedNodes", 1); + yield return factory.CreatePropertyType(contentType, "umbracoUrlAlias", 1); + yield return factory.CreatePropertyType(contentType, "content", 1002); + yield return factory.CreatePropertyType(contentType, "testRecursive", 1); + } + var compositionAliases = new[] { "MyCompositionAlias" }; - var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, propertyTypes); - var homeType = new AutoPublishedContentType(0, "home", compositionAliases, propertyTypes); + var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, CreatePropertyTypes); + var homeType = new AutoPublishedContentType(0, "home", compositionAliases, CreatePropertyTypes); ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType; } @@ -140,7 +142,7 @@ namespace Umbraco.Tests.PublishedContent internal IPublishedContent GetNode(int id) { var ctx = GetUmbracoContext("/test"); - var doc = ctx.ContentCache.GetById(id); + var doc = ctx.Content.GetById(id); Assert.IsNotNull(doc); return doc; } @@ -149,16 +151,16 @@ namespace Umbraco.Tests.PublishedContent public void GetNodeByIds() { var ctx = GetUmbracoContext("/test"); - var contentById = ctx.ContentCache.GetById(1173); + var contentById = ctx.Content.GetById(1173); Assert.IsNotNull(contentById); - var contentByGuid = ctx.ContentCache.GetById(_node1173Guid); + var contentByGuid = ctx.Content.GetById(_node1173Guid); Assert.IsNotNull(contentByGuid); Assert.AreEqual(contentById.Id, contentByGuid.Id); Assert.AreEqual(contentById.Key, contentByGuid.Key); - contentById = ctx.ContentCache.GetById(666); + contentById = ctx.Content.GetById(666); Assert.IsNull(contentById); - contentByGuid = ctx.ContentCache.GetById(Guid.NewGuid()); + contentByGuid = ctx.Content.GetById(Guid.NewGuid()); Assert.IsNull(contentByGuid); } @@ -167,7 +169,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - var items = doc.Children.Where(x => x.IsVisible()).ToIndexedArray(); + var items = doc.Children().Where(x => x.IsVisible()).ToIndexedArray(); foreach (var item in items) { @@ -188,7 +190,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetNode(1173); var items = doc - .Children + .Children() .Where(x => x.IsVisible()) .ToIndexedArray(); @@ -243,7 +245,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetNode(1173); var ct = doc.ContentType; - var items = doc.Children + var items = doc.Children() .Select(x => x.CreateModel()) // linq, returns IEnumerable // only way around this is to make sure every IEnumerable extension @@ -275,7 +277,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - var items = doc.Children.Take(4).ToIndexedArray(); + var items = doc.Children().Take(4).ToIndexedArray(); foreach (var item in items) { @@ -295,7 +297,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - foreach (var d in doc.Children.Skip(1).ToIndexedArray()) + foreach (var d in doc.Children().Skip(1).ToIndexedArray()) { if (d.Content.Id != 1176) { @@ -313,7 +315,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - var items = doc.Children + var items = doc.Children() .Concat(new[] { GetNode(1175), GetNode(4444) }) .ToIndexedArray(); @@ -398,7 +400,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetNode(1046); - var found1 = doc.Children.GroupBy(x => x.ContentType.Alias).ToArray(); + var found1 = doc.Children().GroupBy(x => x.ContentType.Alias).ToArray(); Assert.AreEqual(2, found1.Length); Assert.AreEqual(2, found1.Single(x => x.Key.ToString() == "Home").Count()); @@ -419,8 +421,8 @@ namespace Umbraco.Tests.PublishedContent var doc = GetNode(1046); - var found1 = doc.Children.Where(x => x.ContentType.Alias == "CustomDocument"); - var found2 = doc.Children.Where(x => x.ContentType.Alias == "Home"); + var found1 = doc.Children().Where(x => x.ContentType.Alias == "CustomDocument"); + var found2 = doc.Children().Where(x => x.ContentType.Alias == "Home"); Assert.AreEqual(1, found1.Count()); Assert.AreEqual(2, found2.Count()); @@ -431,7 +433,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - var ordered = doc.Children.OrderBy(x => x.UpdateDate); + var ordered = doc.Children().OrderBy(x => x.UpdateDate); var correctOrder = new[] { 1178, 1177, 1174, 1176 }; for (var i = 0; i < correctOrder.Length; i++) @@ -819,7 +821,7 @@ namespace Umbraco.Tests.PublishedContent var level1_2 = GetNode(1175); var level1_3 = GetNode(4444); - _publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot()).Returns(new []{root}); + _publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot(It.IsAny())).Returns(new []{root}); CollectionAssertAreEqual(new []{root}, root.SiblingsAndSelf()); @@ -858,7 +860,7 @@ namespace Umbraco.Tests.PublishedContent var level1_2 = GetNode(1175); var level1_3 = GetNode(4444); - _publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot()).Returns(new []{root}); + _publishedSnapshotAccessorMock.Setup(x => x.PublishedSnapshot.Content.GetAtRoot(It.IsAny())).Returns(new []{root}); CollectionAssertAreEqual(new IPublishedContent[0], root.Siblings()); @@ -887,8 +889,13 @@ namespace Umbraco.Tests.PublishedContent { var factory = Factory.GetInstance() as PublishedContentTypeFactory; - var pt = factory.CreatePropertyType("detached", 1003); - var ct = factory.CreateContentType(0, "alias", new[] { pt }); + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "detached", 1003); + } + + var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes); + var pt = ct.GetPropertyType("detached"); var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548); Assert.IsInstanceOf(prop.GetValue()); Assert.AreEqual(5548, prop.GetValue()); @@ -906,16 +913,20 @@ namespace Umbraco.Tests.PublishedContent { var factory = Factory.GetInstance() as PublishedContentTypeFactory; - var pt1 = factory.CreatePropertyType("legend", 1004); - var pt2 = factory.CreatePropertyType("image", 1005); - var pt3 = factory.CreatePropertyType("size", 1003); + IEnumerable CreatePropertyTypes(IPublishedContentType contentType) + { + yield return factory.CreatePropertyType(contentType, "legend", 1004); + yield return factory.CreatePropertyType(contentType, "image", 1005); + yield return factory.CreatePropertyType(contentType, "size", 1003); + } + const string val1 = "boom bam"; const int val2 = 0; const int val3 = 666; var guid = Guid.NewGuid(); - var ct = factory.CreateContentType(0, "alias", new[] { pt1, pt2, pt3 }); + var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes); var c = new ImageWithLegendModel(ct, guid, new Dictionary { @@ -930,7 +941,7 @@ namespace Umbraco.Tests.PublishedContent class ImageWithLegendModel : PublishedElement { - public ImageWithLegendModel(PublishedContentType contentType, Guid fragmentKey, Dictionary values, bool previewing) + public ImageWithLegendModel(IPublishedContentType contentType, Guid fragmentKey, Dictionary values, bool previewing) : base(contentType, fragmentKey, values, previewing) { } diff --git a/src/Umbraco.Tests/PublishedContent/RootNodeTests.cs b/src/Umbraco.Tests/PublishedContent/RootNodeTests.cs index 77eab1dbb7..4aad3d0acb 100644 --- a/src/Umbraco.Tests/PublishedContent/RootNodeTests.cs +++ b/src/Umbraco.Tests/PublishedContent/RootNodeTests.cs @@ -13,17 +13,17 @@ namespace Umbraco.Tests.PublishedContent var ctx = GetUmbracoContext("/test"); // there is no content node with ID -1 - var content = ctx.ContentCache.GetById(-1); + var content = ctx.Content.GetById(-1); Assert.IsNull(content); // content at root has null parent - content = ctx.ContentCache.GetById(1046); + content = ctx.Content.GetById(1046); Assert.IsNotNull(content); Assert.AreEqual(1, content.Level); Assert.IsNull(content.Parent); // non-existing content is null - content = ctx.ContentCache.GetById(666); + content = ctx.Content.GetById(666); Assert.IsNull(content); } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 9828a14597..860b9b9179 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -100,7 +100,7 @@ namespace Umbraco.Tests.PublishedContent return _content.ContainsKey(contentId); } - public override IEnumerable GetAtRoot(bool preview) + public override IEnumerable GetAtRoot(bool preview, string culture = null) { return _content.Values.Where(x => x.Parent == null); } @@ -140,27 +140,27 @@ namespace Umbraco.Tests.PublishedContent return _content.Count > 0; } - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { throw new NotImplementedException(); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { throw new NotImplementedException(); } - public override IEnumerable GetByContentType(PublishedContentType contentType) + public override IEnumerable GetByContentType(IPublishedContentType contentType) { throw new NotImplementedException(); } } - class SolidPublishedContent : IPublishedContent + internal class SolidPublishedContent : IPublishedContent { #region Constructor - public SolidPublishedContent(PublishedContentType contentType) + public SolidPublishedContent(IPublishedContentType contentType) { // initialize boring stuff TemplateId = 0; @@ -176,13 +176,19 @@ namespace Umbraco.Tests.PublishedContent #region Content + private Dictionary _cultures; + + private Dictionary GetCultures() + { + return new Dictionary { { "", new PublishedCultureInfo("", Name, UrlSegment, UpdateDate) } }; + } + public int Id { get; set; } public Guid Key { get; set; } public int? TemplateId { get; set; } public int SortOrder { get; set; } public string Name { get; set; } - public PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); - public IReadOnlyDictionary Cultures => throw new NotSupportedException(); + public IReadOnlyDictionary Cultures => _cultures ?? (_cultures = GetCultures()); public string UrlSegment { get; set; } public string WriterName { get; set; } public string CreatorName { get; set; } @@ -194,9 +200,8 @@ namespace Umbraco.Tests.PublishedContent public Guid Version { get; set; } public int Level { get; set; } public string Url { get; set; } - public string GetUrl(string culture = null) => throw new NotSupportedException(); - public PublishedItemType ItemType { get { return PublishedItemType.Content; } } + public PublishedItemType ItemType => PublishedItemType.Content; public bool IsDraft(string culture = null) => false; public bool IsPublished(string culture = null) => true; @@ -209,12 +214,13 @@ namespace Umbraco.Tests.PublishedContent public IPublishedContent Parent { get; set; } public IEnumerable Children { get; set; } + public IEnumerable ChildrenForAllCultures => Children; #endregion #region ContentType - public PublishedContentType ContentType { get; private set; } + public IPublishedContentType ContentType { get; set; } #endregion @@ -236,7 +242,7 @@ namespace Umbraco.Tests.PublishedContent while (content != null && (property == null || property.HasValue() == false)) { content = content.Parent; - property = content == null ? null : content.GetProperty(alias); + property = content?.GetProperty(alias); } return property; @@ -256,7 +262,7 @@ namespace Umbraco.Tests.PublishedContent internal class SolidPublishedProperty : IPublishedProperty { - public PublishedPropertyType PropertyType { get; set; } + public IPublishedPropertyType PropertyType { get; set; } public string Alias { get; set; } public object SolidSourceValue { get; set; } public object SolidValue { get; set; } @@ -400,7 +406,7 @@ namespace Umbraco.Tests.PublishedContent class AutoPublishedContentType : PublishedContentType { - private static readonly PublishedPropertyType Default; + private static readonly IPublishedPropertyType Default; static AutoPublishedContentType() { @@ -415,11 +421,19 @@ namespace Umbraco.Tests.PublishedContent : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) { } + public AutoPublishedContentType(int id, string alias, Func> propertyTypes) + : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) + { } + public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) { } - public override PublishedPropertyType GetPropertyType(string alias) + public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes) + : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) + { } + + public override IPublishedPropertyType GetPropertyType(string alias) { var propertyType = base.GetPropertyType(alias); return propertyType ?? Default; diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs index 5de99fdd38..5af48e64b1 100644 --- a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using Moq; using Newtonsoft.Json; @@ -11,7 +10,6 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; using Umbraco.Web.Routing; @@ -45,7 +43,7 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); - var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto); Assert.AreEqual(expected, resolvedUrl); } @@ -56,15 +54,15 @@ namespace Umbraco.Tests.Routing const string expected = "/media/rfeiw584/test.jpg"; var configuration = new ImageCropperConfiguration(); - var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue + var imageCropperValue = new ImageCropperValue { Src = expected - }); + }; var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration); - var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto); Assert.AreEqual(expected, resolvedUrl); } @@ -78,7 +76,7 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null); - var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Absolute, null, null); + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Absolute); Assert.AreEqual(expected, resolvedUrl); } @@ -89,7 +87,7 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null); - var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "test", UrlProviderMode.Absolute, null, null); + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Absolute, propertyAlias: "test"); Assert.AreEqual(string.Empty, resolvedUrl); } @@ -116,19 +114,32 @@ namespace Umbraco.Tests.Routing var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}}; - var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, "da", null); + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto, "da"); Assert.AreEqual(daMediaUrl, resolvedUrl); } - private static TestPublishedContent CreatePublishedContent(string propertyEditorAlias, object propertyValue, object dataTypeConfiguration) + private static IPublishedContent CreatePublishedContent(string propertyEditorAlias, object propertyValue, object dataTypeConfiguration) { var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new[] {umbracoFilePropertyType}, ContentVariation.Nothing); - return new TestPublishedContent(contentType, 1234, Guid.NewGuid(), - new Dictionary {{"umbracoFile", propertyValue } }, false); + return new SolidPublishedContent(contentType) + { + Id = 1234, + Key = Guid.NewGuid(), + Properties = new[] + { + new SolidPublishedProperty + { + Alias = "umbracoFile", + SolidValue = propertyValue, + SolidHasValue = true, + PropertyType = umbracoFilePropertyType + } + } + }; } private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation) diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index f1f38504f1..935417c510 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext("~/dummy-page", template.Id, routeData); var publishedRouter = CreatePublishedRouter(); var frequest = publishedRouter.CreateRequest(umbracoContext); - frequest.PublishedContent = umbracoContext.ContentCache.GetById(1174); + frequest.PublishedContent = umbracoContext.Content.GetById(1174); frequest.TemplateModel = template; var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); @@ -136,7 +136,7 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext("~/dummy-page", template.Id, routeData, true); var publishedRouter = CreatePublishedRouter(); var frequest = publishedRouter.CreateRequest(umbracoContext); - frequest.PublishedContent = umbracoContext.ContentCache.GetById(1172); + frequest.PublishedContent = umbracoContext.Content.GetById(1172); frequest.TemplateModel = template; var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index 236c198b3a..02aa95cd2e 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -10,8 +10,8 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.LegacyXmlPublishedCache; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -78,7 +78,7 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(randomSample.Value, result); } - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); Assert.AreEqual(8, cachedRoutes.Count); @@ -142,7 +142,7 @@ namespace Umbraco.Tests.Routing new DefaultUrlProvider(umbracoSettings.RequestHandler, Logger, globalSettings.Object, new SiteDomainHelper()) }, globalSettings: globalSettings.Object); - + var result = umbracoContext.UrlProvider.GetUrl(nodeId); Assert.AreEqual(niceUrlMatch, result); } @@ -159,7 +159,7 @@ namespace Umbraco.Tests.Routing var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); - var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) @@ -185,7 +185,7 @@ namespace Umbraco.Tests.Routing snapshotService: snapshotService.Object); //even though we are asking for a specific culture URL, there are no domains assigned so all that can be returned is a normal relative url. - var url = umbracoContext.UrlProvider.GetUrl(1234, "fr-FR"); + var url = umbracoContext.UrlProvider.GetUrl(1234, culture: "fr-FR"); Assert.AreEqual("/home/test-fr/", url); } @@ -204,7 +204,7 @@ namespace Umbraco.Tests.Routing var umbracoSettings = Current.Configs.Settings(); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); - var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) @@ -239,7 +239,7 @@ namespace Umbraco.Tests.Routing snapshotService: snapshotService.Object); - var url = umbracoContext.UrlProvider.GetUrl(1234, "fr-FR"); + var url = umbracoContext.UrlProvider.GetUrl(1234, culture: "fr-FR"); Assert.AreEqual("/home/test-fr/", url); } @@ -258,7 +258,7 @@ namespace Umbraco.Tests.Routing var umbracoSettings = Current.Configs.Settings(); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); - var publishedContent = new TestPublishedContent(contentType, 1234, Guid.NewGuid(), new Dictionary(), false); + var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); publishedContentCache.Setup(x => x.GetRouteById(1234, "fr-FR")) @@ -293,7 +293,7 @@ namespace Umbraco.Tests.Routing snapshotService: snapshotService.Object); - var url = umbracoContext.UrlProvider.GetUrl(1234, "fr-FR"); + var url = umbracoContext.UrlProvider.GetUrl(1234, culture: "fr-FR"); //the current uri is not the culture specific domain we want, so the result is an absolute path to the culture specific domain Assert.AreEqual("http://example.fr/home/test-fr/", url); @@ -314,7 +314,7 @@ namespace Umbraco.Tests.Routing Assert.AreEqual("/home/sub1/custom-sub-1/", umbracoContext.UrlProvider.GetUrl(1177)); - umbracoContext.UrlProvider.Mode = UrlProviderMode.Absolute; + umbracoContext.UrlProvider.Mode = UrlMode.Absolute; Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", umbracoContext.UrlProvider.GetUrl(1177)); } @@ -332,10 +332,10 @@ namespace Umbraco.Tests.Routing }, globalSettings: globalSettings.Object); //mock the Umbraco settings that we need - + Assert.AreEqual("#", umbracoContext.UrlProvider.GetUrl(999999)); - umbracoContext.UrlProvider.Mode = UrlProviderMode.Absolute; + umbracoContext.UrlProvider.Mode = UrlMode.Absolute; Assert.AreEqual("#", umbracoContext.UrlProvider.GetUrl(999999)); } diff --git a/src/Umbraco.Tests/Routing/UrlRoutesTests.cs b/src/Umbraco.Tests/Routing/UrlRoutesTests.cs index 2e944211ca..4b8d708df6 100644 --- a/src/Umbraco.Tests/Routing/UrlRoutesTests.cs +++ b/src/Umbraco.Tests/Routing/UrlRoutesTests.cs @@ -200,7 +200,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(hide); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings: globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var route = cache.GetRouteById(false, id); @@ -224,7 +224,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(hide); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings: globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var route = cache.GetRouteById(false, id); @@ -238,7 +238,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(false); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var route = cache.GetRouteById(false, 1000); @@ -269,7 +269,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(hide); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! @@ -300,7 +300,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(hide); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); const bool preview = false; // make sure we don't cache - but HOW? should be some sort of switch?! @@ -323,7 +323,7 @@ DetermineRouteById(id): globalSettings.Setup(x => x.HideTopLevelNodeFromPath).Returns(false); var umbracoContext = GetUmbracoContext("/test", 0, globalSettings:globalSettings.Object); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var content = cache.GetByRoute(false, "/a/b/c"); diff --git a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs index c01ee83d6a..0a34fb8041 100644 --- a/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/UrlsProviderWithDomainsTests.cs @@ -7,6 +7,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; @@ -188,7 +189,8 @@ namespace Umbraco.Tests.Routing SetDomains1(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current: currentUri); + var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; + var result = umbracoContext.UrlProvider.GetUrl(nodeId, mode, current: currentUri); Assert.AreEqual(expected, result); } @@ -220,7 +222,8 @@ namespace Umbraco.Tests.Routing SetDomains2(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); + var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; + var result = umbracoContext.UrlProvider.GetUrl(nodeId, mode, current : currentUri); Assert.AreEqual(expected, result); } @@ -244,7 +247,8 @@ namespace Umbraco.Tests.Routing SetDomains3(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); + var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; + var result = umbracoContext.UrlProvider.GetUrl(nodeId, mode, current : currentUri); Assert.AreEqual(expected, result); } @@ -274,7 +278,8 @@ namespace Umbraco.Tests.Routing SetDomains4(); var currentUri = new Uri(currentUrl); - var result = umbracoContext.UrlProvider.GetUrl(nodeId, absolute, current : currentUri); + var mode = absolute ? UrlMode.Absolute : UrlMode.Auto; + var result = umbracoContext.UrlProvider.GetUrl(nodeId, mode, current : currentUri); Assert.AreEqual(expected, result); } @@ -294,19 +299,19 @@ namespace Umbraco.Tests.Routing SetDomains4(); string ignore; - ignore = umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(10012, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(10013, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain1.com")); - ignore = umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain2.com")); - ignore = umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain2.com")); - ignore = umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain2.com")); - ignore = umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain2.com")); + ignore = umbracoContext.UrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain2.com")); - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); Assert.AreEqual(7, cachedRoutes.Count); @@ -323,15 +328,15 @@ namespace Umbraco.Tests.Routing CheckRoute(cachedRoutes, cachedIds, 1002, "/1002"); // use the cache - Assert.AreEqual("/", umbracoContext.UrlProvider.GetUrl(1001, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/en/", umbracoContext.UrlProvider.GetUrl(10011, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/fr/", umbracoContext.UrlProvider.GetUrl(10012, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/1001-3/", umbracoContext.UrlProvider.GetUrl(10013, false, current: new Uri("http://domain1.com"))); - Assert.AreEqual("/1002/", umbracoContext.UrlProvider.GetUrl(1002, false, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/", umbracoContext.UrlProvider.GetUrl(1001, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/en/", umbracoContext.UrlProvider.GetUrl(10011, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/fr/", umbracoContext.UrlProvider.GetUrl(10012, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/1001-3/", umbracoContext.UrlProvider.GetUrl(10013, UrlMode.Auto, current: new Uri("http://domain1.com"))); + Assert.AreEqual("/1002/", umbracoContext.UrlProvider.GetUrl(1002, UrlMode.Auto, current: new Uri("http://domain1.com"))); - Assert.AreEqual("http://domain1.com/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, false, current: new Uri("http://domain2.com"))); + Assert.AreEqual("http://domain1.com/fr/1001-2-1/", umbracoContext.UrlProvider.GetUrl(100121, UrlMode.Auto, current: new Uri("http://domain2.com"))); } private static void CheckRoute(IDictionary routes, IDictionary ids, int id, string route) @@ -359,7 +364,7 @@ namespace Umbraco.Tests.Routing Assert.AreEqual("/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111)); Assert.AreEqual("http://domain3.com/en/1003-1-1/", umbracoContext.UrlProvider.GetUrl(100311)); - umbracoContext.UrlProvider.Mode = UrlProviderMode.Absolute; + umbracoContext.UrlProvider.Mode = UrlMode.Absolute; Assert.AreEqual("http://domain1.com/en/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111)); Assert.AreEqual("http://domain3.com/en/1003-1-1/", umbracoContext.UrlProvider.GetUrl(100311)); @@ -380,7 +385,7 @@ namespace Umbraco.Tests.Routing SetDomains5(); - var url = umbracoContext.UrlProvider.GetUrl(100111, true); + var url = umbracoContext.UrlProvider.GetUrl(100111, UrlMode.Absolute); Assert.AreEqual("http://domain1.com/en/1001-1-1/", url); var result = umbracoContext.UrlProvider.GetOtherUrls(100111).ToArray(); diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index 0eb621bd93..6587b2e4f6 100644 --- a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs +++ b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web.Routing; using Umbraco.Core.Services; @@ -44,10 +45,10 @@ namespace Umbraco.Tests.Routing { new DefaultUrlProvider(settings.RequestHandler, Logger, globalSettings.Object, new SiteDomainHelper()) }, globalSettings:globalSettings.Object); - Assert.AreEqual("http://domain2.com/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, true)); + Assert.AreEqual("http://domain2.com/1001-1-1/", umbracoContext.UrlProvider.GetUrl(100111, UrlMode.Absolute)); // check that the proper route has been cached - var cache = umbracoContext.ContentCache as PublishedContentCache; + var cache = umbracoContext.Content as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); Assert.AreEqual("10011/1001-1-1", cachedRoutes[100111]); diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index fc9e0e6166..d0258a100f 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -191,17 +191,17 @@ namespace Umbraco.Tests.Runtimes var umbracoContext = umbracoContextReference.UmbracoContext; // assert that there is no published document - var pcontent = umbracoContext.ContentCache.GetById(content.Id); + var pcontent = umbracoContext.Content.GetById(content.Id); Assert.IsNull(pcontent); // but a draft document - pcontent = umbracoContext.ContentCache.GetById(true, content.Id); + pcontent = umbracoContext.Content.GetById(true, content.Id); Assert.IsNotNull(pcontent); - Assert.AreEqual("test", pcontent.Name); + Assert.AreEqual("test", pcontent.Name()); Assert.IsTrue(pcontent.IsDraft()); // no published url - Assert.AreEqual("#", pcontent.GetUrl()); + Assert.AreEqual("#", pcontent.Url()); // now publish the document + make some unpublished changes contentService.SaveAndPublish(content); @@ -209,22 +209,22 @@ namespace Umbraco.Tests.Runtimes contentService.Save(content); // assert that snapshot has been updated and there is now a published document - pcontent = umbracoContext.ContentCache.GetById(content.Id); + pcontent = umbracoContext.Content.GetById(content.Id); Assert.IsNotNull(pcontent); - Assert.AreEqual("test", pcontent.Name); + Assert.AreEqual("test", pcontent.Name()); Assert.IsFalse(pcontent.IsDraft()); // but the url is the published one - no draft url - Assert.AreEqual("/test/", pcontent.GetUrl()); + Assert.AreEqual("/test/", pcontent.Url()); // and also an updated draft document - pcontent = umbracoContext.ContentCache.GetById(true, content.Id); + pcontent = umbracoContext.Content.GetById(true, content.Id); Assert.IsNotNull(pcontent); - Assert.AreEqual("testx", pcontent.Name); + Assert.AreEqual("testx", pcontent.Name()); Assert.IsTrue(pcontent.IsDraft()); // and the published document has a url - Assert.AreEqual("/test/", pcontent.GetUrl()); + Assert.AreEqual("/test/", pcontent.Url()); umbracoContextReference.Dispose(); mainDom.Stop(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index d969356ce9..397a22fc62 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -92,13 +92,12 @@ namespace Umbraco.Tests.Scoping null, publishedSnapshotAccessor, Mock.Of(), - Mock.Of(), Logger, ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, new DatabaseDataSource(), - Factory.GetInstance(), new SiteDomainHelper(), + Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); @@ -149,11 +148,11 @@ namespace Umbraco.Tests.Scoping { evented++; - var e = umbracoContext.ContentCache.GetById(item.Id); + var e = umbracoContext.Content.GetById(item.Id); // during events, due to LiveSnapshot, we see the changes Assert.IsNotNull(e); - Assert.AreEqual("changed", e.Name); + Assert.AreEqual("changed", e.Name()); }; using (var scope = ScopeProvider.CreateScope()) @@ -163,9 +162,9 @@ namespace Umbraco.Tests.Scoping } // been created - var x = umbracoContext.ContentCache.GetById(item.Id); + var x = umbracoContext.Content.GetById(item.Id); Assert.IsNotNull(x); - Assert.AreEqual("name", x.Name); + Assert.AreEqual("name", x.Name()); ContentService.Published += OnPublishedAssert; @@ -185,9 +184,9 @@ namespace Umbraco.Tests.Scoping // after the scope, // if completed, we see the changes // else changes have been rolled back - x = umbracoContext.ContentCache.GetById(item.Id); + x = umbracoContext.Content.GetById(item.Id); Assert.IsNotNull(x); - Assert.AreEqual(complete ? "changed" : "name", x.Name); + Assert.AreEqual(complete ? "changed" : "name", x.Name()); } } } diff --git a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs index 044965bc79..34482a79fa 100644 --- a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs @@ -75,7 +75,7 @@ namespace Umbraco.Tests.Scoping private static XmlStore XmlStore => (Current.Factory.GetInstance() as PublishedSnapshotService).XmlStore; private static XmlDocument XmlMaster => XmlStore.Xml; - private static XmlDocument XmlInContext => ((PublishedContentCache) Umbraco.Web.Composing.Current.UmbracoContext.ContentCache).GetXml(false); + private static XmlDocument XmlInContext => ((PublishedContentCache) Umbraco.Web.Composing.Current.UmbracoContext.Content).GetXml(false); [TestCase(true)] [TestCase(false)] @@ -85,7 +85,7 @@ namespace Umbraco.Tests.Scoping // sanity checks Assert.AreSame(umbracoContext, Umbraco.Web.Composing.Current.UmbracoContext); - Assert.AreSame(XmlStore, ((PublishedContentCache) umbracoContext.ContentCache).XmlStore); + Assert.AreSame(XmlStore, ((PublishedContentCache) umbracoContext.Content).XmlStore); // create document type, document var contentType = new ContentType(-1) { Alias = "CustomDocument", Name = "Custom Document" }; @@ -199,7 +199,7 @@ namespace Umbraco.Tests.Scoping // sanity checks Assert.AreSame(umbracoContext, Umbraco.Web.Composing.Current.UmbracoContext); - Assert.AreSame(XmlStore, ((PublishedContentCache)umbracoContext.ContentCache).XmlStore); + Assert.AreSame(XmlStore, ((PublishedContentCache)umbracoContext.Content).XmlStore); // create document type var contentType = new ContentType(-1) { Alias = "CustomDocument", Name = "Custom Document" }; diff --git a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs new file mode 100644 index 0000000000..c81c108e0d --- /dev/null +++ b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Specialized; +using System.Web; +using System.Web.Helpers; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Mvc; +using Umbraco.Web.Security; + +namespace Umbraco.Tests.Security +{ + [TestFixture] + public class UmbracoAntiForgeryAdditionalDataProviderTests + { + [Test] + public void Test_Wrapped_Non_BeginUmbracoForm() + { + var wrapped = Mock.Of(x => x.GetAdditionalData(It.IsAny()) == "custom"); + var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var data = provider.GetAdditionalData(httpContextFactory.HttpContext); + + Assert.IsTrue(data.DetectIsJson()); + var json = JsonConvert.DeserializeObject(data); + Assert.AreEqual(null, json.Ufprt); + Assert.IsTrue(json.Stamp != default); + Assert.AreEqual("custom", json.WrappedValue); + } + + [Test] + public void Null_Wrapped_Non_BeginUmbracoForm() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var data = provider.GetAdditionalData(httpContextFactory.HttpContext); + + Assert.IsTrue(data.DetectIsJson()); + var json = JsonConvert.DeserializeObject(data); + Assert.AreEqual(null, json.Ufprt); + Assert.IsTrue(json.Stamp != default); + Assert.AreEqual("default", json.WrappedValue); + } + + [Test] + public void Validate_Non_Json() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "hello"); + + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_Invalid_Json() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '0'}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': ''}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'hello': 'world'}"); + Assert.IsFalse(isValid); + + } + + [Test] + public void Validate_No_Request_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': 'ASBVDFDFDFDF'}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_No_AdditionalData_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns("ABCDEFG"); + + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_No_AdditionalData_Or_Request_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); + Assert.IsTrue(isValid); + } + + [Test] + public void Validate_Request_And_AdditionalData_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); + + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsTrue(isValid); + + routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_Wrapped_Request_And_AdditionalData_Ufprt() + { + var wrapped = Mock.Of(x => x.ValidateAdditionalData(It.IsAny(), "custom") == true); + var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); + + var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); + + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'custom', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsTrue(isValid); + + routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + } + } +} diff --git a/src/Umbraco.Tests/Services/AuditServiceTests.cs b/src/Umbraco.Tests/Services/AuditServiceTests.cs index 6064fe4acc..bfec246e61 100644 --- a/src/Umbraco.Tests/Services/AuditServiceTests.cs +++ b/src/Umbraco.Tests/Services/AuditServiceTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Umbraco.Core.Services.Implement; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; +using Umbraco.Core.Models; namespace Umbraco.Tests.Services { @@ -48,5 +49,19 @@ namespace Umbraco.Tests.Services Assert.AreEqual(123 + 5, entries[0].PerformingUserId); Assert.AreEqual(123 + 4, entries[1].PerformingUserId); } + + [Test] + public void CanReadEntries() + { + var yesterday = DateTime.UtcNow.AddDays(-1); + + for (var i = 0; i < 10; i++) + { + yesterday = yesterday.AddMinutes(1); + ServiceContext.AuditService.Add(AuditType.Unpublish, -1, 33, "", "blah"); + } + + var logs = ServiceContext.AuditService.GetUserLogs(-1, AuditType.Unpublish); + } } } diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index c70b96a175..3121988bfe 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -18,11 +18,9 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; using Umbraco.Tests.Testing; -using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.PublishedCache.NuCache.DataSource; -using Umbraco.Web.Routing; namespace Umbraco.Tests.Services { @@ -65,13 +63,12 @@ namespace Umbraco.Tests.Services null, publishedSnapshotAccessor, Mock.Of(), - Mock.Of(), Logger, ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, new DatabaseDataSource(), - Factory.GetInstance(), new SiteDomainHelper(), + Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index b5712a52aa..e52d338ca6 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -147,7 +147,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting umbracoContextAccessor.UmbracoContext = umbCtx; var urlHelper = new Mock(); - urlHelper.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + urlHelper.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/hello/world/1234")); var membershipHelper = new MembershipHelper(umbCtx.HttpContext, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs deleted file mode 100644 index a9abe96232..0000000000 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestPublishedContent.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.PublishedCache; - -namespace Umbraco.Tests.TestHelpers.Stubs -{ - internal class TestPublishedContent : PublishedElement, IPublishedContent - { - public TestPublishedContent(PublishedContentType contentType, int id, Guid key, Dictionary values, bool previewing, Dictionary cultures = null) - : base(contentType, key, values, previewing) - { - Id = id; - Cultures = cultures; - } - - public int Id { get; } - public int? TemplateId { get; set; } - public int SortOrder { get; set; } - public string Name { get; set; } - public IVariationContextAccessor VariationContextAccessor { get; set; } - public PublishedCultureInfo GetCulture(string culture = null) - { - // handle context culture - if (culture == null) - culture = VariationContextAccessor?.VariationContext?.Culture; - - // no invariant culture infos - if (culture == "" || Cultures == null) return null; - - // get - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; - } - public IReadOnlyDictionary Cultures { get; set; } - public string UrlSegment { get; set; } - public string DocumentTypeAlias => ContentType.Alias; - public int DocumentTypeId { get; set; } - public string WriterName { get; set; } - public string CreatorName { get; set; } - public int WriterId { get; set; } - public int CreatorId { get; set; } - public string Path { get; set; } - public DateTime CreateDate { get; set; } - public DateTime UpdateDate { get; set; } - public Guid Version { get; set; } - public int Level { get; set; } - public string Url { get; set; } - public string GetUrl(string culture = null) => throw new NotSupportedException(); - public PublishedItemType ItemType => ContentType.ItemType; - public bool IsDraft(string culture = null) => false; - public bool IsPublished(string culture = null) => true; - public IPublishedContent Parent { get; set; } - public IEnumerable Children { get; set; } - - // copied from PublishedContentBase - public IPublishedProperty GetProperty(string alias, bool recurse) - { - var property = GetProperty(alias); - if (recurse == false) return property; - - IPublishedContent content = this; - var firstNonNullProperty = property; - while (content != null && (property == null || property.HasValue() == false)) - { - content = content.Parent; - property = content?.GetProperty(alias); - if (firstNonNullProperty == null && property != null) firstNonNullProperty = property; - } - - // if we find a content with the property with a value, return that property - // if we find no content with the property, return null - // if we find a content with the property without a value, return that property - // have to save that first property while we look further up, hence firstNonNullProperty - - return property != null && property.HasValue() ? property : firstNonNullProperty; - } - } -} diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index 647824ab66..dc8e35bb52 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Services; @@ -147,7 +148,7 @@ namespace Umbraco.Tests.TestHelpers var umbracoSettingsMock = new Mock(); var webRoutingSectionMock = new Mock(); - webRoutingSectionMock.Setup(x => x.UrlProviderMode).Returns(UrlProviderMode.Auto.ToString()); + webRoutingSectionMock.Setup(x => x.UrlProviderMode).Returns(UrlMode.Auto.ToString()); umbracoSettingsMock.Setup(x => x.WebRouting).Returns(webRoutingSectionMock.Object); return umbracoSettingsMock.Object; } diff --git a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs index 26bfff0e1a..72fb89ab82 100644 --- a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs +++ b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs @@ -9,30 +9,38 @@ namespace Umbraco.Tests.Testing.Objects { internal class TestDataSource : IDataSource { - private readonly Dictionary _kits; - public TestDataSource(params ContentNodeKit[] kits) : this((IEnumerable) kits) { } public TestDataSource(IEnumerable kits) { - _kits = kits.ToDictionary(x => x.Node.Id, x => x); + Kits = kits.ToDictionary(x => x.Node.Id, x => x); } + public Dictionary Kits { get; } + + // note: it is important to clone the returned kits, as the inner + // ContentNode is directly reused and modified by the snapshot service + public ContentNodeKit GetContentSource(IScope scope, int id) - => _kits.TryGetValue(id, out var kit) ? kit : default; + => Kits.TryGetValue(id, out var kit) ? kit.Clone() : default; public IEnumerable GetAllContentSources(IScope scope) - => _kits.Values; + => Kits.Values + .OrderBy(x => x.Node.Level) + .Select(x => x.Clone()); public IEnumerable GetBranchContentSources(IScope scope, int id) - { - throw new NotImplementedException(); - } + => Kits.Values + .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) + .OrderBy(x => x.Node.Level).ThenBy(x => x.Node.SortOrder) + .Select(x => x.Clone()); public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) - => _kits.Values.Where(x => ids.Contains(x.ContentTypeId)); + => Kits.Values + .Where(x => ids.Contains(x.ContentTypeId)) + .Select(x => x.Clone()); public ContentNodeKit GetMediaSource(IScope scope, int id) { diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index d85f610236..28fecd6b2b 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Testing.TestingTests var umbracoContext = TestObjects.GetUmbracoContextMock(); var urlProviderMock = new Mock(); - urlProviderMock.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + urlProviderMock.Setup(provider => provider.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/hello/world/1234")); var urlProvider = urlProviderMock.Object; diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 2d2cbaa3fd..7e72a5aefb 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -248,6 +248,11 @@ namespace Umbraco.Tests.Testing // register empty content apps collection Composition.WithCollectionBuilder(); + + // manifest + Composition.ManifestValueValidators(); + Composition.ManifestFilters(); + } protected virtual void ComposeMapper(bool configure) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2f617395fb..745a4fb7aa 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -138,6 +138,7 @@ + @@ -145,6 +146,7 @@ + @@ -204,7 +206,6 @@ - diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs index a4213b4f0e..d77867152a 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs @@ -77,6 +77,19 @@ namespace Umbraco.Tests.Web.Controllers var entityService = new Mock(); entityService.Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, It.IsAny())) .Returns((UmbracoObjectTypes objType, int[] ids) => ids.Select(x => new TreeEntityPath { Path = $"-1,{x}", Id = x }).ToList()); + entityService.Setup(x => x.GetKey(It.IsAny(), UmbracoObjectTypes.DataType)) + .Returns((int id, UmbracoObjectTypes objType) => + { + switch (id) + { + case Constants.DataTypes.Textbox: + return Attempt.Succeed(Constants.DataTypes.Guids.TextstringGuid); + case Constants.DataTypes.RichtextEditor: + return Attempt.Succeed(Constants.DataTypes.Guids.RichtextEditorGuid); + } + return Attempt.Fail(); + }); + var dataTypeService = new Mock(); dataTypeService.Setup(service => service.GetDataType(It.IsAny())) diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index 82624eced5..3a5405548b 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -82,8 +82,8 @@ namespace Umbraco.Tests.Web //setup a mock url provider which we'll use for testing var testUrlProvider = new Mock(); testUrlProvider - .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => UrlInfo.Url("/my-test-url")); + .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlMode mode, string culture, Uri url) => UrlInfo.Url("/my-test-url")); var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 0750937cb6..34ab3eb90d 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -9799,9 +9799,9 @@ "dev": true }, "nouislider": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-12.1.0.tgz", - "integrity": "sha512-SAOabF6hBm8201c6LDbkVOVhgwY49+/ms72ZLUF2qkN5RCf7FfUvEh/hGZ7XcwZHU+I/grlicPmcSk1/rrMnOw==" + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.0.1.tgz", + "integrity": "sha512-YNLKuABWYxmC5WXJ9TUj3N7+iyL/xT3+jm1mgOMXoqBhAL0Pj9BMgyKmLgwRnrxNN+C/fe7sFmpQDDPsxbMT2w==" }, "npm": { "version": "6.4.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ef1a9cc28d..35d808056c 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -35,7 +35,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "12.1.0", + "nouislider": "14.0.1", "npm": "^6.4.1", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js index 91eb077ba3..8434a96ba5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js @@ -22,13 +22,16 @@ vm.search = search; vm.clickItem = clickItem; vm.clearSearch = clearSearch; - vm.handleKeyUp = handleKeyUp; + vm.handleKeyDown = handleKeyDown; vm.closeSearch = closeSearch; vm.focusSearch = focusSearch; //we need to capture the focus before this element is initialized. vm.focusBeforeOpening = focusService.getLastKnownFocus(); + vm.activeResult = null; + vm.activeResultGroup = null; + function onInit() { vm.searchQuery = ""; vm.searchResults = []; @@ -72,14 +75,66 @@ * Handles all keyboard events * @param {object} event */ - function handleKeyUp(event) { - - event.stopPropagation(); - event.preventDefault(); + function handleKeyDown(event) { // esc if(event.keyCode === 27) { + event.stopPropagation(); + event.preventDefault(); + closeSearch(); + return; + } + + // up/down (navigate search results) + if (vm.hasResults && (event.keyCode === 38 || event.keyCode === 40)) { + event.stopPropagation(); + event.preventDefault(); + + var allGroups = _.values(vm.searchResults); + var down = event.keyCode === 40; + if (vm.activeResultGroup === null) { + // it's the first time navigating, pick the appropriate group and result + // - first group and first result when navigating down + // - last group and last result when navigating up + vm.activeResultGroup = down ? _.first(allGroups) : _.last(allGroups); + vm.activeResult = down ? _.first(vm.activeResultGroup.results) : _.last(vm.activeResultGroup.results); + } + else if (down) { + // handle navigation down through the groups and results + if (vm.activeResult === _.last(vm.activeResultGroup.results)) { + if (vm.activeResultGroup === _.last(allGroups)) { + vm.activeResultGroup = _.first(allGroups); + } + else { + vm.activeResultGroup = allGroups[allGroups.indexOf(vm.activeResultGroup) + 1]; + } + vm.activeResult = _.first(vm.activeResultGroup.results); + } + else { + vm.activeResult = vm.activeResultGroup.results[vm.activeResultGroup.results.indexOf(vm.activeResult) + 1]; + } + } + else { + // handle navigation up through the groups and results + if (vm.activeResult === _.first(vm.activeResultGroup.results)) { + if (vm.activeResultGroup === _.first(allGroups)) { + vm.activeResultGroup = _.last(allGroups); + } + else { + vm.activeResultGroup = allGroups[allGroups.indexOf(vm.activeResultGroup) - 1]; + } + vm.activeResult = _.last(vm.activeResultGroup.results); + } + else { + vm.activeResult = vm.activeResultGroup.results[vm.activeResultGroup.results.indexOf(vm.activeResult) - 1]; + } + } + + $timeout(function () { + var resultElementLink = angular.element(".umb-search-item[active-result='true'] .umb-search-result__link"); + resultElementLink[0].focus(); + }); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index 6127153a16..f026a05c45 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -95,7 +95,8 @@ Use this directive to render an umbraco button. The directive can be used to gen size: "@?", alias: "@?", addEllipsis: "@?", - showCaret: "@?" + showCaret: "@?", + autoFocus: "@?" } }); @@ -115,6 +116,7 @@ Use this directive to render an umbraco button. The directive can be used to gen vm.blockElement = false; vm.style = null; vm.innerState = "init"; + vm.generalActions = vm.labelKey === "general_actions"; vm.buttonLabel = vm.label; // is this a primary button style (i.e. anything but an 'info' button)? diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 3a874f83c6..a548820138 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -8,6 +8,7 @@ var evts = []; var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode; + var watchingCulture = false; //setup scope vars $scope.defaultButton = null; @@ -26,26 +27,58 @@ $scope.allowOpen = true; $scope.app = null; + //initializes any watches + function startWatches(content) { + + //watch for changes to isNew & the content.id, set the page.isNew accordingly and load the breadcrumb if we can + $scope.$watchGroup(['isNew', 'content.id'], function (newVal, oldVal) { + + var contentId = newVal[1]; + $scope.page.isNew = Object.toBoolean(newVal[0]); + + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$scope.page.isNew && contentId && content.parentId && content.parentId !== -1) { + loadBreadcrumb(); + if (!watchingCulture) { + $scope.$watch('culture', + function (value, oldValue) { + if (value !== oldValue) { + loadBreadcrumb(); + } + }); + } + } + }); + + } + + //this initializes the editor with the data which will be called more than once if the data is re-loaded function init() { - + var content = $scope.content; - + + if (content.id && content.isChildOfListView && content.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) ? + "/content/content/edit/" + content.parentId + "?page=" + $routeParams.page : + "/content/content/edit/" + content.parentId; + } + // we need to check wether an app is present in the current data, if not we will present the default app. var isAppPresent = false; - + // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { - + // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) - _.forEach(content.apps, function(app) { + _.forEach(content.apps, function (app) { if (app === $scope.app) { isAppPresent = true; } }); - + // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function(app) { + _.forEach(content.apps, function (app) { if (app.alias === $scope.app.alias) { isAppPresent = true; app.active = true; @@ -53,7 +86,7 @@ } }); } - + } // if we still dont have a app, lets show the first one: @@ -61,21 +94,8 @@ content.apps[0].active = true; $scope.appChanged(content.apps[0]); } - - editorState.set(content); - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!$scope.page.isNew) { - if (content.parentId && content.parentId !== -1) { - loadBreadcrumb(); - $scope.$watch('culture', - function (value, oldValue) { - if (value !== oldValue) { - loadBreadcrumb(); - } - }); - } - } + editorState.set(content); bindEvents(); @@ -118,10 +138,10 @@ function isContentCultureVariant() { return $scope.content.variants.length > 1; } - + function reload() { $scope.page.loading = true; - loadContent().then(function() { + loadContent().then(function () { $scope.page.loading = false; }); } @@ -134,7 +154,7 @@ evts.push(eventsService.on("editors.documentType.saved", function (name, args) { // if this content item uses the updated doc type we need to reload the content item - if(args && args.documentType && $scope.content.documentType.id === args.documentType.id) { + if (args && args.documentType && $scope.content.documentType.id === args.documentType.id) { reload(); } })); @@ -152,12 +172,6 @@ $scope.content = data; - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) ? - "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : - "/content/content/edit/" + data.parentId; - } - init(); syncTreeNode($scope.content, $scope.content.path, true); @@ -219,7 +233,7 @@ $scope.page.showPreviewButton = true; } - + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -228,9 +242,13 @@ } if (!$scope.content.isChildOfListView) { - navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.page.menu.currentNode = syncArgs.node; - }); + navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }) + .then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }, function () { + //handle the rejection + console.log("A problem occurred syncing the tree! A path is probably incorrect.") + }); } else if (initialLoad === true) { @@ -328,7 +346,7 @@ $scope.contentForm.$dirty = false; for (var i = 0; i < $scope.content.variants.length; i++) { - if($scope.content.variants[i].isDirty){ + if ($scope.content.variants[i].isDirty) { $scope.contentForm.$dirty = true; return; } @@ -337,7 +355,7 @@ // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - + //Used to check validility of nested form - coming from Content Apps mostly //Set them all to be invalid var fieldsToRollback = checkValidility(); @@ -348,7 +366,8 @@ scope: $scope, content: $scope.content, action: args.action, - showNotifications: args.showNotifications + showNotifications: args.showNotifications, + softRedirect: true }).then(function (data) { //success init(); @@ -364,11 +383,6 @@ function (err) { syncTreeNode($scope.content, $scope.content.path); - //error - if (err) { - editorState.set($scope.content); - } - resetNestedFieldValiation(fieldsToRollback); return $q.reject(err); @@ -421,9 +435,9 @@ //need to show a notification else it's not clear there was an error. localizationService.localizeMany([ - "speechBubbles_validationFailedHeader", - "speechBubbles_validationFailedMessage" - ] + "speechBubbles_validationFailedHeader", + "speechBubbles_validationFailedMessage" + ] ).then(function (data) { notificationsService.error(data[0], data[1]); }); @@ -440,6 +454,7 @@ $scope.content = data; init(); + startWatches($scope.content); resetLastListPageNumber($scope.content); @@ -454,13 +469,14 @@ $scope.page.loading = true; loadContent().then(function () { + startWatches($scope.content); $scope.page.loading = false; }); } $scope.unpublish = function () { clearNotifications($scope.content); - if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) { + if (formHelper.submitForm({ scope: $scope, action: "unpublish", skipValidation: true })) { var dialog = { parentScope: $scope, view: "views/content/overlays/unpublish.html", @@ -494,6 +510,7 @@ overlayService.close(); } }; + overlayService.open(dialog); } }; @@ -510,7 +527,7 @@ variants: $scope.content.variants, //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation submitButtonLabelKey: "buttons_saveToPublish", - submit: function(model) { + submit: function (model) { model.submitButtonState = "busy"; clearNotifications($scope.content); //we need to return this promise so that the dialog can handle the result and wire up the validation response @@ -518,14 +535,14 @@ saveMethod: contentResource.sendToPublish, action: "sendToPublish", showNotifications: false - }).then(function(data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, - function(err) { + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, + function (err) { clearDirtyState($scope.content.variants); model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties @@ -534,7 +551,7 @@ return $q.when(err); }); }, - close: function() { + close: function () { overlayService.close(); } }; @@ -563,14 +580,13 @@ if (isContentCultureVariant()) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { - var dialog = { parentScope: $scope, view: "views/content/overlays/publish.html", variants: $scope.content.variants, //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation submitButtonLabelKey: "buttons_saveAndPublish", - submit: function(model) { + submit: function (model) { model.submitButtonState = "busy"; clearNotifications($scope.content); //we need to return this promise so that the dialog can handle the result and wire up the validation response @@ -578,14 +594,14 @@ saveMethod: contentResource.publish, action: "publish", showNotifications: false - }).then(function(data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, - function(err) { + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, + function (err) { clearDirtyState($scope.content.variants); model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties @@ -594,11 +610,10 @@ return $q.when(err); }); }, - close: function() { + close: function () { overlayService.close(); } }; - overlayService.open(dialog); } else { @@ -617,7 +632,7 @@ $scope.page.buttonGroupState = "success"; }, function () { $scope.page.buttonGroupState = "error"; - });; + }); } }; @@ -634,7 +649,7 @@ variants: $scope.content.variants, //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation submitButtonLabelKey: "buttons_save", - submit: function(model) { + submit: function (model) { model.submitButtonState = "busy"; clearNotifications($scope.content); //we need to return this promise so that the dialog can handle the result and wire up the validation response @@ -642,14 +657,14 @@ saveMethod: $scope.saveMethod(), action: "save", showNotifications: false - }).then(function(data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, - function(err) { + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); + clearNotifications($scope.content); + overlayService.close(); + return $q.when(data); + }, + function (err) { clearDirtyState($scope.content.variants); model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties @@ -658,7 +673,7 @@ return $q.when(err); }); }, - close: function(oldModel) { + close: function (oldModel) { overlayService.close(); } }; @@ -685,7 +700,7 @@ }; - $scope.schedule = function() { + $scope.schedule = function () { clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { @@ -755,7 +770,7 @@ } }; - $scope.publishDescendants = function() { + $scope.publishDescendants = function () { clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) { @@ -883,13 +898,13 @@ * @param {any} app */ $scope.appChanged = function (app) { - + $scope.app = app; - + $scope.$broadcast("editors.apps.appChanged", { app: app }); - + createButtons($scope.content); - + }; /** @@ -907,11 +922,11 @@ $scope.infiniteModel.close($scope.infiniteModel); } }; - + /** * Call back when user click the back-icon */ - $scope.onBack = function() { + $scope.onBack = function () { if ($scope.infiniteModel && $scope.infiniteModel.close) { $scope.infiniteModel.close($scope.infiniteModel); } else { @@ -927,9 +942,7 @@ } //since we are not notifying and clearing server validation messages when they are received due to how the variant //switching works, we need to ensure they are cleared when this editor is destroyed - if (!$scope.page.isNew) { - serverValidationManager.clear(); - } + serverValidationManager.clear(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index e58ff14e21..e2f5f71781 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -328,6 +328,8 @@ isInfoTab = true; loadAuditTrail(); loadRedirectUrls(); + setNodePublishStatus(); + formatDatesToLocal(); } else { isInfoTab = false; } @@ -344,6 +346,7 @@ loadAuditTrail(true); loadRedirectUrls(); setNodePublishStatus(); + formatDatesToLocal(); } updateCurrentUrls(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 77f2ffb54a..47e6818466 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -3,113 +3,113 @@ **/ angular.module('umbraco.directives') -.directive('onDragEnter', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnter); - }; - elm.on("dragenter", f); - scope.$on("$destroy", function(){ elm.off("dragenter", f);} ); - } - }; -}) - -.directive('onDragLeave', function () { - return function (scope, elm, attrs) { - var f = function (event) { - var rect = this.getBoundingClientRect(); - var getXY = function getCursorPosition(event) { - var x, y; - - if (typeof event.clientX === 'undefined') { - // try touch screen - x = event.pageX + document.documentElement.scrollLeft; - y = event.pageY + document.documentElement.scrollTop; - } else { - x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - - return { x: x, y : y }; - }; - - var e = getXY(event.originalEvent); - - // Check the mouseEvent coordinates are outside of the rectangle - if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { - scope.$apply(attrs.onDragLeave); + .directive('onDragEnter', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnter); + }; + elm.on("dragenter", f); + scope.$on("$destroy", function () { elm.off("dragenter", f); }); } }; + }) - elm.on("dragleave", f); - scope.$on("$destroy", function(){ elm.off("dragleave", f);} ); - }; -}) + .directive('onDragLeave', function () { + return function (scope, elm, attrs) { + var f = function (event) { + var rect = this.getBoundingClientRect(); + var getXY = function getCursorPosition(event) { + var x, y; -.directive('onDragOver', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragOver); + if (typeof event.clientX === 'undefined') { + // try touch screen + x = event.pageX + document.documentElement.scrollLeft; + y = event.pageY + document.documentElement.scrollTop; + } else { + x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return { x: x, y: y }; + }; + + var e = getXY(event.originalEvent); + + // Check the mouseEvent coordinates are outside of the rectangle + if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { + scope.$apply(attrs.onDragLeave); + } }; - elm.on("dragover", f); - scope.$on("$destroy", function(){ elm.off("dragover", f);} ); - } - }; -}) -.directive('onDragStart', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragStart); - }; - elm.on("dragstart", f); - scope.$on("$destroy", function(){ elm.off("dragstart", f);} ); - } - }; -}) + elm.on("dragleave", f); + scope.$on("$destroy", function () { elm.off("dragleave", f); }); + }; + }) -.directive('onDragEnd', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDragEnd); - }; - elm.on("dragend", f); - scope.$on("$destroy", function(){ elm.off("dragend", f);} ); - } - }; -}) + .directive('onDragOver', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragOver); + }; + elm.on("dragover", f); + scope.$on("$destroy", function () { elm.off("dragover", f); }); + } + }; + }) -.directive('onDrop', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onDrop); - }; - elm.on("drop", f); - scope.$on("$destroy", function(){ elm.off("drop", f);} ); - } - }; -}) + .directive('onDragStart', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragStart); + }; + elm.on("dragstart", f); + scope.$on("$destroy", function () { elm.off("dragstart", f); }); + } + }; + }) -.directive('onOutsideClick', function ($timeout, angularHelper) { - return function (scope, element, attrs) { + .directive('onDragEnd', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnd); + }; + elm.on("dragend", f); + scope.$on("$destroy", function () { elm.off("dragend", f); }); + } + }; + }) - var eventBindings = []; + .directive('onDrop', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDrop); + }; + elm.on("drop", f); + scope.$on("$destroy", function () { elm.off("drop", f); }); + } + }; + }) - function oneTimeClick(event) { - var el = event.target.nodeName; + .directive('onOutsideClick', function ($timeout, angularHelper) { + return function (scope, element, attrs) { - //ignore link and button clicks - var els = ["INPUT","A","BUTTON"]; - if(els.indexOf(el) >= 0){return;} + var eventBindings = []; + + function oneTimeClick(event) { + // ignore clicks on button groups toggles (i.e. the save and publish button) + var parents = $(event.target).closest("[data-element='button-group-toggle']"); + if (parents.length > 0) { + return; + } // ignore clicks on new overlay - var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour"); - if(parents.length > 0){ + parents = $(event.target).parents(".umb-overlay,.umb-tour"); + if (parents.length > 0) { return; } @@ -131,71 +131,78 @@ angular.module('umbraco.directives') return; } - //ignore clicks inside this element - if( $(element).has( $(event.target) ).length > 0 ){ + // ignore clicks on dialog actions + var actions = $(event.target).parents(".umb-action"); + if (actions.length === 1) { return; } - scope.$apply(attrs.onOutsideClick); - } - - - $timeout(function(){ - - if ("bindClickOn" in attrs) { - - eventBindings.push(scope.$watch(function() { - return attrs.bindClickOn; - }, function(newValue) { - if (newValue === "true") { - $(document).on("click", oneTimeClick); - } else { - $(document).off("click", oneTimeClick); - } - })); - - } else { - $(document).on("click", oneTimeClick); - } - - scope.$on("$destroy", function() { - $(document).off("click", oneTimeClick); - - // unbind watchers - for (var e in eventBindings) { - eventBindings[e](); + //ignore clicks inside this element + if ($(element).has($(event.target)).length > 0) { + return; } + // please to not use angularHelper.safeApply here, it won't work + scope.$apply(attrs.onOutsideClick); + } + + + $timeout(function () { + + if ("bindClickOn" in attrs) { + + eventBindings.push(scope.$watch(function () { + return attrs.bindClickOn; + }, function (newValue) { + if (newValue === "true") { + $(document).on("click", oneTimeClick); + } else { + $(document).off("click", oneTimeClick); + } + })); + + } else { + $(document).on("click", oneTimeClick); + } + + scope.$on("$destroy", function () { + $(document).off("click", oneTimeClick); + + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + + }); + }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. + + }; + }) + + .directive('onRightClick', function ($parse) { + + document.oncontextmenu = function (e) { + if (e.target.hasAttribute('on-right-click')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + + return function (scope, el, attrs) { + el.on('contextmenu', function (e) { + e.preventDefault(); + e.stopPropagation(); + var fn = $parse(attrs.onRightClick); + scope.$apply(function () { + fn(scope, { $event: e }); + }); + return false; }); - }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. + }; + }) - }; -}) - -.directive('onRightClick',function($parse){ - - document.oncontextmenu = function (e) { - if(e.target.hasAttribute('on-right-click')) { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - return function(scope,el,attrs){ - el.on('contextmenu',function(e){ - e.preventDefault(); - e.stopPropagation(); - var fn = $parse(attrs.onRightClick); - scope.$apply(function () { - fn(scope, { $event: e }); - }); - return false; - }); - }; -}) - -.directive('onDelayedMouseleave', function ($timeout, $parse) { + .directive('onDelayedMouseleave', function ($timeout, $parse) { return { restrict: 'A', @@ -204,20 +211,20 @@ angular.module('umbraco.directives') var active = false; var fn = $parse(attrs.onDelayedMouseleave); - var leave_f = function(event) { - var callback = function() { - fn(scope, {$event:event}); + var leave_f = function (event) { + var callback = function () { + fn(scope, { $event: event }); }; active = false; - $timeout(function(){ - if(active === false){ + $timeout(function () { + if (active === false) { scope.$apply(callback); } }, 650); }; - var enter_f = function(event, args){ + var enter_f = function (event, args) { active = true; }; @@ -226,7 +233,7 @@ angular.module('umbraco.directives') element.on("mouseenter", enter_f); //unsub events - scope.$on("$destroy", function(){ + scope.$on("$destroy", function () { element.off("mouseleave", leave_f); element.off("mouseenter", enter_f); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js index 6fcdd99001..eb3503f799 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js @@ -9,8 +9,10 @@ angular.module("umbraco.directives") } }; - $timeout(function() { - update(); - }); + if (attr.umbAutoFocus !== "false") { + $timeout(function() { + update(); + }); + } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index dee3cfdab7..cd1f011018 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -4,7 +4,9 @@ angular.module("umbraco.directives") scope: { uniqueId: '=', value: '=', - configuration: "=" + configuration: "=", //this is the RTE configuration + datatypeKey: '@', + ignoreUserStartNodes: '@' }, templateUrl: 'views/components/grid/grid-rte.html', replace: true, @@ -35,6 +37,14 @@ angular.module("umbraco.directives") editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; } + //ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format + //since below we are just passing up `scope` as the actual model and for 2 way binding to work with `value` that + //is the way it needs to be unless we start adding watchers. We'll just go with this for now but it's super ugly. + scope.config = { + ignoreUserStartNodes: scope.ignoreUserStartNodes === "true" + } + scope.dataTypeKey = scope.datatypeKey; //Yes - this casing is rediculous, but it's because the var starts with `data` so it can't be `data-type-id` :/ + //stores a reference to the editor var tinyMceEditor = null; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index 8c32d93c01..0135abd97c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -311,9 +311,8 @@ Opens an overlay to show a custom YSOD.
var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); var submitOnEnterValue = submitOnEnter ? document.activeElement.getAttribute("overlay-submit-on-enter") : ""; - if(clickableElements.indexOf(activeElementType) === 0) { - document.activeElement.trigger("click"); - event.preventDefault(); + if(clickableElements.indexOf(activeElementType) >= 0) { + // don't do anything, let the browser Enter key handle this } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js index 99a5dad58c..904a2ce8ca 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbcontextdialog/umbcontextdialog.directive.js @@ -1,16 +1,20 @@ (function() { 'use strict'; - function UmbContextDialog(navigationService, keyboardService) { + function UmbContextDialog(navigationService, keyboardService, localizationService, overlayService) { function link($scope) { - - $scope.outSideClick = function() { - navigationService.hideDialog(); - } - keyboardService.bind("esc", function() { - navigationService.hideDialog(); + $scope.dialog = { + confirmDiscardChanges: false + }; + + $scope.outSideClick = function() { + hide(); + }; + + keyboardService.bind("esc", function () { + hide(); }); //ensure to unregister from all events! @@ -18,6 +22,35 @@ keyboardService.unbind("esc"); }); + function hide() { + if ($scope.dialog.confirmDiscardChanges) { + localizationService.localizeMany(["prompt_unsavedChanges", "prompt_unsavedChangesWarning", "prompt_discardChanges", "prompt_stay"]).then( + function (values) { + var overlay = { + "view": "default", + "title": values[0], + "content": values[1], + "disableBackdropClick": true, + "disableEscKey": true, + "submitButtonLabel": values[2], + "closeButtonLabel": values[3], + submit: function () { + overlayService.close(); + navigationService.hideDialog(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(overlay); + } + ); + } + else { + navigationService.hideDialog(); + } + } } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 7055d1a746..2bd93a4b27 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -38,7 +38,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use treeOptionsClick: [], treeNodeAltSelect: [] }; - + //this is the API exposed by this directive, for either hosting controllers or for other directives vm.callbacks = { treeNodeExpanded: function (f) { @@ -82,7 +82,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use // since it saves on data retreival and DOM processing. // TODO: This isn't used!? var lastSection = ""; - + /** Helper function to emit tree events */ function emitEvent(eventName, args) { if (registeredCallbacks[eventName] && angular.isArray(registeredCallbacks[eventName])) { @@ -92,10 +92,6 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use } } - // TODO: This isn't used!? - function clearCache(section) { - treeService.clearCache({ section: section }); - } /** * Re-loads the tree with the updated parameters @@ -119,7 +115,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use $scope.cachekey = args.cacheKey; } } - + return loadTree(); } @@ -148,7 +144,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use if (!args.path) { throw "args.path cannot be null"; } - + if (angular.isString(args.path)) { args.path = args.path.replace('"', '').split(','); } @@ -172,8 +168,16 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use emitEvent("treeSynced", { node: data, activate: args.activate }); return $q.when({ node: data, activate: args.activate }); + }, function (data) { + return $q.reject(data); + }, function (data) { + //on notification + if (data.type === "treeNodeExpanded") { + //raise the event + emitEvent("treeNodeExpanded", { tree: $scope.tree, node: data.node, children: data.children }); + } }); - + } /** This will check the section tree loaded and return all actual root nodes based on a tree type (non group nodes, non section groups) */ @@ -201,7 +205,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use //given a tree alias, this will search the current section tree for the specified tree alias and set the current active tree to it's root node function loadActiveTree(treeAlias) { - + if (!$scope.tree) { throw "Err in umbtree.directive.loadActiveTree, $scope.tree is null"; } @@ -229,11 +233,9 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use } /** Method to load in the tree data */ - function loadTree() { - if (!$scope.loading && $scope.section) { - $scope.loading = true; - + if ($scope.section) { + //default args var args = { section: $scope.section, tree: $scope.treealias, cacheKey: $scope.cachekey, isDialog: $scope.isdialog ? $scope.isdialog : false }; @@ -244,20 +246,22 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use return treeService.getTree(args) .then(function (data) { - + //Only use the tree data, if we are still on the correct section + if(data.alias !== $scope.section){ + return $q.reject(); + } + //set the data once we have it $scope.tree = data; - $scope.loading = false; - //set the root as the current active tree $scope.activeTree = $scope.tree.root; emitEvent("treeLoaded", { tree: $scope.tree }); emitEvent("treeNodeExpanded", { tree: $scope.tree, node: $scope.tree.root, children: $scope.tree.root.children }); + return $q.when(data); }, function (reason) { - $scope.loading = false; notificationsService.error("Tree Error", reason); return $q.reject(reason); }); @@ -279,7 +283,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use if (forceReload || (node.hasChildren && node.children.length === 0)) { //get the children from the tree service return treeService.loadNodeChildren({ node: node, section: $scope.section, isDialog: $scope.isdialog }) - .then(function(data) { + .then(function (data) { //emit expanded event emitEvent("treeNodeExpanded", { tree: $scope.tree, node: node, children: data }); @@ -302,7 +306,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use // TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time // it would be better if we could cache the processing. The problem is that some of these things are dynamic. - + var css = []; if (node.cssClasses) { _.each(node.cssClasses, function (c) { @@ -322,7 +326,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use }; /* helper to force reloading children of a tree node */ - $scope.loadChildren = function(node, forceReload) { + $scope.loadChildren = function (node, forceReload) { return loadChildren(node, forceReload); }; @@ -359,7 +363,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use $scope.altSelect = function (n, ev) { emitEvent("treeNodeAltSelect", { element: $element, tree: $scope.tree, node: n, event: ev }); }; - + //call the onInit method, if the result is a promise then load the tree after that resolves (if it's not a promise this will just resolve automatically). //NOTE: The promise cannot be rejected, else the tree won't be loaded and we'll get exceptions if some API calls syncTree or similar. $q.when($scope.onInit(), function (args) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 4ba4cf96bb..9c28cbe367 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -12,6 +12,7 @@ function treeSearchBox(localizationService, searchService, $q) { searchFromName: "@", showSearch: "@", section: "@", + datatypeKey: "@", hideSearchCallback: "=", searchCallback: "=" }, @@ -34,6 +35,7 @@ function treeSearchBox(localizationService, searchService, $q) { scope.showSearch = "false"; } + //used to cancel any request in progress if another one needs to take it's place var canceler = null; @@ -60,6 +62,11 @@ function treeSearchBox(localizationService, searchService, $q) { searchArgs["searchFrom"] = scope.searchFromId; } + //append dataTypeId value if there is one + if (scope.datatypeKey) { + searchArgs["dataTypeKey"] = scope.datatypeKey; + } + searcher(searchArgs).then(function (data) { scope.searchCallback(data); //set back to null so it can be re-created diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 396699866c..a51afd200c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -474,6 +474,11 @@ if (!property.inherited) { var oldPropertyModel = angular.copy(property); + if (oldPropertyModel.allowCultureVariant === undefined) { + // this is necessary for comparison when detecting changes to the property + oldPropertyModel.allowCultureVariant = scope.model.allowCultureVariant; + oldPropertyModel.alias = ""; + } var propertyModel = angular.copy(property); var propertySettings = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js index 196a28c753..9c140d572e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -71,7 +71,7 @@ } // set published state for content if (c.metaData) { - c.hasChildren = c.metaData.HasChildren; + c.hasChildren = c.metaData.hasChildren; if(scope.entityType === "Document") { c.published = c.metaData.IsPublished; } @@ -79,7 +79,7 @@ // filter items if there is a filter and it's not advanced // ** ignores advanced filter at the moment - if (scope.entityTypeFilter && !scope.entityTypeFilter.filterAdvanced) { + if (scope.entityTypeFilter && scope.entityTypeFilter.filter && !scope.entityTypeFilter.filterAdvanced) { var a = scope.entityTypeFilter.filter.toLowerCase().replace(/\s/g, '').split(','); var found = a.indexOf(c.metaData.ContentTypeAlias.toLowerCase()) >= 0; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index ea57a3fad6..b49d47b979 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -140,10 +140,9 @@ Use this directive to generate a pagination. tempPagination.push({ val: "...", isActive: false }, { name: lastLabel, val: scope.totalPages, isActive: false }); }); } - - scope.pagination = tempPagination; } + scope.pagination = tempPagination; } scope.next = function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js new file mode 100644 index 0000000000..2953347f55 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js @@ -0,0 +1,59 @@ +/** +* @ngdoc service +* @name umbraco.mocks.authMocks +* @description +* Mocks data retrival for the auth service +**/ +function authMocks($httpBackend, mocksUtils) { + + /** internal method to mock the current user to be returned */ + function getCurrentUser() { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var currentUser = { + "email":"warren@umbraco.com", + "locale":"en-US", + "emailHash":"da0673cb2c930ee247e8ba5ebe4355bf", + "userGroups":[ + "admin", + "sensitiveData" + ], + "remainingAuthSeconds":1178.2645038, + "startContentIds":[-1], + "startMediaIds":[-1], + "avatars":[ + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=30", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=60", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=90", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=150", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=300" + ], + "allowedSections":[ + "content", + "forms", + "media", + "member", + "packages", + "settings", + "users" + ], + "id":-1, + "name":"Warren Buckley" + }; + + return [200, currentUser, null]; + } + + return { + register: function () { + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Authentication/GetCurrentUser')) + .respond(getCurrentUser); + } + }; +} + +angular.module('umbraco.mocks').factory('authMocks', ['$httpBackend', 'mocksUtils', authMocks]); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js index 92c2a67d23..21f2b020dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js @@ -1,10 +1,10 @@ var umbracoAppDev = angular.module('umbraco.httpbackend', ['umbraco', 'ngMockE2E', 'umbraco.mocks']); -function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMocks, contentTypeMocks, sectionMocks, entityMocks, dataTypeMocks, dashboardMocks, macroMocks, utilMocks, localizationMocks, prevaluesMocks) { +function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMocks, contentTypeMocks, sectionMocks, entityMocks, dataTypeMocks, dashboardMocks, macroMocks, utilMocks, localizationMocks, prevaluesMocks, authMocks) { console.log("httpBackend inited"); - + //Register mocked http responses contentMocks.register(); mediaMocks.register(); @@ -19,6 +19,7 @@ function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMock localizationMocks.register(); prevaluesMocks.register(); entityMocks.register(); + authMocks.register(); $httpBackend.whenGET(/^..\/config\//).passThrough(); $httpBackend.whenGET(/^views\//).passThrough(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b807a4dc31..d714ea4938 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -1,27 +1,27 @@ /** - * @ngdoc service - * @name umbraco.resources.contentResource - * @description Handles all transactions of content data - * from the angular application to the Umbraco database, using the Content WebApi controller - * - * all methods returns a resource promise async, so all operations won't complete untill .then() is completed. - * - * @requires $q - * @requires $http - * @requires umbDataFormatter - * @requires umbRequestHelper - * - * ##usage - * To use, simply inject the contentResource into any controller or service that needs it, and make - * sure the umbraco.resources module is accesible - which it should be by default. - * - *
-  *    contentResource.getById(1234)
-  *          .then(function(data) {
-  *              $scope.content = data;
+ * @ngdoc service
+ * @name umbraco.resources.contentResource
+ * @description Handles all transactions of content data
+ * from the angular application to the Umbraco database, using the Content WebApi controller
+ *
+ * all methods returns a resource promise async, so all operations won't complete untill .then() is completed.
+ *
+ * @requires $q
+ * @requires $http
+ * @requires umbDataFormatter
+ * @requires umbRequestHelper
+ *
+ * ##usage
+ * To use, simply inject the contentResource into any controller or service that needs it, and make
+ * sure the umbraco.resources module is accesible - which it should be by default.
+ *
+ * 
+ *    contentResource.getById(1234)
+ *          .then(function(data) {
+ *              $scope.content = data;
   *          });
   * 
- **/ + **/ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { @@ -79,27 +79,27 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-          * var ids = [123,34533,2334,23434];
-          * contentResource.sort({ parentId: 1244, sortedIds: ids })
-          *    .then(function() {
-          *        $scope.complete = true;
-          *    });
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#sort
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Sorts all children below a given parent node id, based on a collection of node-ids
+         *
+         * ##usage
+         * 
+         * var ids = [123,34533,2334,23434];
+         * contentResource.sort({ parentId: 1244, sortedIds: ids })
+         *    .then(function() {
+         *        $scope.complete = true;
+         *    });
           * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ + * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ sort: function (args) { if (!args) { throw "args cannot be null"; @@ -121,28 +121,28 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-          * contentResource.move({ parentId: 1244, id: 123 })
-          *    .then(function() {
-          *        alert("node was moved");
-          *    }, function(err){
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#move
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Moves a node underneath a new parentId
+         *
+         * ##usage
+         * 
+         * contentResource.move({ parentId: 1244, id: 123 })
+         *    .then(function() {
+         *        alert("node was moved");
+         *    }, function(err){
           *      alert("node didnt move:" + err.data.Message);
-          *    });
+         *    });
           * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ move: function (args) { if (!args) { throw "args cannot be null"; @@ -164,29 +164,29 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-          * contentResource.copy({ parentId: 1244, id: 123 })
-          *    .then(function() {
-          *        alert("node was copied");
-          *    }, function(err){
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#copy
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Copies a node underneath a new parentId
+         *
+         * ##usage
+         * 
+         * contentResource.copy({ parentId: 1244, id: 123 })
+         *    .then(function() {
+         *        alert("node was copied");
+         *    }, function(err){
           *      alert("node wasnt copy:" + err.data.Message);
-          *    });
+         *    });
           * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ + * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ copy: function (args) { if (!args) { throw "args cannot be null"; @@ -205,26 +205,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method + * @ngdoc method * @name umbraco.resources.contentResource#unpublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Unpublishes a content item with a given Id
+         *
+         * ##usage
+         * 
           * contentResource.unpublish(1234)
-          *    .then(function() {
-          *        alert("node was unpulished");
-          *    }, function(err){
+         *    .then(function() {
+         *        alert("node was unpulished");
+         *    }, function(err){
           *      alert("node wasnt unpublished:" + err.data.Message);
-          *    });
+         *    });
           * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ + * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ unpublish: function (id, cultures) { if (!id) { throw "id cannot be null"; @@ -242,7 +242,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to publish content with id ' + id); }, /** - * @ngdoc method + * @ngdoc method * @name umbraco.resources.contentResource#getCultureAndDomains * @methodOf umbraco.resources.contentResource * @@ -281,23 +281,23 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-          * contentResource.emptyRecycleBin()
-          *    .then(function() {
-          *        alert('its empty!');
-          *    });
+         * @name umbraco.resources.contentResource#emptyRecycleBin
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Empties the content recycle bin
+         *
+         * ##usage
+         * 
+         * contentResource.emptyRecycleBin()
+         *    .then(function() {
+         *        alert('its empty!');
+         *    });
           * 
* - * @returns {Promise} resourcePromise object. - * - */ + * @returns {Promise} resourcePromise object. + * + */ emptyRecycleBin: function () { return umbRequestHelper.resourcePromise( $http.post( @@ -308,25 +308,25 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-          * contentResource.deleteById(1234)
-          *    .then(function() {
-          *        alert('its gone!');
-          *    });
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#deleteById
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Deletes a content item with a given id
+         *
+         * ##usage
+         * 
+         * contentResource.deleteById(1234)
+         *    .then(function() {
+         *        alert('its gone!');
+         *    });
           * 
* * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ + * @returns {Promise} resourcePromise object. + * + */ deleteById: function (id) { return umbRequestHelper.resourcePromise( $http.post( @@ -348,27 +348,27 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#getById
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Gets a content item with a given id
+         *
+         * ##usage
+         * 
+         * contentResource.getById(1234)
+         *    .then(function(content) {
           *        var myDoc = content;
-          *        alert('its here!');
-          *    });
+         *        alert('its here!');
+         *    });
           * 
* * @param {Int} id id of content item to return * @param {Int} culture optional culture to retrieve the item in - * @returns {Promise} resourcePromise object containing the content item. - * - */ + * @returns {Promise} resourcePromise object containing the content item. + * + */ getById: function (id) { return umbRequestHelper.resourcePromise( $http.get( @@ -419,26 +419,26 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-          * contentResource.getByIds( [1234,2526,28262])
-          *    .then(function(contentArray) {
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#getByIds
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Gets an array of content items, given a collection of ids
+         *
+         * ##usage
+         * 
+         * contentResource.getByIds( [1234,2526,28262])
+         *    .then(function(contentArray) {
           *        var myDoc = contentArray;
-          *        alert('they are here!');
-          *    });
+         *        alert('they are here!');
+         *    });
           * 
* * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ + * @returns {Promise} resourcePromise object containing the content items array. + * + */ getByIds: function (ids) { var idQuery = ""; @@ -464,37 +464,37 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content + * - Parent Id must be provided so umbraco knows where to store the content * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold * - * The scaffold is used to build editors for content that has not yet been populated with data. + * The scaffold is used to build editors for content that has not yet been populated with data. * - * ##usage - *
-          * contentResource.getScaffold(1234, 'homepage')
-          *    .then(function(scaffold) {
-          *        var myDoc = scaffold;
+         * ##usage
+         * 
+         * contentResource.getScaffold(1234, 'homepage')
+         *    .then(function(scaffold) {
+         *        var myDoc = scaffold;
           *        myDoc.name = "My new document";
-          *
-          *        contentResource.publish(myDoc, true)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and published again");
-          *            });
-          *    });
+         *
+         *        contentResource.publish(myDoc, true)
+         *            .then(function(content){
+         *                alert("Retrieved, updated and published again");
+         *            });
+         *    });
           * 
* - * @param {Int} parentId id of content item to return + * @param {Int} parentId id of content item to return * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ getScaffold: function (parentId, alias) { return umbRequestHelper.resourcePromise( @@ -524,25 +524,25 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-          * contentResource.getNiceUrl(id)
-          *    .then(function(url) {
-          *        alert('its here!');
-          *    });
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#getNiceUrl
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Returns a url, given a node ID
+         *
+         * ##usage
+         * 
+         * contentResource.getNiceUrl(id)
+         *    .then(function(url) {
+         *        alert('its here!');
+         *    });
           * 
* - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ getNiceUrl: function (id) { return umbRequestHelper.resourcePromise( $http.get( @@ -554,33 +554,33 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-          * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-          *    .then(function(contentArray) {
+         * @ngdoc method
+         * @name umbraco.resources.contentResource#getChildren
+         * @methodOf umbraco.resources.contentResource
+         *
+         * @description
+         * Gets children of a content item with a given id
+         *
+         * ##usage
+         * 
+         * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+         *    .then(function(contentArray) {
           *        var children = contentArray;
-          *        alert('they are here!');
-          *    });
+         *        alert('they are here!');
+         *    });
           * 
* - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` * @param {String} options.cultureName if provided, the results will be for this specific culture/variant - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ getChildren: function (parentId, options) { var defaults = { @@ -651,34 +651,34 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name!";
-          *          contentResource.save(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and saved again");
-          *            });
-          *    });
+         * ##usage
+         * 
+         * contentResource.getById(1234)
+         *    .then(function(content) {
+         *          content.name = "I want a new name!";
+         *          contentResource.save(content, false)
+         *            .then(function(content){
+         *                alert("Retrieved, updated and saved again");
+         *            });
+         *    });
           * 
* - * @param {Object} content The content item object with changes applied + * @param {Object} content The content item object with changes applied * @param {Bool} isNew set to true to create a new item or to update an existing * @param {Array} files collection of files for the document * @param {Bool} showNotifications an option to disable/show notifications (default is true) - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ save: function (content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", @@ -694,34 +694,34 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation * if the content item needs to have files attached, they must be provided as the files param and passed separately * * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name, and be published!";
-          *          contentResource.publish(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and published again");
-          *            });
-          *    });
+         * ##usage
+         * 
+         * contentResource.getById(1234)
+         *    .then(function(content) {
+         *          content.name = "I want a new name, and be published!";
+         *          contentResource.publish(content, false)
+         *            .then(function(content){
+         *                alert("Retrieved, updated and published again");
+         *            });
+         *    });
           * 
* - * @param {Object} content The content item object with changes applied + * @param {Object} content The content item object with changes applied * @param {Bool} isNew set to true to create a new item or to update an existing * @param {Array} files collection of files for the document * @param {Bool} showNotifications an option to disable/show notifications (default is true) - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ publish: function (content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", @@ -743,31 +743,31 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name, and be published!";
-          *          contentResource.sendToPublish(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and notication send off");
-          *            });
-          *    });
+         * ##usage
+         * 
+         * contentResource.getById(1234)
+         *    .then(function(content) {
+         *          content.name = "I want a new name, and be published!";
+         *          contentResource.sendToPublish(content, false)
+         *            .then(function(content){
+         *                alert("Retrieved, updated and notication send off");
+         *            });
+         *    });
           * 
* - * @param {Object} content The content item object with changes applied + * @param {Object} content The content item object with changes applied * @param {Bool} isNew set to true to create a new item or to update an existing * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ sendToPublish: function (content, isNew, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", @@ -797,25 +797,25 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-          * contentResource.publishById(1234)
-          *    .then(function(content) {
-          *        alert("published");
-          *    });
+         * ##usage
+         * 
+         * contentResource.publishById(1234)
+         *    .then(function(content) {
+         *        alert("published");
+         *    });
           * 
* - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ publishById: function (id) { if (!id) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 753d180880..69522f3fa4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -2,10 +2,10 @@ * @ngdoc service * @name umbraco.resources.entityResource * @description Loads in basic data for all entities - * + * * ##What is an entity? * An entity is a basic **read-only** representation of an Umbraco node. It contains only the most - * basic properties used to display the item in trees, lists and navigation. + * basic properties used to display the item in trees, lists and navigation. * * ##What is the difference between entity and content/media/etc...? * the entity only contains the basic node data, name, id and guid, whereas content @@ -15,7 +15,7 @@ * * ##Entity object types? * You need to specify the type of object you want returned. - * + * * The core object types are: * * - Document @@ -35,7 +35,7 @@ function entityResource($q, $http, umbRequestHelper) { //the factory object returned return { - + getSafeAlias: function (value, camelCase) { if (!value) { @@ -64,10 +64,10 @@ function entityResource($q, $http, umbRequestHelper) { * .then(function(pathArray) { * alert('its here!'); * }); - *
- * + *
+ * * @param {Int} id Id of node to return the public url to - * @param {string} type Object type name + * @param {string} type Object type name * @returns {Promise} resourcePromise object containing the url. * */ @@ -100,8 +100,8 @@ function entityResource($q, $http, umbRequestHelper) { * .then(function(url) { * alert('its here!'); * }); - *
- * + *
+ * * @param {Int} id Id of node to return the public url to * @param {string} type Object type name * @returns {Promise} resourcePromise object containing the url. @@ -135,17 +135,17 @@ function entityResource($q, $http, umbRequestHelper) { * //get media by id * entityResource.getEntityById(0, "Media") * .then(function(ent) { - * var myDoc = ent; + * var myDoc = ent; * alert('its here!'); * }); - *
- * + *
+ * * @param {Int} id id of entity to return - * @param {string} type Object type name + * @param {string} type Object type name * @returns {Promise} resourcePromise object containing the entity. * */ - getById: function (id, type) { + getById: function (id, type) { if (id === -1 || id === "-1") { return null; @@ -160,6 +160,39 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve entity data for id ' + id); }, + + getUrlAndAnchors: function (id) { + + if (id === -1 || id === "-1") { + return null; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetUrlAndAnchors", + { id: id })), + 'Failed to retrieve url and anchors data for id ' + id); + }, + + getAnchors: function (rteContent) { + + if (!rteContent || rteContent.length === 0) { + return []; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + 'GetAnchors'), + { + rteContent: rteContent + }), + 'Failed to anchors data for rte content ' + rteContent); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getByIds @@ -173,18 +206,18 @@ function entityResource($q, $http, umbRequestHelper) { * //Get templates for ids * entityResource.getEntitiesByIds( [1234,2526,28262], "Template") * .then(function(templateArray) { - * var myDoc = contentArray; + * var myDoc = contentArray; * alert('they are here!'); * }); - *
- * + *
+ * * @param {Array} ids ids of entities to return as an array - * @param {string} type type name + * @param {string} type type name * @returns {Promise} resourcePromise object containing the entity array. * */ getByIds: function (ids, type) { - + var query = "type=" + type; return umbRequestHelper.resourcePromise( @@ -212,14 +245,14 @@ function entityResource($q, $http, umbRequestHelper) { * //get content by xpath * entityResource.getByQuery("$current", -1, "Document") * .then(function(ent) { - * var myDoc = ent; + * var myDoc = ent; * alert('its here!'); * }); - *
- * + *
+ * * @param {string} query xpath to use in query * @param {Int} nodeContextId id id to start from - * @param {string} type Object type name + * @param {string} type Object type name * @returns {Promise} resourcePromise object containing the entity. * */ @@ -247,12 +280,12 @@ function entityResource($q, $http, umbRequestHelper) { * //Only return media * entityResource.getAll("Media") * .then(function(ent) { - * var myDoc = ent; + * var myDoc = ent; * alert('its here!'); * }); - *
- * - * @param {string} type Object type name + *
+ * + * @param {string} type Object type name * @param {string} postFilter optional filter expression which will execute a dynamic where clause on the server * @returns {Promise} resourcePromise object containing the entity. * @@ -277,24 +310,36 @@ function entityResource($q, $http, umbRequestHelper) { * * @description * Gets ancestor entities for a given item - * - * + * + * * @param {string} type Object type name * @param {string} culture Culture * @returns {Promise} resourcePromise object containing the entity. * */ - getAncestors: function (id, type, culture) { - if (culture === undefined) culture = ""; + getAncestors: function (id, type, culture, options) { + if (!culture) { + culture = ""; + } + + var args = [ + { id: id }, + { type: type }, + { culture: culture} + ]; + if (options && options.dataTypeKey) { + args.push({ dataTypeKey: options.dataTypeKey }); + } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetAncestors", - [{ id: id }, { type: type }, { culture: culture }])), - 'Failed to retrieve ancestor data for id ' + id); + args)), + 'Failed to retrieve ancestor data for id ' + id); }, - + /** * @ngdoc method * @name umbraco.resources.entityResource#getChildren @@ -302,20 +347,25 @@ function entityResource($q, $http, umbRequestHelper) { * * @description * Gets children entities for a given item - * + * * @param {Int} parentid id of content item to return children of - * @param {string} type Object type name + * @param {string} type Object type name * @returns {Promise} resourcePromise object containing the entity. * */ - getChildren: function (id, type) { + getChildren: function (id, type, options) { + + var args = [{ id: id }, { type: type }]; + if (options && options.dataTypeKey) { + args.push({ dataTypeKey: options.dataTypeKey }); + } return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetChildren", - [{ id: id }, { type: type }])), + args)), 'Failed to retrieve child data for id ' + id); }, @@ -331,11 +381,11 @@ function entityResource($q, $http, umbRequestHelper) { *
           * entityResource.getPagedChildren(1234, "Content", {pageSize: 10, pageNumber: 2})
           *    .then(function(contentArray) {
-          *        var children = contentArray; 
+          *        var children = contentArray;
           *        alert('they are here!');
           *    });
-          * 
- * + *
+ * * @param {Int} parentid id of content item to return children of * @param {string} type Object type name * @param {Object} options optional options object @@ -383,7 +433,8 @@ function entityResource($q, $http, umbRequestHelper) { pageSize: options.pageSize, orderBy: options.orderBy, orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) + filter: encodeURIComponent(options.filter), + dataTypeKey: options.dataTypeKey } )), 'Failed to retrieve child data for id ' + parentId); @@ -401,11 +452,11 @@ function entityResource($q, $http, umbRequestHelper) { *
           * entityResource.getPagedDescendants(1234, "Document", {pageSize: 10, pageNumber: 2})
           *    .then(function(contentArray) {
-          *        var children = contentArray; 
+          *        var children = contentArray;
           *        alert('they are here!');
           *    });
-          * 
- * + *
+ * * @param {Int} parentid id of content item to return descendants of * @param {string} type Object type name * @param {Object} options optional options object @@ -424,7 +475,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + dataTypeKey: null }; if (options === undefined) { options = {}; @@ -453,12 +505,14 @@ function entityResource($q, $http, umbRequestHelper) { pageSize: options.pageSize, orderBy: options.orderBy, orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) + filter: encodeURIComponent(options.filter), + dataTypeKey: options.dataTypeKey } )), 'Failed to retrieve child data for id ' + parentId); }, - + + /** * @ngdoc method * @name umbraco.resources.entityResource#search @@ -471,23 +525,27 @@ function entityResource($q, $http, umbRequestHelper) { *
          * entityResource.search("news", "Media")
          *    .then(function(mediaArray) {
-         *        var myDoc = mediaArray; 
+         *        var myDoc = mediaArray;
          *        alert('they are here!');
          *    });
-         * 
- * - * @param {String} Query search query - * @param {String} Type type of conten to search + *
+ * + * @param {String} Query search query + * @param {String} Type type of conten to search * @returns {Promise} resourcePromise object containing the entity array. * */ - search: function (query, type, searchFrom, canceler) { + search: function (query, type, searchFrom, canceler, dataTypeKey) { var args = [{ query: query }, { type: type }]; if (searchFrom) { args.push({ searchFrom: searchFrom }); } + if (dataTypeKey) { + args.push({ dataTypeKey: dataTypeKey }); + } + var httpConfig = {}; if (canceler) { httpConfig["timeout"] = canceler; @@ -502,7 +560,7 @@ function entityResource($q, $http, umbRequestHelper) { httpConfig), 'Failed to retrieve entity data for query ' + query); }, - + /** * @ngdoc method @@ -516,12 +574,12 @@ function entityResource($q, $http, umbRequestHelper) { *
          * entityResource.searchAll("bob")
          *    .then(function(array) {
-         *        var myDoc = array; 
+         *        var myDoc = array;
          *        alert('they are here!');
          *    });
-         * 
- * - * @param {String} Query search query + *
+ * + * @param {String} Query search query * @returns {Promise} resourcePromise object containing the entity array. * */ @@ -541,7 +599,9 @@ function entityResource($q, $http, umbRequestHelper) { httpConfig), 'Failed to retrieve entity data for query ' + query); } - + + + }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 1d6d5171a1..ca7700c188 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -1,16 +1,16 @@ /** - * @ngdoc service - * @name umbraco.resources.mediaResource - * @description Loads in data for media - **/ + * @ngdoc service + * @name umbraco.resources.mediaResource + * @description Loads in data for media + **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { /** internal method process the saving of data and post processing the result */ function saveMediaItem(content, action, files) { return umbRequestHelper.postSaveContent({ restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), + "mediaApiBaseUrl", + "PostSave"), content: content, action: action, files: files, @@ -24,35 +24,35 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { getRecycleBin: function () { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for media recycle bin'); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-          * var ids = [123,34533,2334,23434];
-          * mediaResource.sort({ sortedIds: ids })
-          *    .then(function() {
-          *        $scope.complete = true;
-          *    });
-          * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+         * var ids = [123,34533,2334,23434];
+         * mediaResource.sort({ sortedIds: ids })
+         *    .then(function() {
+         *        $scope.complete = true;
+         *    });
+         * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ sort: function (args) { if (!args) { throw "args cannot be null"; @@ -65,37 +65,37 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort media'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-          * mediaResource.move({ parentId: 1244, id: 123 })
-          *    .then(function() {
-          *        alert("node was moved");
-          *    }, function(err){
-          *      alert("node didnt move:" + err.data.Message);
-          *    });
-          * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+         * mediaResource.move({ parentId: 1244, id: 123 })
+         *    .then(function() {
+         *        alert("node was moved");
+         *    }, function(err){
+         *      alert("node didnt move:" + err.data.Message);
+         *    });
+         * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ move: function (args) { if (!args) { throw "args cannot be null"; @@ -108,121 +108,121 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id }, {responseType: 'text'}), - { - error: function(data){ - var errorMsg = 'Failed to move media'; - if (data.id !== undefined && data.parentId !== undefined) { - if (data.id === data.parentId) { - errorMsg = 'Media can\'t be moved into itself'; - } - } - else if (data.notifications !== undefined) { - if (data.notifications.length > 0) { - if (data.notifications[0].header.length > 0) { - errorMsg = data.notifications[0].header; - } - if (data.notifications[0].message.length > 0) { - errorMsg = errorMsg + ": " + data.notifications[0].message; - } + { + error: function(data){ + var errorMsg = 'Failed to move media'; + if (data.id !== undefined && data.parentId !== undefined) { + if (data.id === data.parentId) { + errorMsg = 'Media can\'t be moved into itself'; + } + } + else if (data.notifications !== undefined) { + if (data.notifications.length > 0) { + if (data.notifications[0].header.length > 0) { + errorMsg = data.notifications[0].header; + } + if (data.notifications[0].message.length > 0) { + errorMsg = errorMsg + ": " + data.notifications[0].message; } } + } - return { - errorMsg: errorMsg - }; - } - }); + return { + errorMsg: errorMsg + }; + } + }); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
-          * mediaResource.getById(1234)
-          *    .then(function(media) {
-          *        var myMedia = media;
-          *        alert('its here!');
-          *    });
-          * 
- * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
+         * mediaResource.getById(1234)
+         *    .then(function(media) {
+         *        var myMedia = media;
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ getById: function (id) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for media id ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
-          * mediaResource.deleteById(1234)
-          *    .then(function() {
-          *        alert('its gone!');
-          *    });
-          * 
- * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
+         * mediaResource.deleteById(1234)
+         *    .then(function() {
+         *        alert('its gone!');
+         *    });
+         * 
+ * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ deleteById: function (id) { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
-          * mediaResource.getByIds( [1234,2526,28262])
-          *    .then(function(mediaArray) {
-          *        var myDoc = contentArray;
-          *        alert('they are here!');
-          *    });
-          * 
- * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
+         * mediaResource.getByIds( [1234,2526,28262])
+         *    .then(function(mediaArray) {
+         *        var myDoc = contentArray;
+         *        alert('they are here!');
+         *    });
+         * 
+ * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ getByIds: function (ids) { var idQuery = ""; @@ -231,96 +231,96 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { }); return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for media ids ' + ids); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
-          * mediaResource.getScaffold(1234, 'folder')
-          *    .then(function(scaffold) {
-          *        var myDoc = scaffold;
-          *        myDoc.name = "My new media item";
-          *
-          *        mediaResource.save(myDoc, true)
-          *            .then(function(media){
-          *                alert("Retrieved, updated and saved again");
-          *            });
-          *    });
-          * 
- * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
+         * mediaResource.getScaffold(1234, 'folder')
+         *    .then(function(scaffold) {
+         *        var myDoc = scaffold;
+         *        myDoc.name = "My new media item";
+         *
+         *        mediaResource.save(myDoc, true)
+         *            .then(function(media){
+         *                alert("Retrieved, updated and saved again");
+         *            });
+         *    });
+         * 
+ * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ getScaffold: function (parentId, alias) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty media item type ' + alias); }, rootMedia: function () { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), + 'Failed to retrieve data for root media'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
-          * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-          *    .then(function(contentArray) {
-          *        var children = contentArray;
-          *        alert('they are here!');
-          *    });
-          * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
+         * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+         *    .then(function(contentArray) {
+         *        var children = contentArray;
+         *        alert('they are here!');
+         *    });
+         * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ getChildren: function (parentId, options) { var defaults = { @@ -361,111 +361,111 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: toBool(options.orderBySystemField) }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), + 'Failed to retrieve children for media item ' + parentId); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-          * mediaResource.getById(1234)
-          *    .then(function(media) {
-          *          media.name = "I want a new name!";
-          *          mediaResource.save(media, false)
-          *            .then(function(media){
-          *                alert("Retrieved, updated and saved again");
-          *            });
-          *    });
-          * 
- * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+         * mediaResource.getById(1234)
+         *    .then(function(media) {
+         *          media.name = "I want a new name!";
+         *          mediaResource.save(media, false)
+         *            .then(function(media){
+         *                alert("Retrieved, updated and saved again");
+         *            });
+         *    });
+         * 
+ * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ save: function (media, isNew, files) { return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
-          * mediaResource.addFolder("My gallery", 1234)
-          *    .then(function(folder) {
-          *        alert('New folder');
-          *    });
-          * 
- * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
+         * mediaResource.addFolder("My gallery", 1234)
+         *    .then(function(folder) {
+         *        alert('New folder');
+         *    });
+         * 
+ * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ addFolder: function (name, parentId) { return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); + $http.post(umbRequestHelper + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), + 'Failed to add folder'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildFolders - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves all media children with types used as folders. - * Uses the convention of looking for media items with mediaTypes ending in - * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, - * + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * * NOTE: This will return a page of max 500 folders, if more is required it needs to be paged * and then folders are in the .items property of the returned promise data - * - * ##usage - *
-          * mediaResource.getChildFolders(1234)
+         *
+         * ##usage
+         * 
+         * mediaResource.getChildFolders(1234)
           *    .then(function(page) {
-          *        alert('folders');
-          *    });
-          * 
- * - * @param {int} parentId Id of the media item to query for child folders - * @returns {Promise} resourcePromise object. - * - */ + * alert('folders'); + * }); + *
+ * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ getChildFolders: function (parentId) { if (!parentId) { parentId = -1; @@ -473,69 +473,69 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { //NOTE: This will return a max of 500 folders, if more is required it needs to be paged return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - { + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + { id: parentId, pageNumber: 1, pageSize: 500 - })), + })), 'Failed to retrieve child folders for media item ' + parentId); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
-          * mediaResource.emptyRecycleBin()
-          *    .then(function() {
-          *        alert('its empty!');
-          *    });
-          * 
- * - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+         * mediaResource.emptyRecycleBin()
+         *    .then(function() {
+         *        alert('its empty!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object. + * + */ emptyRecycleBin: function () { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#search - * @methodOf umbraco.resources.mediaResource - * - * @description - * Paginated search for media items starting on the supplied nodeId - * - * ##usage - *
-          * mediaResource.search("my search", 1, 100, -1)
-          *    .then(function(searchResult) {
-          *        alert('it's here!');
-          *    });
-          * 
- * - * @param {string} query The search query - * @param {int} pageNumber The page number - * @param {int} pageSize The number of media items on a page - * @param {int} searchFrom NodeId to search from (-1 for root) - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#search + * @methodOf umbraco.resources.mediaResource + * + * @description + * Paginated search for media items starting on the supplied nodeId + * + * ##usage + *
+         * mediaResource.search("my search", 1, 100, -1)
+         *    .then(function(searchResult) {
+         *        alert('it's here!');
+         *    });
+         * 
+ * + * @param {string} query The search query + * @param {int} pageNumber The page number + * @param {int} pageSize The number of media items on a page + * @param {int} searchFrom NodeId to search from (-1 for root) + * @returns {Promise} resourcePromise object. + * + */ search: function (query, pageNumber, pageSize, searchFrom) { var args = [ @@ -546,12 +546,12 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { ]; return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "Search", - args)), - 'Failed to retrieve media items for search: ' + query); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "Search", + args)), + 'Failed to retrieve media items for search: ' + query); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 1b2be5f635..732f682082 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -5,7 +5,7 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, editorState, notificationsService, navigationService, localizationService, serverValidationManager, formHelper) { function isValidIdentifier(id) { @@ -34,9 +34,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return { + //TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire + //service should only be used for content/media/members + /** Used by the content editor and mini content editor to perform saving operations */ - // TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms - // = this is already done in the formhelper service contentEditorPerformSave: function (args) { if (!angular.isObject(args)) { throw "args must be an object"; @@ -53,18 +54,19 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (args.showNotifications === undefined) { args.showNotifications = true; } - - var redirectOnSuccess = args.redirectOnSuccess !== undefined ? args.redirectOnSuccess : true; - var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true; + if (args.softRedirect === undefined) { + //when true, the url will change but it won't actually re-route + //this is merely here for compatibility, if only the content/media/members used this service we'd prob be ok but tons of editors + //use this service unfortunately and probably packages too. + args.softRedirect = false; + } var self = this; //we will use the default one for content if not specified var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, action: args.action })) { - - args.scope.busy = true; + if (formHelper.submitForm({ scope: args.scope, action: args.action })) { return args.saveMethod(args.content, $routeParams.create, fileManager.getFiles(), args.showNotifications) .then(function (data) { @@ -74,26 +76,30 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica self.handleSuccessfulSave({ scope: args.scope, savedContent: data, - redirectOnSuccess: redirectOnSuccess, + softRedirect: args.softRedirect, rebindCallback: function () { rebindCallback.apply(self, [args.content, data]); } }); - args.scope.busy = false; + //update editor state to what is current + editorState.set(args.content); + return $q.resolve(data); }, function (err) { self.handleSaveError({ showNotifications: args.showNotifications, - redirectOnFailure: redirectOnFailure, + softRedirect: args.softRedirect, err: err, rebindCallback: function () { rebindCallback.apply(self, [args.content, err.data]); } }); - args.scope.busy = false; + //update editor state to what is current + editorState.set(args.content); + return $q.reject(err); }); } @@ -265,7 +271,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // if publishing is allowed also allow schedule publish // we add this manually becuase it doesn't have a permission so it wont // get picked up by the loop through permissions - if( _.contains(args.content.allowedActions, "U")) { + if (_.contains(args.content.allowedActions, "U")) { buttons.subButtons.push(createButtonDefinition("SCHEDULE")); buttons.subButtons.push(createButtonDefinition("PUBLISH_DESCENDANTS")); } @@ -274,7 +280,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica // so long as it's already published and if the user has access to publish // and the user has access to unpublish (may have been removed via Event) if (!args.create) { - var hasPublishedVariant = args.content.variants.filter(function(variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0; + var hasPublishedVariant = args.content.variants.filter(function (variant) { return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); }).length > 0; if (hasPublishedVariant && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) { buttons.subButtons.push(createButtonDefinition("Z")); } @@ -444,7 +450,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var shouldIgnore = function (propName) { return _.some([ "variants", - + "tabs", "properties", "apps", @@ -574,15 +580,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * A function to handle what happens when we have validation issues from the server side * */ + + //TODO: Too many editors use this method for saving when this entire service should only be used for content/media/members, + // there is formHelper.handleError for other editors which should be used! + handleSaveError: function (args) { if (!args.err) { throw "args.err cannot be null"; } - if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) { - throw "args.redirectOnFailure must be set to true or false"; - } - + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). //Or, some strange server error @@ -600,16 +607,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } - if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { - //we are not redirecting because this is not new content, it is existing content. In this case - // we need to detect what properties have changed and re-bind them with the server data. Then we need - // to re-bind any server validation errors after the digest takes place. + if (!this.redirectToCreatedContent(args.err.data.id) || args.softRedirect) { + // If we are not redirecting it's because this is not newly created content, else in some cases we are + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). + // In this case we need to detect what properties have changed and re-bind them with the server data. if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { args.rebindCallback(); } - //notify all validators (don't clear the server validations though since we need to maintain their state because of + // In this case notify all validators (don't clear the server validations though since we need to maintain their state because of // how the variant switcher works in content). server validation state is always cleared when an editor first loads // and in theory when an editor is destroyed. serverValidationManager.notify(); @@ -633,6 +640,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * ensure the notifications are displayed and that the appropriate events are fired. This will also check if we need to redirect * when we're creating new content. */ + + //TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire + //service should only be used for content/media/members + handleSuccessfulSave: function (args) { if (!args) { @@ -642,14 +653,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica throw "args.savedContent cannot be null"; } - // the default behaviour is to redirect on success. This adds option to prevent when false - args.redirectOnSuccess = args.redirectOnSuccess !== undefined ? args.redirectOnSuccess : true; + if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id) || args.softRedirect) { - if (!args.redirectOnSuccess || !this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { + // If we are not redirecting it's because this is not newly created content, else in some cases we are + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). - //we are not redirecting because this is not new content, it is existing content. In this case - // we need to detect what properties have changed and re-bind them with the server data. - //call the callback + // In this case we need to detect what properties have changed and re-bind them with the server data. if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { args.rebindCallback(); } @@ -667,7 +676,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * We need to decide if we need to redirect to edito mode or if we will remain in create mode. * We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID */ - redirectToCreatedContent: function (id, modelState) { + redirectToCreatedContent: function (id) { //only continue if we are currently in create mode and not in infinite mode and if the resulting ID is valid if ($routeParams.create && (isValidIdentifier(id))) { @@ -679,7 +688,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //clear the query strings navigationService.clearSearch(["cculture"]); - + //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this @@ -699,6 +708,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * For some editors like scripts or entites that have names as ids, these names can change and we need to redirect * to their new paths, this is helper method to do that. */ + + //TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire + //service should only be used for content/media/members + redirectToRenamedContent: function (id) { //clear the query strings navigationService.clearSearch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index a97773f77e..72957e1c72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -368,6 +368,28 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#contentTypePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a content type picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * + * @returns {Object} editor object + */ + function contentTypePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "documentTypes"; + open(editor); + } /** * @ngdoc method * @name umbraco.services.editorService#copy @@ -881,6 +903,7 @@ When building a custom infinite editor view you can use the same components as a mediaEditor: mediaEditor, contentEditor: contentEditor, contentPicker: contentPicker, + contentTypePicker: contentTypePicker, copy: copy, move: move, embed: embed, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js index 5e42af9c5e..97a9ac5c4b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js @@ -5,13 +5,15 @@ * * @description * Tracks the parent object for complex editors by exposing it as - * an object reference via editorState.current.entity + * an object reference via editorState.current.getCurrent(). + * The state is cleared on each successful route. * * it is possible to modify this object, so should be used with care */ -angular.module('umbraco.services').factory("editorState", function() { +angular.module('umbraco.services').factory("editorState", function ($rootScope) { var current = null; + var state = { /** @@ -40,7 +42,7 @@ angular.module('umbraco.services').factory("editorState", function() { * Since the editorstate entity is read-only, you cannot set it to null * only through the reset() method */ - reset: function() { + reset: function () { current = null; }, @@ -59,9 +61,10 @@ angular.module('umbraco.services').factory("editorState", function() { * editorState.current can not be overwritten, you should only read values from it * since modifying individual properties should be handled by the property editors */ - getCurrent: function() { + getCurrent: function () { return current; } + }; // TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing. @@ -76,5 +79,13 @@ angular.module('umbraco.services').factory("editorState", function() { } }); + //execute on each successful route (this is only bound once per application since a service is a singleton) + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + + //reset the editorState on each successful route chage + state.reset(); + + }); + return state; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 6d319ad90a..8fe6761c94 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -8,11 +8,12 @@ * that need to attach files. * When a route changes successfully, we ensure that the collection is cleared. */ -function fileManager() { +function fileManager($rootScope) { var fileCollection = []; - return { + + var mgr = { /** * @ngdoc function * @name umbraco.services.fileManager#addFiles @@ -24,7 +25,7 @@ function fileManager() { * for the files collection that effectively clears the files for the specified editor. */ setFiles: function (args) { - + //propertyAlias, files if (!angular.isString(args.propertyAlias)) { throw "args.propertyAlias must be a non empty string"; @@ -52,7 +53,7 @@ function fileManager() { fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData }); } }, - + /** * @ngdoc function * @name umbraco.services.fileManager#getFiles @@ -62,10 +63,10 @@ function fileManager() { * @description * Returns all of the files attached to the file manager */ - getFiles: function() { + getFiles: function () { return fileCollection; }, - + /** * @ngdoc function * @name umbraco.services.fileManager#clearFiles @@ -78,7 +79,17 @@ function fileManager() { clearFiles: function () { fileCollection = []; } -}; + }; + + //execute on each successful route (this is only bound once per application since a service is a singleton) + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + //reset the file manager on each route change, the file collection is only relavent + // when working in an editor and submitting data to the server. + //This ensures that memory remains clear of any files and that the editors don't have to manually clear the files. + mgr.clearFiles(); + }); + + return mgr; } angular.module('umbraco.services').factory('fileManager', fileManager); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index f8d95ef1b3..0555318bae 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -40,7 +40,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService else { currentForm = args.formCtrl; } - + //the first thing any form must do is broadcast the formSubmitting event args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); @@ -53,10 +53,10 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //reset the server validations serverValidationManager.reset(); - + return true; }, - + /** * @ngdoc function * @name umbraco.services.formHelper#submitForm @@ -75,21 +75,21 @@ function formHelper(angularHelper, serverValidationManager, notificationsService if (!args.scope) { throw "args.scope cannot be null"; } - + args.scope.$broadcast("formSubmitted", { scope: args.scope }); }, showNotifications: function (args) { - if (!args || !args.notifications) { - return false; - } - if (angular.isArray(args.notifications)) { - for (var i = 0; i < args.notifications.length; i++) { - notificationsService.showNotification(args.notifications[i]); + if (!args || !args.notifications) { + return false; } - return true; - } - return false; + if (angular.isArray(args.notifications)) { + for (var i = 0; i < args.notifications.length; i++) { + notificationsService.showNotification(args.notifications[i]); + } + return true; + } + return false; }, /** @@ -104,7 +104,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * * @param {object} err The error object returned from the http promise */ - handleError: function (err) { + handleError: function (err) { + + //TODO: Potentially add in the logic to showNotifications like the contentEditingHelper.handleSaveError does so that + // non content editors can just use this method instead of contentEditingHelper.handleSaveError which they should not use + // and they won't need to manually do it. + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). //Or, some strange server error @@ -116,7 +121,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService this.handleServerValidation(err.data.ModelState); //execute all server validation events and subscribers - serverValidationManager.notifyAndClearAllSubscriptions(); + serverValidationManager.notifyAndClearAllSubscriptions(); } } else { @@ -124,7 +129,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService // TODO: All YSOD handling should be done with an interceptor overlayService.ysod(err); } - + }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index ea2ad73263..6081cbd9ad 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -29,35 +29,48 @@ angular.module('umbraco.services') var resourceFileLoadStatus = "none"; var resourceLoadingPromise = []; - function _lookup(value, tokens, dictionary) { + // array to hold the localized resource string entries + var innerDictionary = []; + + function _lookup(alias, tokens, dictionary, fallbackValue) { //strip the key identifier if its there - if (value && value[0] === "@") { - value = value.substring(1); + if (alias && alias[0] === "@") { + alias = alias.substring(1); } + var underscoreIndex = alias.indexOf("_"); //if no area specified, add general_ - if (value && value.indexOf("_") < 0) { - value = "general_" + value; + if (alias && underscoreIndex < 0) { + alias = "general_" + alias; + underscoreIndex = alias.indexOf("_"); } - var entry = dictionary[value]; - if (entry) { - return service.tokenReplace(entry, tokens); + var areaAlias = alias.substring(0, underscoreIndex); + var valueAlias = alias.substring(underscoreIndex + 1); + + var areaEntry = dictionary[areaAlias]; + if (areaEntry) { + var valueEntry = areaEntry[valueAlias]; + if (valueEntry) { + return service.tokenReplace(valueEntry, tokens); + } } - return "[" + value + "]"; + + if (fallbackValue) return fallbackValue; + + return "[" + alias + "]"; } var service = { - // array to hold the localized resource string entries - dictionary: [], + // loads the language resource file from the server initLocalizedResources: function () { var deferred = $q.defer(); if (resourceFileLoadStatus === "loaded") { - deferred.resolve(service.dictionary); + deferred.resolve(innerDictionary); return deferred.promise; } @@ -77,7 +90,7 @@ angular.module('umbraco.services') $http({ method: "GET", url: url, cache: false }) .then(function (response) { resourceFileLoadStatus = "loaded"; - service.dictionary = response.data; + innerDictionary = response.data; eventsService.emit("localizationService.updated", response.data); @@ -159,11 +172,14 @@ angular.module('umbraco.services') * @param {Array} tokens if specified this array will be sent as parameter values * This replaces %0% and %1% etc in the dictionary key value with the passed in strings * + * @param {String} fallbackValue if specified this string will be returned if no matching + * entry was found in the dictionary + * * @returns {String} localized resource string */ - localize: function (value, tokens) { + localize: function (value, tokens, fallbackValue) { return service.initLocalizedResources().then(function (dic) { - return _lookup(value, tokens, dic); + return _lookup(value, tokens, dic, fallbackValue); }); }, @@ -244,8 +260,8 @@ angular.module('umbraco.services') //Build a concat string by looping over the array of resolved promises/translations var returnValue = ""; - for(var i = 0; i < localizedValues.length; i++){ - returnValue += localizedValues[i]; + for(var j = 0; j < localizedValues.length; j++){ + returnValue += localizedValues[j]; } return returnValue; @@ -292,11 +308,11 @@ angular.module('umbraco.services') return $q.all(promises).then(function(localizedValues){ //Replace {0} and {1} etc in message with the localized values - for(var i = 0; i < localizedValues.length; i++){ - var token = "%" + i + "%"; + for(var j = 0; j < localizedValues.length; j++){ + var token = "%" + j + "%"; var regex = new RegExp(token, "g"); - message = message.replace(regex, localizedValues[i]); + message = message.replace(regex, localizedValues[j]); } return message; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index d85a59d836..9350af1c47 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -3,7 +3,7 @@ * @name umbraco.services.mediaHelper * @description A helper object used for dealing with media items **/ -function mediaHelper(umbRequestHelper) { +function mediaHelper(umbRequestHelper, $log) { //container of fileresolvers var _mediaFileResolvers = {}; @@ -13,11 +13,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#getImagePropertyValue * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Returns the file path associated with the media property if there is one - * + * * @param {object} options Options object * @param {object} options.mediaModel The media object to retrieve the image path from * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image @@ -80,11 +80,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#getImagePropertyValue * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Returns the actual image path associated with the image property if there is one - * + * * @param {object} options Options object * @param {object} options.imageModel The media object to retrieve the image path from */ @@ -104,11 +104,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#getThumbnail * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * formats the display model used to display the content to the model used to save the content - * + * * @param {object} options Options object * @param {object} options.imageModel The media object to retrieve the image path from */ @@ -133,18 +133,20 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#resolveFileFromEntity * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Gets the media file url for a media entity returned with the entityResource - * + * * @param {object} mediaEntity A media Entity returned from the entityResource * @param {boolean} thumbnail Whether to return the thumbnail url or normal url */ resolveFileFromEntity: function (mediaEntity, thumbnail) { if (!angular.isObject(mediaEntity.metaData) || !mediaEntity.metaData.MediaPath) { - throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"; + //don't throw since this image legitimately might not contain a media path, but output a warning + $log.warn("Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"); + return null; } if (thumbnail) { @@ -164,11 +166,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#resolveFile * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Gets the media file url for a media object returned with the mediaResource - * + * * @param {object} mediaEntity A media Entity returned from the entityResource * @param {boolean} thumbnail Whether to return the thumbnail url or normal url */ @@ -240,11 +242,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#scaleToMaxSize * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios - * + * * @param {number} maxSize Maximum width & height * @param {number} width Current width * @param {number} height Current height @@ -283,11 +285,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#getThumbnailFromPath * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Returns the path to the thumbnail version of a given media library image path - * + * * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg */ getThumbnailFromPath: function (imagePath) { @@ -310,11 +312,11 @@ function mediaHelper(umbRequestHelper) { * @ngdoc function * @name umbraco.services.mediaHelper#detectIfImageByExtension * @methodOf umbraco.services.mediaHelper - * @function + * @function * * @description * Returns true/false, indicating if the given path has an allowed image extension - * + * * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg */ detectIfImageByExtension: function (imagePath) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index ba8334d307..a36e1a7633 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -13,7 +13,7 @@ * Section navigation and search, and maintain their state for the entire application lifetime * */ -function navigationService($routeParams, $location, $q, $timeout, $injector, eventsService, umbModelMapper, treeService, appState) { +function navigationService($routeParams, $location, $q, $injector, eventsService, umbModelMapper, treeService, appState) { //the promise that will be resolved when the navigation is ready var navReadyPromise = $q.defer(); @@ -31,7 +31,8 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve //A list of query strings defined that when changed will not cause a reload of the route var nonRoutingQueryStrings = ["mculture", "cculture", "lq"]; var retainedQueryStrings = ["mculture"]; - + //A list of trees that don't cause a route when creating new items (TODO: eventually all trees should do this!) + var nonRoutingTreesOnCreate = ["content", "contentblueprints"]; function setMode(mode) { switch (mode) { @@ -115,16 +116,17 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve } var service = { - + /** * @ngdoc method * @name umbraco.services.navigationService#isRouteChangingNavigation * @methodOf umbraco.services.navigationService * * @description - * Detects if the route param differences will cause a navigation change or if the route param differences are + * Detects if the route param differences will cause a navigation/route change or if the route param differences are * only tracking state changes. - * This is used for routing operations where reloadOnSearch is false and when detecting form dirty changes when navigating to a different page. + * This is used for routing operations where "reloadOnSearch: false" or "reloadOnUrl: false", when detecting form dirty changes when navigating to a different page, + * and when we are creating new entities and moving from a route with the ?create=true parameter to an ID based parameter once it's created. * @param {object} currUrlParams Either a string path or a dictionary of route parameters * @param {object} nextUrlParams Either a string path or a dictionary of route parameters */ @@ -138,6 +140,14 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve nextUrlParams = pathToRouteParts(nextUrlParams); } + //first check if this is a ?create=true url being redirected to it's true url + if (currUrlParams.create === "true" && currUrlParams.id && currUrlParams.section && currUrlParams.tree && currUrlParams.method === "edit" && + !nextUrlParams.create && nextUrlParams.id && nextUrlParams.section === currUrlParams.section && nextUrlParams.tree === currUrlParams.tree && nextUrlParams.method === currUrlParams.method && + nonRoutingTreesOnCreate.indexOf(nextUrlParams.tree.toLowerCase()) >= 0) { + //this means we're coming from a path like /content/content/edit/1234?create=true to the created path like /content/content/edit/9999 + return false; + } + var allowRoute = true; //The only time that we want to not route is if only any of the nonRoutingQueryStrings have changed/added. @@ -339,7 +349,7 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve reloadSection: function(sectionAlias) { return navReadyPromise.promise.then(function () { - mainTreeApi.clearCache({ section: sectionAlias }); + treeService.clearCache({ section: sectionAlias }); return mainTreeApi.load(sectionAlias); }); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index e853e07092..2165c1b7cb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -77,4 +77,5 @@ angular.module("umbraco.services").factory("overlayService", overlayService); + })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index 04c431767c..fef286ec7e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -2,7 +2,7 @@ * @ngdoc service * @name umbraco.services.searchService * - * + * * @description * Service for handling the main application search, can currently search content, media and members * @@ -15,10 +15,10 @@ * angular.forEach(results, function(result){ * //returns: * {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" } - * }) - * var result = - * }) - * + * }) + * var result = + * }) + * */ angular.module('umbraco.services') .factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) { @@ -67,7 +67,7 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { + return entityResource.search(args.term, "Document", args.searchFrom, args.canceler, args.dataTypeKey).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureContentResult(item); }); @@ -92,7 +92,7 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { + return entityResource.search(args.term, "Media", args.searchFrom, args.canceler, args.dataTypeKey).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureMediaResult(item); }); @@ -122,7 +122,7 @@ angular.module('umbraco.services') _.each(data, function (resultByType) { //we need to format the search result data to include things like the subtitle, urls, etc... - // this is done with registered angular services as part of the SearchableTreeAttribute, if that + // this is done with registered angular services as part of the SearchableTreeAttribute, if that // is not found, than we format with the default formatter var formatterMethod = searchResultFormatter.configureDefaultResult; //check if a custom formatter is specified... @@ -143,7 +143,7 @@ angular.module('umbraco.services') _.each(resultByType.results, function (item) { formatterMethod.apply(this, [item, resultByType.treeAlias, resultByType.appAlias]); }); - + }); return data; @@ -157,4 +157,4 @@ angular.module('umbraco.services') var currentSection = sectionAlias; } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index f72c447627..e61bd38bc0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -6,7 +6,7 @@ * @description * A service containing all logic for all of the Umbraco TinyMCE plugins */ -function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService, editorService, editorState) { +function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce @@ -895,38 +895,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }, - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#getAnchorNames - * @methodOf umbraco.services.tinyMceService - * - * @description - * From the given string, generates a string array where each item is the id attribute value from a named anchor - * 'some string
with a named anchor' returns ['anchor'] - * - * @param {string} input the string to parse - */ - getAnchorNames: function (input) { - var anchors = []; - if (!input) { - return anchors; - } - - var anchorPattern = //gi; - var matches = input.match(anchorPattern); - - - if (matches) { - anchors = matches.map(function (v) { - return v.substring(v.indexOf('"') + 1, v.lastIndexOf('\\')); - }); - } - - return anchors.filter(function(val, i, self) { - return self.indexOf(val) === i; - }); - }, - insertLinkInEditor: function (editor, target, anchorElm) { var href = target.url; @@ -1148,29 +1116,50 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //create link picker self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { - var linkPicker = { - currentTarget: currentTarget, - anchors: editorState.current ? self.getAnchorNames(JSON.stringify(editorState.current.properties)) : [], - submit: function (model) { - self.insertLinkInEditor(args.editor, model.target, anchorElement); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.linkPicker(linkPicker); + + + entityResource.getAnchors(args.model.value).then(function (anchorValues) { + var linkPicker = { + currentTarget: currentTarget, + dataTypeKey: args.model.dataTypeKey, + ignoreUserStartNodes: args.model.config.ignoreUserStartNodes, + anchors: anchorValues, + submit: function (model) { + self.insertLinkInEditor(args.editor, model.target, anchorElement); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.linkPicker(linkPicker); + }); + }); //Create the insert media plugin self.createMediaPicker(args.editor, function (currentTarget, userData) { + + var startNodeId, startNodeIsVirtual; + if (!args.model.config.startNodeId) { + if (args.model.config.ignoreUserStartNodes === true) { + startNodeId = -1; + startNodeIsVirtual = true; + } + else { + startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + } + var mediaPicker = { currentTarget: currentTarget, onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + dataTypeKey: args.model.dataTypeKey, submit: function (model) { self.insertMediaInEditor(args.editor, model.selection[0]); editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index d61d1c3ba1..3e60b09ad9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -41,7 +41,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return { /** Internal method to return the tree cache */ - _getTreeCache: function() { + _getTreeCache: function () { return treeCache; }, @@ -97,7 +97,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } //create a method outside of the loop to return the parent - otherwise jshint blows up - var funcParent = function() { + var funcParent = function () { return parentNode; }; @@ -168,7 +168,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @param {String} treeAlias The tree alias to check */ - getTreePackageFolder: function(treeAlias) { + getTreePackageFolder: function (treeAlias) { //we determine this based on the server variables if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && @@ -220,7 +220,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS var self = this; this.clearCache({ cacheKey: args.cacheKey, - filter: function(cc) { + filter: function (cc) { //get the new parent node from the tree cache var parent = self.getDescendantNode(cc.root, args.childrenOf); if (parent) { @@ -288,7 +288,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @param {object} args.node The tree node * @param {object} args.section The current section */ - loadNodeChildren: function(args) { + loadNodeChildren: function (args) { if (!args) { throw "No args object defined for loadNodeChildren"; } @@ -303,7 +303,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS args.node.loading = true; return this.getChildren(args) - .then(function(data) { + .then(function (data) { //set state to done and expand (only if there actually are children!) args.node.loading = false; @@ -320,10 +320,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return $q.when(data); - }, function(reason) { + }, function (reason) { //in case of error, emit event - eventsService.emit("treeService.treeNodeLoadError", {error: reason } ); + eventsService.emit("treeService.treeNodeLoadError", { error: reason }); //stop show the loading indicator args.node.loading = false; @@ -346,7 +346,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * Removes a given node from the tree * @param {object} treeNode the node to remove */ - removeNode: function(treeNode) { + removeNode: function (treeNode) { if (!angular.isFunction(treeNode.parent)) { return; } @@ -359,7 +359,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS parent.children.splice(parent.children.indexOf(treeNode), 1); parent.hasChildren = parent.children.length !== 0; - + //Notify that the node has been removed eventsService.emit("treeService.removeNode", { node: treeNode }); }, @@ -374,7 +374,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * Removes all child nodes from a given tree node * @param {object} treeNode the node to remove children from */ - removeChildNodes : function(treeNode) { + removeChildNodes: function (treeNode) { treeNode.expanded = false; treeNode.children = []; treeNode.hasChildren = false; @@ -413,7 +413,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * @param {int} id id of descendant node * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document */ - getDescendantNode: function(treeNode, id, treeAlias) { + getDescendantNode: function (treeNode, id, treeAlias) { //validate if it is a section container since we'll need a treeAlias if it is one if (treeNode.isContainer === true && !treeAlias) { @@ -432,7 +432,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS var root = getTreeRoot(tn.children[c]); //only return if we found the root in this child, otherwise continue. - if(root){ + if (root) { return root; } } @@ -531,7 +531,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node * @param {object} treeNode to retrive tree alias from */ - getTreeAlias : function(treeNode) { + getTreeAlias: function (treeNode) { var root = this.getTreeRoot(treeNode); if (root) { return root.metaData["treeAlias"]; @@ -570,7 +570,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS var self = this; return treeResource.loadApplication(args) - .then(function(data) { + .then(function (data) { //this will be called once the tree app data has loaded var result = { name: data.name, @@ -624,7 +624,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } return treeResource.loadMenu(args.treeNode) - .then(function(data) { + .then(function (data) { //need to convert the icons to new ones for (var i = 0; i < data.length; i++) { data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); @@ -677,7 +677,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * Re-loads the single node from the server * @param {object} node Tree node to reload */ - reloadNode: function(node) { + reloadNode: function (node) { if (!node) { throw "node cannot be null"; } @@ -691,10 +691,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //set the node to loading node.loading = true; - return this.getChildren({ node: node.parent(), section: node.section }).then(function(data) { + return this.getChildren({ node: node.parent(), section: node.section }).then(function (data) { //ok, now that we have the children, find the node we're reloading - var found = _.find(data, function(item) { + var found = _.find(data, function (item) { return item.id === node.id; }); if (found) { @@ -720,7 +720,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS else { return $q.reject(); } - }, function() { + }, function () { return $q.reject(); }); }, @@ -735,7 +735,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * This will return the current node's path by walking up the tree * @param {object} node Tree node to retrieve path for */ - getPath: function(node) { + getPath: function (node) { if (!node) { throw "node cannot be null"; } @@ -760,7 +760,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return reversePath.reverse(); }, - syncTree: function(args) { + syncTree: function (args) { if (!args) { throw "No args object defined for syncTree"; @@ -800,6 +800,8 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } } + var deferred = $q.defer(); + //now that we have the first id to lookup, we can start the process var self = this; @@ -831,6 +833,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS else { //couldn't find it in the return self.loadNodeChildren({ node: node, section: node.section }).then(function (children) { + + //send back some progress to allow the caller to deal with expanded nodes + deferred.notify({ type: "treeNodeExpanded", node: node, children: children }) + //ok, got the children, let's find it var found = self.getChildNode(node, args.path[currPathIndex]); if (found) { @@ -858,8 +864,16 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS }; //start - return doSync(); + var wrappedPromise = doSync(); + //then wrap it + wrappedPromise.then(function (args) { + deferred.resolve(args); + }, function (args) { + deferred.reject(args); + }); + + return deferred.promise; } }; diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index 83b3c920d7..654bbb1d03 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -9,8 +9,8 @@ * */ function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, assetsService, eventsService, - tmhDynamicLocale, localStorageService, editorService, overlayService, focusService) { + userService, historyService, updateChecker, navigationService, eventsService, + tmhDynamicLocale, localStorageService, editorService, overlayService) { //the null is important because we do an explicit bool check on this in the view $scope.authenticated = null; @@ -105,6 +105,13 @@ function MainController($scope, $location, appState, treeService, notificationsS //if the user has changed we need to redirect to the root so they don't try to continue editing the //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { + + var section = appState.getSectionState("currentSection"); + if (section) { + //if there's a section already assigned, reload it so the tree is cleared + navigationService.reloadSection(section); + } + $location.path("/").search(""); historyService.removeAll(); treeService.clearCache(); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index c426d0d955..e4c94f3c66 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -140,8 +140,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //// TODO: remove this it's not a thing //$scope.selectedId = navigationService.currentId; + var isInit = false; var evts = []; - + //Listen for global state changes evts.push(eventsService.on("appState.globalState.changed", function (e, args) { if (args.key === "showNavigation") { @@ -236,8 +237,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar })); //when the application is ready and the user is authorized, setup the data + //this will occur anytime a new user logs in! evts.push(eventsService.on("app.ready", function (evt, data) { - init(); + $scope.authenticated = true; + ensureInit(); })); // event for infinite editors @@ -305,9 +308,14 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar /** * Called when the app is ready and sets up the navigation (should only be called once) */ - function init() { + function ensureInit() { - $scope.authenticated = true; + //only run once ever! + if (isInit) { + return; + } + + isInit = true; var navInit = false; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index eaa2fe7b31..e169d78b36 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource', - function (userService, $q, $log, $rootScope, $route, $location, urlHelper, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) { +app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', + function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -91,13 +91,6 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH $rootScope.locationTitle = "Umbraco - " + $location.$$host; } - //reset the editorState on each successful route chage - editorState.reset(); - - //reset the file manager on each route change, the file collection is only relavent - // when working in an editor and submitting data to the server. - //This ensures that memory remains clear of any files and that the editors don't have to manually clear the files. - fileManager.clearFiles(); }); /** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including @@ -122,7 +115,7 @@ app.run(['userService', '$q', '$log', '$rootScope', '$route', '$location', 'urlH }); //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. - //This is the case when a route uses reloadOnSearch: false which is the case for many or our routes so that we are able to maintain + //This is the case when a route uses "reloadOnSearch: false" or "reloadOnUrl: false" which is the case for many or our routes so that we are able to maintain //global state query strings without force re-loading views. //We can then detect if it's a location change that should force a route or not programatically. $rootScope.$on('$routeUpdate', function (event, next) { diff --git a/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less b/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less new file mode 100644 index 0000000000..21ca5e6718 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/accessibility/sr-only.less @@ -0,0 +1,24 @@ + +// sr-only - based on the boot strap naming conventions used to remove an element from the view, whilst retaining accessibily for screen readers. More info available at https://getbootstrap.com/docs/4.0/utilities/screenreaders/ +// -------------------------------------------------- +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; + + &--focusable:active, + &--focusable:focus, + &--hoverable:hover { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 1e48500bb0..bd1cdd5b4f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -80,6 +80,9 @@ @import "forms/umb-validation-label.less"; +// Umbraco Accessibility +@import "accessibility/sr-only.less"; + // Umbraco Components @import "components/application/umb-app-header.less"; @import "components/application/umb-app-content.less"; @@ -162,6 +165,7 @@ @import "components/umb-textarea.less"; @import "components/umb-dropdown.less"; @import "components/umb-range-slider.less"; +@import "components/umb-number.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index f21c7f3106..91a6c29a17 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -67,6 +67,21 @@ border-color: rgba(0,0,0,0.09); } +// Button Reset - remove the default browser styles from the button element +// -------------------------------------------------- + +.btn-reset { + padding: 0; + margin: 0; + border: none; + background: none; + color: currentColor; + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + cursor: pointer; +} + // Button Sizes // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index d4b21e66f0..bd1b8ab07a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -16,27 +16,26 @@ margin-right: -10px; } -.umb-app-header__action a { +.umb-app-header__button { padding-left: 10px; padding-right: 10px; text-decoration: none; display: flex; align-items: center; height: @appHeaderHeight; -} - -.umb-app-header__action a { outline: none; + &:focus { .tabbing-active & { .umb-app-header__action-icon::after { content: ''; position: absolute; z-index:10000; - top: -7px; - left: -7px; + top: 50%; + left: 50%; width: 36px; height: 35px; + transform: translate(-50%, -50%); border-radius: 3px; box-shadow: 0 0 2px @pinkLight, inset 0 0 2px 1px @pinkLight; } @@ -51,7 +50,7 @@ font-size: 22px; } -.umb-app-header__action a:hover .umb-app-header__action-icon, -.umb-app-header__action a:focus .umb-app-header__action-icon { +.umb-app-header__button:hover .umb-app-header__action-icon, +.umb-app-header__button:focus .umb-app-header__action-icon { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less index 6f23677a1c..a621370d02 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less @@ -21,23 +21,23 @@ background-color: @inputBorder; position: relative; transition: background-color 120ms; - - .umb-toggle:hover &, + + .umb-toggle:hover &, .umb-toggle:focus & { border-color: @inputBorderFocus; } - + .umb-toggle.umb-toggle--checked & { border-color: @ui-btn; background-color: @ui-btn; - + &:hover { background-color: @ui-btn-hover; } } - - .umb-toggle.umb-toggle--checked:focus & { - border-color: black; + + .tabbing-active .umb-toggle:focus & { + box-shadow: 0 0 0 2px highlight; } } @@ -54,12 +54,12 @@ background-color: @white; border-radius: 8px; transition: transform 120ms ease-in-out, background-color 120ms; - + .umb-toggle.umb-toggle--checked & { transform: translateX(20px); background-color: white; } - + } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index 14544ded10..d6e792de73 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -27,6 +27,7 @@ } .umb-action-link { + position: relative; white-space: nowrap; font-size: 15px; color: @black; @@ -34,6 +35,7 @@ text-decoration: none; cursor: pointer; display: flex; + width: 100%; align-items: center; body.touch & { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index b372d41eb4..8f0b55f9ed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -8,12 +8,28 @@ user-select: none; } - &:hover ins { + &:hover .umb-tree-item__arrow { visibility: visible; cursor: pointer } } +.umb-tree-item__arrow { + position: relative; + margin-left: -16px; + width: 16px; + height: 16px; + visibility: hidden; + text-decoration: none; + font-size: 12px; + line-height: 12px; + transition: color 120ms; + + &:hover { + color: @ui-option-type-hover; + } +} + .umb-tree-item > .umb-tree-item__inner { &:hover .umb-tree-item__label { @@ -95,7 +111,7 @@ a, .umb-tree-icon, - ins { + .umb-tree-item__arrow { color: @ui-active-type !important; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 202c9400da..5c54232200 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -15,20 +15,6 @@ text-decoration: none; } - ins { - margin: -4px 0 0 -16px; - width: 16px; - height: 16px; - visibility: hidden; - text-decoration: none; - font-size: 12px; - transition: color 120ms; - - &:hover { - color: @ui-option-type-hover; - } - } - i.noSpr { display: inline-block; margin-top: 1px; @@ -62,7 +48,7 @@ } body.touch .umb-tree { - ins { + .umb-tree-item__arrow { font-size: 14px; visibility: visible; padding: 7px; @@ -104,7 +90,13 @@ body.touch .umb-tree { } > .umb-options { - visibility: visible; + position: relative; + width: auto; + height: auto; + margin: 0 10px 0 auto; + padding: 9px 5px; + overflow: visible; + clip: auto; } .umb-tree-icon { @@ -178,7 +170,7 @@ body.touch .umb-tree { } .umb-options { - visibility: hidden; + position: relative; display: flex; flex: 0 0 auto; justify-content: flex-end; @@ -192,6 +184,20 @@ body.touch .umb-tree { background: @btnBackgroundHighlight; } + // NOTE - We're having to repeat ourselves here due to an .sr-only class appearing in umbraco/lib/font-awesome/css/font-awesome.min.css + &.sr-only--hoverable:hover, + &.sr-only--focusable:focus { + position: relative; + display: flex; + flex: 0 0 auto; + justify-content: flex-end; + padding: 9px 5px; + text-align: center; + margin: 0 10px 0 auto; + cursor: pointer; + border-radius: 3px; + } + i { height: 5px !important; width: 5px !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index d8a5ebf9d8..deb573920f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -6,7 +6,7 @@ flex-wrap: wrap; align-items: center; position: relative; - padding: 0; + padding: 0 !important; margin: 0; min-height: 22px; line-height: 22px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less index 6af0641d8c..30a32b8123 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less @@ -1,9 +1,10 @@ .umb-multiple-textbox{ &__confirm{ position: relative; + display: inline-block; &-action{ - margin: 0; + margin: -2px 0 0 0; padding: 2px; background: transparent; border: 0 none; @@ -11,6 +12,10 @@ } } +.umb-multiple-textbox .icon-wrapper { + width: 50px; +} + .umb-multiple-textbox .textbox-wrapper { align-items: center; margin-bottom: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index f1a6300481..59c90972d2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -56,13 +56,20 @@ -webkit-user-select: none; -o-user-select: none; user-select: none; + + &:hover { + .umb-nested-content__heading .umb-nested-content__item-name { + padding-right: 60px; + } + } + } .umb-nested-content__heading { line-height: 20px; position: relative; margin-top:1px; - padding: 15px 20px; + padding: 15px 5px; color:@ui-option-type; border-radius: 3px 3px 0 0; @@ -71,16 +78,21 @@ } i { - display: inline; - margin-right: 10px; + position: absolute; + margin-top: -1px; } .umb-nested-content__item-name { - display: inline; + display: block; max-height: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding-left: 5px; + + &.--has-icon { + padding-left: 30px; + } } } @@ -89,9 +101,10 @@ opacity: 0; transition: opacity 120ms ease-in-out; position: absolute; - right: 8px; - top: 4px; + right: 0; + top: 3px; padding: 5px; + background-color: @white; } .umb-nested-content__item--active > .umb-nested-content__header-bar { @@ -100,6 +113,9 @@ &:hover { color:@ui-option-type; } + .umb-nested-content__item-name { + padding-right: 60px; + } } .umb-nested-content__icons { background-color: @ui-active; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less new file mode 100644 index 0000000000..c0e3dee8e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less @@ -0,0 +1,3 @@ +.umb-number { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less index f39096b565..47a2af9231 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less @@ -34,9 +34,13 @@ margin-top: 2px; } -.umb-user-group-preview__permission { +.umb-user-group-preview__permissions { font-size: 13px; color: @gray-3; + + .umb-user-group-preview__permission:not(:last-child):after { + content: ', '; + } } .umb-user-group-preview__actions { @@ -61,4 +65,4 @@ .umb-user-group-preview__action--red:hover { color: @red; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 52a573d8c4..84de751b12 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -113,6 +113,10 @@ bottom: 0px; padding: 20px; margin: 0; + + .btn.umb-outline { + position: relative + } } /*we will always make sure to wrap iframe dialogs in proper padding*/ diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less index 69bbeef0af..b9c8b909e8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less @@ -57,3 +57,12 @@ .mb5 { margin-bottom: @spacing-extra-large; } .mb6 { margin-bottom: @spacing-extra-extra-large; } .mb7 { margin-bottom: @spacing-extra-extra-extra-large; } + +.ml0 { margin-left: @spacing-none; } +.ml1 { margin-left: @spacing-extra-small; } +.ml2 { margin-left: @spacing-small; } +.ml3 { margin-left: @spacing-medium; } +.ml4 { margin-left: @spacing-large; } +.ml5 { margin-left: @spacing-extra-large; } +.ml6 { margin-left: @spacing-extra-extra-large; } +.ml7 { margin-left: @spacing-extra-extra-extra-large; } diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index 7d6584d2f1..1316642e93 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -113,7 +113,10 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.exitPreview = function () { var culture = $location.search().culture || getParameterByName("culture"); - var relativeUrl = "/" + $scope.pageId +'?culture='+ culture; + var relativeUrl = "/" + $scope.pageId; + if (culture) { + relativeUrl += '?culture=' + culture; + } window.top.location.href = "../preview/end?redir=" + encodeURIComponent(relativeUrl); }; diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index e2a2cfe938..556a4d6aef 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -228,6 +228,7 @@ app.config(function ($routeProvider) { }, reloadOnSearch: false, + reloadOnUrl: false, resolve: canRoute(true) }) .otherwise({ redirectTo: '/login' }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 6568cfb567..5a0ab51fd0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", - function ($scope, eventsService, entityResource, contentResource, mediaResource, mediaHelper, udiParser, userService, localizationService, tinyMceService, editorService, contentEditingHelper) { + function ($scope, eventsService, entityResource, mediaResource, mediaHelper, udiParser, userService, localizationService, editorService) { var vm = this; var dialogOptions = $scope.model; @@ -20,13 +20,14 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.model.title = value; }); } - + $scope.customTreeParams = dialogOptions.dataTypeKey ? "dataTypeKey=" + dialogOptions.dataTypeKey : ""; $scope.dialogTreeApi = {}; $scope.model.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, + dataTypeKey: dialogOptions.dataTypeKey, results: [], selectedSearchResults: [] }; @@ -86,10 +87,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", oneTimeTreeSync.sync(); }); - // get the content properties to build the anchor name list - contentResource.getById(id).then(function (resp) { - handleContentTarget(resp); + entityResource.getUrlAndAnchors(id).then(function (resp) { + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); + } } else if ($scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs @@ -136,10 +138,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; - } else { - contentResource.getById(args.node.id).then(function (resp) { - handleContentTarget(resp); - + } + else { + entityResource.getUrlAndAnchors(args.node.id).then(function (resp) { + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); } @@ -148,10 +151,6 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", } } - function handleContentTarget(content) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(contentEditingHelper.getAllProps(content.variants[0]))); - $scope.model.target.url = content.urls.filter(item => item.culture === $scope.currentNode.metaData.culture)[0].text; - } function nodeExpandedHandler(args) { // open mini list view for list views @@ -162,9 +161,21 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { + + var startNodeId, startNodeIsVirtual; + if (dialogOptions.ignoreUserStartNodes === true) { + startNodeId = -1; + startNodeIsVirtual = true; + } + else { + startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + var mediaPicker = { - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + dataTypeKey: dialogOptions.dataTypeKey, submit: function (model) { var media = model.selection[0]; @@ -172,7 +183,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.model.target.udi = media.udi; $scope.model.target.isMedia = true; $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + $scope.model.target.url = media.image; editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 0bb91d8da6..704b61e333 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -25,7 +25,7 @@ ng-model="model.target.url" ng-disabled="model.target.id || model.target.udi" /> - + - + - + - +
Link to page
- +
- - +
- - - +
- + enablecheckboxes="true" + customtreeparams="{{customTreeParams}}">
- - - + - +
Link to media
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 3a0deb812e..bcda921269 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function($scope, mediaResource, entityResource, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService) { + function ($scope, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService) { if (!$scope.model.title) { localizationService.localizeMany(["defaultdialogs_selectMedia", "general_includeFromsubFolders"]) @@ -25,6 +25,8 @@ angular.module("umbraco") $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; + var userStartNodes = []; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { @@ -45,7 +47,7 @@ angular.module("umbraco") $scope.acceptedMediatypes = []; mediaTypeHelper.getAllowedImagetypes($scope.startNodeId) - .then(function(types) { + .then(function (types) { $scope.acceptedMediatypes = types; }); @@ -54,7 +56,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: '', + dataTypeKey: $scope.model.dataTypeKey }; //preload selected item @@ -64,15 +67,19 @@ angular.module("umbraco") } function onInit() { - if ($scope.startNodeId !== -1) { - entityResource.getById($scope.startNodeId, "media") - .then(function (ent) { - $scope.startNodeId = ent.id; - run(); - }); - } else { - run(); - } + userService.getCurrentUser().then(function (userData) { + userStartNodes = userData.startMediaIds; + + if ($scope.startNodeId !== -1) { + entityResource.getById($scope.startNodeId, "media") + .then(function (ent) { + $scope.startNodeId = ent.id; + run(); + }); + } else { + run(); + } + }); } function run() { @@ -89,7 +96,7 @@ angular.module("umbraco") //media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; - mediaResource.getById(id) + entityResource.getById(id, "Media") .then(function (node) { $scope.target = node; if (ensureWithinStartNode(node)) { @@ -99,28 +106,28 @@ angular.module("umbraco") $scope.openDetailsDialog(); } }, - gotoStartNode); + gotoStartNode); } } - $scope.upload = function(v) { + $scope.upload = function (v) { angular.element(".umb-file-dropzone .file-select").trigger("click"); }; - $scope.dragLeave = function(el, event) { + $scope.dragLeave = function (el, event) { $scope.activeDrag = false; }; - $scope.dragEnter = function(el, event) { + $scope.dragEnter = function (el, event) { $scope.activeDrag = true; }; - $scope.submitFolder = function() { + $scope.submitFolder = function () { if ($scope.model.newFolderName) { $scope.model.creatingFolder = true; mediaResource .addFolder($scope.model.newFolderName, $scope.currentFolder.id) - .then(function(data) { + .then(function (data) { //we've added a new folder so lets clear the tree cache for that specific item treeService.clearCache({ cacheKey: "__media", //this is the main media tree cache key @@ -136,14 +143,14 @@ angular.module("umbraco") } }; - $scope.enterSubmitFolder = function(event) { + $scope.enterSubmitFolder = function (event) { if (event.keyCode === 13) { $scope.submitFolder(); event.stopPropagation(); } }; - $scope.gotoFolder = function(folder) { + $scope.gotoFolder = function (folder) { if (!$scope.multiPicker) { deselectAllImages($scope.model.selection); } @@ -153,30 +160,30 @@ angular.module("umbraco") } if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media") - .then(function(anc) { + entityResource.getAncestors(folder.id, "media", null, { dataTypeKey: $scope.model.dataTypeKey }) + .then(function (anc) { $scope.path = _.filter(anc, - function(f) { + function (f) { return f.path.indexOf($scope.startNodeId) !== -1; }); }); mediaTypeHelper.getAllowedImagetypes(folder.id) - .then(function(types) { + .then(function (types) { $scope.acceptedMediatypes = types; }); } else { $scope.path = []; } - $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual; + $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; $scope.currentFolder = folder; localStorageService.set("umbLastOpenedMediaNodeId", folder.id); return getChildren(folder.id); }; - $scope.clickHandler = function(image, event, index) { + $scope.clickHandler = function (image, event, index) { if (image.isFolder) { if ($scope.disableFolderSelect) { $scope.gotoFolder(image); @@ -204,7 +211,7 @@ angular.module("umbraco") } }; - $scope.clickItemName = function(item) { + $scope.clickItemName = function (item) { if (item.isFolder) { $scope.gotoFolder(item); } @@ -236,8 +243,8 @@ angular.module("umbraco") images.length = 0; } - $scope.onUploadComplete = function(files) { - $scope.gotoFolder($scope.currentFolder).then(function() { + $scope.onUploadComplete = function (files) { + $scope.gotoFolder($scope.currentFolder).then(function () { if (files.length === 1 && $scope.model.selection.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; @@ -247,7 +254,7 @@ angular.module("umbraco") }); }; - $scope.onFilesQueue = function() { + $scope.onFilesQueue = function () { $scope.activeDrag = false; }; @@ -265,16 +272,27 @@ angular.module("umbraco") } } + function hasFolderAccess(node) { + var nodePath = node.path ? node.path.split(',') : [node.id]; + + for (var i = 0; i < nodePath.length; i++) { + if (userStartNodes.indexOf(parseInt(nodePath[i])) !== -1) + return true; + } + + return false; + } + function gotoStartNode(err) { $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } - $scope.openDetailsDialog = function() { + $scope.openDetailsDialog = function () { $scope.mediaPickerDetailsOverlay = {}; $scope.mediaPickerDetailsOverlay.show = true; - $scope.mediaPickerDetailsOverlay.submit = function(model) { + $scope.mediaPickerDetailsOverlay.submit = function (model) { $scope.model.selection.push($scope.target); $scope.model.submit($scope.model); @@ -282,41 +300,42 @@ angular.module("umbraco") $scope.mediaPickerDetailsOverlay = null; }; - $scope.mediaPickerDetailsOverlay.close = function(oldModel) { + $scope.mediaPickerDetailsOverlay.close = function (oldModel) { $scope.mediaPickerDetailsOverlay.show = false; $scope.mediaPickerDetailsOverlay = null; }; }; - var debounceSearchMedia = _.debounce(function() { - $scope.$apply(function() { - if ($scope.searchOptions.filter) { - searchMedia(); - } else { - // reset pagination - $scope.searchOptions = { - pageNumber: 1, - pageSize: 100, - totalItems: 0, - totalPages: 0, - filter: '' - }; - getChildren($scope.currentFolder.id); - } - }); - }, 500); + var debounceSearchMedia = _.debounce(function () { + $scope.$apply(function () { + if ($scope.searchOptions.filter) { + searchMedia(); + } else { + // reset pagination + $scope.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '', + dataTypeKey: $scope.model.dataTypeKey + }; + getChildren($scope.currentFolder.id); + } + }); + }, 500); - $scope.changeSearch = function() { + $scope.changeSearch = function () { $scope.loading = true; debounceSearchMedia(); }; - $scope.toggle = function() { + $scope.toggle = function () { // Make sure to activate the changeSearch function everytime the toggle is clicked $scope.changeSearch(); } - $scope.changePagination = function(pageNumber) { + $scope.changePagination = function (pageNumber) { $scope.loading = true; $scope.searchOptions.pageNumber = pageNumber; searchMedia(); @@ -325,9 +344,9 @@ angular.module("umbraco") function searchMedia() { $scope.loading = true; entityResource.getPagedDescendants($scope.currentFolder.id, "Media", $scope.searchOptions) - .then(function(data) { + .then(function (data) { // update image data to work with image grid - angular.forEach(data.items, function(mediaItem) { + angular.forEach(data.items, function (mediaItem) { setMediaMetaData(mediaItem); }); // update images @@ -350,29 +369,47 @@ angular.module("umbraco") mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); // set properties to match a media object - if (mediaItem.metaData && - mediaItem.metaData.umbracoWidth && - mediaItem.metaData.umbracoHeight) { - - mediaItem.properties = [ - { - alias: "umbracoWidth", - value: mediaItem.metaData.umbracoWidth.Value - }, - { - alias: "umbracoHeight", - value: mediaItem.metaData.umbracoHeight.Value - } - ]; + if (mediaItem.metaData) { + mediaItem.properties = []; + if (mediaItem.metaData.umbracoWidth && mediaItem.metaData.umbracoHeight) { + mediaItem.properties.push( + { + alias: "umbracoWidth", + editor: mediaItem.metaData.umbracoWidth.PropertyEditorAlias, + value: mediaItem.metaData.umbracoWidth.Value + }, + { + alias: "umbracoHeight", + editor: mediaItem.metaData.umbracoHeight.PropertyEditorAlias, + value: mediaItem.metaData.umbracoHeight.Value + } + ); + } + if (mediaItem.metaData.umbracoFile) { + // this is required for resolving files through the mediahelper + mediaItem.properties.push( + { + alias: "umbracoFile", + editor: mediaItem.metaData.umbracoFile.PropertyEditorAlias, + value: mediaItem.metaData.umbracoFile.Value + } + ); + } } } function getChildren(id) { $scope.loading = true; - return mediaResource.getChildren(id) - .then(function(data) { + return entityResource.getChildren(id, "Media", $scope.searchOptions) + .then(function (data) { + for (var i = 0; i < data.length; i++) { + if (data[i].metaData.MediaPath !== null) { + data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); + data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); + } + } $scope.searchOptions.filter = ""; - $scope.images = data.items ? data.items : []; + $scope.images = data ? data : []; // set already selected images to selected preSelectImages(); $scope.loading = false; @@ -402,15 +439,15 @@ angular.module("umbraco") } } - $scope.editMediaItem = function(item) { + $scope.editMediaItem = function (item) { var mediaEditor = { id: item.id, - submit: function(model) { + submit: function (model) { editorService.close() // update the media picker item in the picker so it matched the saved media item // the media picker is using media entities so we get the // entity so we easily can format it for use in the media grid - if(model && model.mediaNode) { + if (model && model.mediaNode) { entityResource.getById(model.mediaNode.id, "media") .then(function (mediaEntity) { angular.extend(item, mediaEntity); @@ -419,7 +456,7 @@ angular.module("umbraco") }); } }, - close: function(model) { + close: function (model) { setUpdatedMediaNodes(item); editorService.close(); } @@ -429,19 +466,19 @@ angular.module("umbraco") function setUpdatedMediaNodes(item) { // add udi to list of updated media items so we easily can update them in other editors - if($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { + if ($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { $scope.model.updatedMediaNodes.push(item.udi); } } - $scope.submit = function() { - if($scope.model.submit) { + $scope.submit = function () { + if ($scope.model.submit) { $scope.model.submit($scope.model); } }; - $scope.close = function() { - if($scope.model.close) { + $scope.close = function () { + if ($scope.model.close) { $scope.model.close($scope.model); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 93d7936326..fab6ba4069 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -91,6 +91,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 915abf62b0..31430c81cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -28,10 +28,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.treeAlias = $scope.model.treeAlias; vm.multiPicker = $scope.model.multiPicker; vm.hideHeader = (typeof $scope.model.hideHeader) === "boolean" ? $scope.model.hideHeader : true; + vm.dataTypeKey = $scope.model.dataTypeKey; vm.searchInfo = { searchFromId: $scope.model.startNodeId, searchFromName: null, showSearch: false, + dataTypeKey: vm.dataTypeKey, results: [], selectedSearchResults: [] } @@ -59,6 +61,8 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.submit = submit; vm.close = close; + var currentNode = $scope.model.currentNode; + function initDialogTree() { vm.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); // TODO: Also deal with unexpanding!! @@ -92,6 +96,14 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + if (vm.treeAlias === "documentTypes") { + vm.entityType = "DocumentType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectContentType").then(function(value){ + $scope.model.title = value; + }); + } + } else if (vm.treeAlias === "member" || vm.section === "member") { vm.entityType = "Member"; if (!$scope.model.title) { @@ -160,6 +172,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", } } } + + vm.filter = { + filterAdvanced: $scope.model.filterAdvanced, + filterExclude: $scope.model.filterExclude, + filter: $scope.model.filter + }; } /** @@ -174,6 +192,10 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (vm.selectedLanguage && vm.selectedLanguage.id) { queryParams["culture"] = vm.selectedLanguage.culture; } + if (vm.dataTypeKey) { + queryParams["dataTypeKey"] = vm.dataTypeKey; + } + var queryString = $.param(queryParams); //create the query string from the params object if (!queryString) { @@ -256,6 +278,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.hasItems = args.tree.root.children.length > 0; tree = args.tree; + + var nodeHasPath = currentNode && currentNode.path; + var startNodeNotDefined = !vm.startNodeId; + if (startNodeNotDefined && nodeHasPath) { + vm.dialogTreeApi.syncTree({ path: currentNode.path, activate: true }); + } } //wires up selection diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index c592b4ec3b..78c75f6f8d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -27,7 +27,7 @@ {{language.name}}
- +
- + - + {{ vm.emptyStateMessage }} - +
-
- + - - + entity-type-filter="vm.filter"> @@ -92,7 +93,7 @@ shortcut="esc" action="vm.close()"> - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index ac919d3e41..93c3a9b50d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,3 +1,4 @@ +
@@ -10,25 +11,28 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 92da12c423..a6d42d923b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -6,10 +6,14 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html index 56d9eae16c..35bf725e0a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -1,5 +1,5 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 95c628376b..8163b2807b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -1,4 +1,4 @@ -
+
@@ -15,7 +15,7 @@
- -
@@ -74,7 +75,8 @@ type="button" disabled="model.disableSubmitButton" action="submitForm(model)" - state="model.submitButtonState"> + state="model.submitButtonState" + auto-focus="true">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html index 2005666292..46fb11c310 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html @@ -1,5 +1,5 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html index e8d9839d45..d777b3db78 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html @@ -1,16 +1,20 @@
  • -   + ng-click="load(node)">  + + Expand child items for {{node.name}} + {{node.name}} - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html index c2559ef31a..0141ff264f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html @@ -8,9 +8,9 @@ {{tree.name}} - +
  • - +
    {{ name }}
    {{ description }}
    -
    +
    Permissions: - {{ permission.name }}, + {{ permission.name }}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html index 817a4f2a94..fae431a11a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html @@ -7,7 +7,7 @@ -
  • {{ name }}
    -
    +
    Sections: - {{ section.name }}, + {{ section.name }} All sections
    -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index f101450705..d04c707b25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -7,86 +7,103 @@ * The controller for the content creation dialog */ function contentCreateController($scope, - $routeParams, - contentTypeResource, - iconHelper, - $location, - navigationService, - blueprintConfig) { - - var mainCulture = $routeParams.mculture ? $routeParams.mculture : null; + $routeParams, + contentTypeResource, + iconHelper, + $location, + navigationService, + blueprintConfig, + authResource, + contentResource) { - function initialize() { - $scope.allowedTypes = null; - contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); + var mainCulture = $routeParams.mculture ? $routeParams.mculture : null; - $scope.selectContentType = true; - $scope.selectBlueprint = false; - $scope.allowBlank = blueprintConfig.allowBlank; - } + function initialize() { + $scope.allowedTypes = null; + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + }); - function close() { - navigationService.hideMenu(); - } + if ($scope.currentNode.id > -1) { + authResource.getCurrentUser().then(function(currentUser) { + if (currentUser.allowedSections.indexOf("settings") > -1) { + $scope.hasSettingsAccess = true; + contentResource.getById($scope.currentNode.id).then(function(data) { + $scope.contentTypeId = data.contentTypeId; + }); + } + }); + } - function createBlank(docType) { - $location - .path("/content/content/edit/" + $scope.currentNode.id) - .search("doctype", docType.alias) - .search("create", "true") - /* when we create a new node we want to make sure it uses the same - language as what is selected in the tree */ - .search("cculture", mainCulture); - close(); - } - - function createOrSelectBlueprintIfAny(docType) { - // map the blueprints into a collection that's sortable in the view - var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { - return { - id: pair[0], - name: pair[1] - }; - }); - $scope.docType = docType; - if (blueprints.length) { - if (blueprintConfig.skipSelect) { - createFromBlueprint(blueprints[0].id); - } else { - $scope.selectContentType = false; - $scope.selectBlueprint = true; - $scope.selectableBlueprints = blueprints; - } - } else { - createBlank(docType); + $scope.selectContentType = true; + $scope.selectBlueprint = false; + $scope.allowBlank = blueprintConfig.allowBlank; } - } - function createFromBlueprint(blueprintId) { - $location - .path("/content/content/edit/" + $scope.currentNode.id) - .search("doctype", $scope.docType.alias) - .search("create", "true") - .search("blueprintId", blueprintId); - close(); - } + function close() { + navigationService.hideMenu(); + } - $scope.closeDialog = function(showMenu) { - navigationService.hideDialog(showMenu); - }; + function createBlank(docType) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype", docType.alias) + .search("create", "true") + /* when we create a new node we want to make sure it uses the same + language as what is selected in the tree */ + .search("cculture", mainCulture); + close(); + } - $scope.createBlank = createBlank; - $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; - $scope.createFromBlueprint = createFromBlueprint; + function createOrSelectBlueprintIfAny(docType) { + // map the blueprints into a collection that's sortable in the view + var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); + $scope.docType = docType; + if (blueprints.length) { + if (blueprintConfig.skipSelect) { + createFromBlueprint(blueprints[0].id); + } else { + $scope.selectContentType = false; + $scope.selectBlueprint = true; + $scope.selectableBlueprints = blueprints; + } + } else { + createBlank(docType); + } + } - // the current node changes behind the scenes when the context menu is clicked without closing - // the default menu first, so we must watch the current node and re-initialize accordingly - var unbindModelWatcher = $scope.$watch("currentNode", initialize); - $scope.$on('$destroy', function () { - unbindModelWatcher(); - }); + function createFromBlueprint(blueprintId) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype", $scope.docType.alias) + .search("create", "true") + .search("blueprintId", blueprintId); + close(); + } + + $scope.close = function() { + close(); + } + + $scope.closeDialog = function (showMenu) { + navigationService.hideDialog(showMenu); + }; + + $scope.createBlank = createBlank; + $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; + $scope.createFromBlueprint = createFromBlueprint; + + // the current node changes behind the scenes when the context menu is clicked without closing + // the default menu first, so we must watch the current node and re-initialize accordingly + var unbindModelWatcher = $scope.$watch("currentNode", initialize); + $scope.$on('$destroy', function () { + unbindModelWatcher(); + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js index c0002220e3..9a2c845d49 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.createblueprint.controller.js @@ -36,7 +36,6 @@ function(err) { contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index cc55fbbf4d..5cbf25ba7d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the content editor */ -function ContentEditController($scope, $rootScope, $routeParams, contentResource) { +function ContentEditController($scope, $routeParams, contentResource) { var infiniteMode = $scope.model && $scope.model.infiniteMode; @@ -24,13 +24,17 @@ function ContentEditController($scope, $rootScope, $routeParams, contentResource $scope.page = $routeParams.page; $scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create; //load the default culture selected in the main tree if any - $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.culture = $routeParams.cculture ? $routeParams.cculture : ($routeParams.mculture === "true"); //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change - // and then we can pass in the updated culture to the editor + //and then we can pass in the updated culture to the editor. + //This will also execute when we are redirecting from creating an item to a newly created item since that + //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.isNew = next.params.create === "true"; + $scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 8d80f308ab..fcd0294849 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -102,6 +102,7 @@ }; }); navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + $scope.dialog.confirmDiscardChanges = true; }, function (error) { vm.error = error; vm.buttonState = "error"; @@ -117,6 +118,7 @@ function toggle(group) { group.selected = !group.selected; + $scope.dialog.confirmDiscardChanges = true; } function pickGroup() { @@ -137,6 +139,7 @@ }); editorService.close(); navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; }, close: function() { editorService.close(); @@ -147,6 +150,7 @@ function removeGroup(group) { vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); + $scope.dialog.confirmDiscardChanges = true; } function pickMember() { @@ -186,6 +190,7 @@ $q.all(promises).then(function() { vm.loading = false; }); + $scope.dialog.confirmDiscardChanges = true; } }, close: function () { @@ -219,6 +224,7 @@ } editorService.close(); navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; }, close: function () { editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js index a8f87ce2c9..0e40fc2e6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ContentRightsController($scope, $timeout, contentResource, localizationService, angularHelper, navigationService) { + function ContentRightsController($scope, $timeout, contentResource, localizationService, angularHelper, navigationService, overlayService) { var vm = this; var currentForm; @@ -11,7 +11,6 @@ vm.removedUserGroups = []; vm.viewState = "manageGroups"; vm.labels = {}; - vm.showNotification = false; vm.setViewSate = setViewSate; vm.editPermissions = editPermissions; @@ -20,7 +19,6 @@ vm.removePermissions = removePermissions; vm.cancelManagePermissions = cancelManagePermissions; vm.closeDialog = closeDialog; - vm.stay = stay; vm.discardChanges = discardChanges; function onInit() { @@ -95,6 +93,7 @@ function setPermissions(group) { assignGroupPermissions(group); setViewSate("manageGroups"); + $scope.dialog.confirmDiscardChanges = true; } /** @@ -164,14 +163,31 @@ }); } - function stay() { - vm.showNotification = false; - } - function closeDialog() { // check if form has been changed. If it has show discard changes notification if (currentForm && currentForm.$dirty) { - vm.showNotification = true; + localizationService.localizeMany(["prompt_unsavedChanges", "prompt_unsavedChangesWarning", "prompt_discardChanges", "prompt_stay"]).then( + function(values) { + var overlay = { + "view": "default", + "title": values[0], + "content": values[1], + "disableBackdropClick": true, + "disableEscKey": true, + "submitButtonLabel": values[2], + "closeButtonLabel": values[3], + submit: function () { + overlayService.close(); + navigationService.hideDialog(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(overlay); + } + ); } else { navigationService.hideDialog(); } @@ -187,4 +203,4 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RightsController", ContentRightsController); -})(); \ No newline at end of file +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index 94299f6a54..059b5eaf25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -1,19 +1,25 @@
    - + \ No newline at end of file +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index 292db6f105..f430aad342 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -62,24 +62,6 @@
    -
    -
    -

    -

    - - - - -
    -
    - diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js index 59351ffd38..982af76d69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js @@ -47,9 +47,13 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change - // and then we can pass in the updated culture to the editor + //and then we can pass in the updated culture to the editor. + //This will also execute when we are redirecting from creating an item to a newly created item since that + //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.isNew = $routeParams.id === "-1"; + $scope.contentId = $routeParams.id; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js new file mode 100644 index 0000000000..295263a47c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js @@ -0,0 +1,38 @@ +function ProfilerController($scope, $cookies, $http, umbRequestHelper) { + var vm = this; + + vm.loading = true; + vm.toggle = toggle; + + function toggle() { + if (vm.alwaysOn === true) { + $cookies.remove("UMB-DEBUG", { + path: "/" + }); + vm.alwaysOn = false; + } + else { + $cookies.put("UMB-DEBUG", "true", { + path: "/", + expires: "Tue, 01 Jan 2100 00:00:01 GMT" + }); + vm.alwaysOn = true; + } + } + + function init() { + vm.alwaysOn = $cookies.get("UMB-DEBUG") === "true"; + + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("webProfilingBaseUrl", "GetStatus")), + "Failed to retrieve status for web profiling" + ).then(function(status) { + vm.loading = false; + vm.profilerEnabled = status.Enabled; + }); + } + + init(); +} + +angular.module("umbraco").controller("Umbraco.Dashboard.ProfilerController", ProfilerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html new file mode 100644 index 0000000000..2a7419c0ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html @@ -0,0 +1,41 @@ +
    + + +

    Performance profiling

    +
    +

    + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

    +

    + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

    +

    + If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

    +

     

    +
    +
    +
    Activate the profiler by default
    +
    +
    + +
    +
    +

    Friendly reminder

    +

    + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

    +
    +
    +

    + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

    +

    + Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

    +
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index ead73beab8..6282ecb0b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -174,7 +174,6 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic //NOTE: in the case of data type values we are setting the orig/new props // to be the same thing since that only really matters for content/media. contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index 596f848abe..c05638f344 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -91,7 +91,6 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes function (err) { contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 62cf9d3e88..5ceb5f01f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -56,7 +56,7 @@ function onInit() { // get init values from model when in infinite mode - if(infiniteMode) { + if (infiniteMode) { documentTypeId = $scope.model.id; create = $scope.model.create; noTemplate = $scope.model.notemplate; @@ -89,8 +89,7 @@ "name": vm.labels.design, "alias": "design", "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true + "view": "views/documenttypes/views/design/design.html" }, { "name": vm.labels.listview, @@ -291,6 +290,28 @@ }); vm.page.navigation = buttons; + initializeActiveNavigationPanel(); + } + + function initializeActiveNavigationPanel() { + // Initialise first loaded panel based on page route paramater + // i.e. ?view=design|listview|permissions + var initialViewSetFromRouteParams = false; + var view = $routeParams.view; + if (view) { + var viewPath = "views/documenttypes/views/" + view + "/" + view + ".html"; + for (var i = 0; i < vm.page.navigation.length; i++) { + if (vm.page.navigation[i].view === viewPath) { + vm.page.navigation[i].active = true; + initialViewSetFromRouteParams = true; + break; + } + } + } + + if (initialViewSetFromRouteParams === false) { + vm.page.navigation[0].active = true; + } } /* ---------- SAVE ---------- */ @@ -317,10 +338,6 @@ saveMethod: contentTypeResource.save, scope: $scope, content: vm.contentType, - //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc - // type when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { vm.contentType.id = savedContentType.id; diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js index e91d8ae366..79d837e516 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js @@ -35,7 +35,6 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta vm.page.saveButtonState = "success"; }, function (error) { contentEditingHelper.handleSaveError({ - redirectOnFailure: false, err: error }); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/create.html b/src/Umbraco.Web.UI.Client/src/views/media/create.html index 13c12f3c9a..d93d4f0e30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/create.html @@ -4,9 +4,15 @@
    Create under {{currentNode.name}}
    -

    - -

    +
    +

    +
    +

    + + + +
    +
  • /// - /// Some objects such as macros are not based on CMSNode + /// + /// This controller allows resolving basic entity data for various entities without placing the hard restrictions on users that may not have access + /// to the sections these entities entities exist in. This is to allow pickers, etc... of data to work for all users. In some cases such as accessing + /// Members, more explicit security checks are done. + /// + /// Some objects such as macros are not based on CMSNode /// [EntityControllerConfiguration] [PluginController("UmbracoApi")] @@ -98,19 +104,20 @@ namespace Umbraco.Web.Editors /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// + /// If set used to look up whether user and group start node permissions will be ignored. /// [HttpGet] - public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) + public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, Guid? dataTypeKey = null) { - // TODO: Should we restrict search results based on what app the user has access to? - // - Theoretically you shouldn't be able to see member data if you don't have access to members right? + // NOTE: Theoretically you shouldn't be able to see member data if you don't have access to members right? ... but there is a member picker, so can't really do that if (string.IsNullOrEmpty(query)) return Enumerable.Empty(); //TODO: This uses the internal UmbracoTreeSearcher, this instead should delgate to the ISearchableTree implementation for the type - return ExamineSearch(query, type, searchFrom); + var ignoreUserStartNodes = dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes); } /// @@ -204,6 +211,9 @@ namespace Umbraco.Web.Editors /// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member /// The URL or path to the item + /// + /// We are not restricting this with security because there is no sensitive data + /// public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) { var returnUrl = string.Empty; @@ -277,6 +287,23 @@ namespace Umbraco.Web.Editors publishedContentExists: i => Umbraco.Content(i) != null); } + [HttpGet] + public UrlAndAnchors GetUrlAndAnchors([FromUri]int id, [FromUri]string culture = "*") + { + var url = UmbracoContext.UrlProvider.GetUrl(id); + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id, culture); + return new UrlAndAnchors(url, anchorValues); + } + + [HttpGet] + [HttpPost] + public IEnumerable GetAnchors(AnchorsModel model) + { + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEContent(model.RteContent); + return anchorValues; + } + + #region GetById /// @@ -397,9 +424,43 @@ namespace Umbraco.Web.Editors } #endregion - public IEnumerable GetChildren(int id, UmbracoEntityTypes type) + public IEnumerable GetChildren(int id, UmbracoEntityTypes type, Guid? dataTypeKey = null) { - return GetResultForChildren(id, type); + var objectType = ConvertToObjectType(type); + if (objectType.HasValue) + { + //TODO: Need to check for Object types that support hierarchy here, some might not. + + var startNodes = GetStartNodes(type); + + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); + + // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes + if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) + { + var nodes = Services.EntityService.GetAll(objectType.Value, startNodes).ToArray(); + if (nodes.Length == 0) + return Enumerable.Empty(); + var pr = new List(nodes.Select(Mapper.Map)); + return pr; + } + + // else proceed as usual + + return Services.EntityService.GetChildren(id, objectType.Value) + .WhereNotNull() + .Select(Mapper.Map); + } + //now we need to convert the unknown ones + switch (type) + { + case UmbracoEntityTypes.Domain: + case UmbracoEntityTypes.Language: + case UmbracoEntityTypes.User: + case UmbracoEntityTypes.Macro: + default: + throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); + } } /// @@ -420,7 +481,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (int.TryParse(id, out var intId)) { @@ -445,7 +507,7 @@ namespace Umbraco.Web.Editors //the EntityService can search paged members from the root intId = -1; - return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); + return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter, dataTypeKey); } //the EntityService cannot search members of a certain type, this is currently not supported and would require @@ -480,7 +542,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -490,12 +553,37 @@ namespace Umbraco.Web.Editors var objectType = ConvertToObjectType(type); if (objectType.HasValue) { - var entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out var totalRecords, + IEnumerable entities; + long totalRecords; + + var startNodes = GetStartNodes(type); + + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); + + // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes + if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) + { + if (pageNumber > 0) + return new PagedResult(0, 0, 0); + var nodes = Services.EntityService.GetAll(objectType.Value, startNodes).ToArray(); + if (nodes.Length == 0) + return new PagedResult(0, 0, 0); + if (pageSize < nodes.Length) pageSize = nodes.Length; // bah + var pr = new PagedResult(nodes.Length, pageNumber, pageSize) + { + Items = nodes.Select(Mapper.Map) + }; + return pr; + } + + // else proceed as usual + entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, filter.IsNullOrWhiteSpace() ? null : SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); + if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -510,6 +598,7 @@ namespace Umbraco.Web.Editors { context.SetCulture(culture); }); + //TODO: Why is this here and not in the mapping? target.AdditionalData["hasChildren"] = source.HasChildren; return target; }) @@ -532,6 +621,19 @@ namespace Umbraco.Web.Editors } } + private int[] GetStartNodes(UmbracoEntityTypes type) + { + switch (type) + { + case UmbracoEntityTypes.Document: + return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + case UmbracoEntityTypes.Media: + return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + default: + return Array.Empty(); + } + } + public PagedResult GetPagedDescendants( int id, UmbracoEntityTypes type, @@ -539,7 +641,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + Guid? dataTypeKey = null) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -556,18 +659,10 @@ namespace Umbraco.Web.Editors { // root is special: we reduce it to start nodes - int[] aids = null; - switch (type) - { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } + int[] aids = GetStartNodes(type); - entities = aids == null || aids.Contains(Constants.System.Root) + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); + entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? Services.EntityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection), includeTrashed: false) @@ -609,6 +704,8 @@ namespace Umbraco.Web.Editors } } + private bool IsDataTypeIgnoringUserStartNodes(Guid? dataTypeKey) => dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings) { return GetResultForAncestors(id, type, queryStrings); @@ -620,10 +717,11 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { - return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, searchFrom); + return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, searchFrom, ignoreUserStartNodes); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) @@ -658,35 +756,39 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); - int[] aids = null; - switch (entityType) + var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue("dataTypeId")); + if (ignoreUserStartNodes == false) { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } - - if (aids != null) - { - var lids = new List(); - var ok = false; - foreach (var i in ids) + int[] aids = null; + switch (entityType) { - if (ok) - { - lids.Add(i); - continue; - } - if (aids.Contains(i)) - { - lids.Add(i); - ok = true; - } + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + break; + } + + if (aids != null) + { + var lids = new List(); + var ok = false; + foreach (var i in ids) + { + if (ok) + { + lids.Add(i); + continue; + } + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } + } + ids = lids.ToArray(); } - ids = lids.ToArray(); } var culture = queryStrings?.GetValue("culture"); diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs index bec978788c..d469d9d9eb 100644 --- a/src/Umbraco.Web/Editors/IEditorValidator.cs +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Editors /// /// Provides a general object validator. /// - internal interface IEditorValidator : IDiscoverable + public interface IEditorValidator : IDiscoverable { /// /// Gets the object type validated by this validator. diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 2ee77ca418..650dcea6e9 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Editors { var allLanguages = Services.LocalizationService.GetAllLanguages(); - return Mapper.MapEnumerable(allLanguages); + return Mapper.Map, IEnumerable>(allLanguages); } [HttpGet] diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web/Editors/MacroRenderingController.cs index a2bbfe1dfd..efad07ce89 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web/Editors/MacroRenderingController.cs @@ -104,11 +104,11 @@ namespace Umbraco.Web.Editors if (m == null) throw new HttpResponseException(HttpStatusCode.NotFound); - var publishedContent = UmbracoContext.ContentCache.GetById(true, pageId); + var publishedContent = UmbracoContext.Content.GetById(true, pageId); //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist - if (pageId == -1 || publishedContent == null || !m.UseInEditor) + if (pageId == -1 || publishedContent == null || m.DontRender) { var response = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured @@ -123,13 +123,17 @@ namespace Umbraco.Web.Editors // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want // to set the current culture to the culture related to the content item. This is hacky but it works. - var culture = publishedContent.GetCulture(); - _variationContextAccessor.VariationContext = new VariationContext(); //must have an active variation context! + // fixme + // in a 1:1 situation we do not handle the language being edited + // so the macro renders in the wrong language + + var culture = publishedContent.GetCultureFromDomains(); + if (culture != null) - { - Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture.Culture); - _variationContextAccessor.VariationContext = new VariationContext(Thread.CurrentThread.CurrentCulture.Name); - } + Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture); + + // must have an active variation context! + _variationContextAccessor.VariationContext = new VariationContext(culture); var result = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 391f3b8cf9..c55f07d559 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -135,7 +135,10 @@ namespace Umbraco.Web.Editors } public MediaTypeDisplay GetEmpty(int parentId) { - var ct = new MediaType(parentId) {Icon = "icon-picture"}; + var ct = new MediaType(parentId) + { + Icon = Constants.Icons.MediaImage + }; var dto = Mapper.Map(ct); return dto; diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index df7faeccf4..f406988ae5 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -102,7 +102,7 @@ namespace Umbraco.Web.Editors public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); - ct.Icon = "icon-user"; + ct.Icon = Constants.Icons.Member; var dto = Mapper.Map(ct); return dto; diff --git a/src/Umbraco.Web/Editors/UpdateCheckController.cs b/src/Umbraco.Web/Editors/UpdateCheckController.cs index cd11382d13..132526576b 100644 --- a/src/Umbraco.Web/Editors/UpdateCheckController.cs +++ b/src/Umbraco.Web/Editors/UpdateCheckController.cs @@ -24,7 +24,8 @@ namespace Umbraco.Web.Editors { try { - var check = new org.umbraco.update.CheckForUpgrade(); + var check = new org.umbraco.update.CheckForUpgrade { Timeout = 2000 }; + var result = check.CheckUpgrade(UmbracoVersion.Current.Major, UmbracoVersion.Current.Minor, UmbracoVersion.Current.Build, @@ -37,6 +38,11 @@ namespace Umbraco.Web.Editors //this occurs if the server is down or cannot be reached return null; } + catch (System.Web.Services.Protocols.SoapException) + { + //this occurs if the server has a timeout + return null; + } } return null; } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 99adf71742..e9b731348d 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -223,8 +223,17 @@ namespace Umbraco.Web _method = method; _controllerName = controllerName; _encryptedString = UrlHelperRenderExtensions.CreateEncryptedRouteString(controllerName, controllerAction, area, additionalRouteVals); + + //For UmbracoForm's we want to add our routing string to the httpcontext items in the case where anti-forgery tokens are used. + //In which case our custom UmbracoAntiForgeryAdditionalDataProvider will kick in and validate the values in the request against + //the values that will be appended to the token. This essentially means that when anti-forgery tokens are used with UmbracoForm's forms, + //that each token is unique to the controller/action/area instead of the default ASP.Net implementation which is that the token is unique + //per user. + _viewContext.HttpContext.Items["ufprt"] = _encryptedString; + } + private readonly ViewContext _viewContext; private readonly FormMethod _method; private bool _disposed; @@ -236,7 +245,6 @@ namespace Umbraco.Web if (this._disposed) return; this._disposed = true; - //Detect if the call is targeting UmbRegisterController/UmbProfileController/UmbLoginStatusController/UmbLoginController and if it is we automatically output a AntiForgeryToken() // We have a controllerName and area so we can match if (_controllerName == "UmbRegister" @@ -244,7 +252,7 @@ namespace Umbraco.Web || _controllerName == "UmbLoginStatus" || _controllerName == "UmbLogin") { - _viewContext.Writer.Write(AntiForgery.GetHtml().ToString()); + _viewContext.Writer.Write(AntiForgery.GetHtml().ToString()); } //write out the hidden surface form routes diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index d7f457287a..656f1e05a2 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -130,7 +130,7 @@ namespace Umbraco.Web if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) return string.Empty; - var mediaItemUrl = mediaItem.MediaUrl(propertyAlias); + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); //get the default obj from the value converter var cropperValue = mediaItem.Value(propertyAlias); diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index 14c1bb065f..512edb2296 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -68,6 +68,7 @@ namespace Umbraco.Web.Logging if (request.Result.Url.IsClientSideRequest()) return false; if (bool.TryParse(request.Result.QueryString["umbDebug"], out var umbDebug)) return umbDebug; if (bool.TryParse(request.Result.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Result.Cookies["UMB-DEBUG"]?.Value, out var cUmbDebug)) return cUmbDebug; return false; } diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs index 9736aa283b..61b2f0bda3 100755 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -439,7 +439,7 @@ namespace Umbraco.Web.Macros // this was, and still is, an ugly piece of nonsense var value = string.Empty; - var cache = _umbracoContextAccessor.UmbracoContext.ContentCache; + var cache = _umbracoContextAccessor.UmbracoContext.Content; var splitpath = (string[])pageElements["splitpath"]; for (var i = splitpath.Length - 1; i > 0; i--) // at 0 we have root (-1) diff --git a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs index 67669fd607..41f0e2fb65 100644 --- a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs +++ b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs @@ -140,14 +140,14 @@ namespace Umbraco.Web.Macros private readonly object _sourceValue; private readonly IPublishedContent _content; - public PagePublishedProperty(PublishedPropertyType propertyType, IPublishedContent content) + public PagePublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { _sourceValue = null; _content = content; } - public PagePublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, Umbraco.Core.Models.Property property) + public PagePublishedProperty(IPublishedPropertyType propertyType, IPublishedContent content, Umbraco.Core.Models.Property property) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { _sourceValue = property.GetValue(); @@ -218,7 +218,7 @@ namespace Umbraco.Web.Macros Parent = new PagePublishedContent(_inner.ParentId); } - public PublishedContentType ContentType { get; } + public IPublishedContentType ContentType { get; } public int Id { get; } @@ -230,19 +230,6 @@ namespace Umbraco.Web.Macros public string Name => _inner.Name; - public PublishedCultureInfo GetCulture(string culture = null) - { - // handle context culture - if (culture == null) - culture = _variationContextAccessor.VariationContext.Culture; - - // no invariant culture infos - if (culture == "") return null; - - // get - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; - } - public IReadOnlyDictionary Cultures { get @@ -279,8 +266,6 @@ namespace Umbraco.Web.Macros public string Url => throw new NotImplementedException(); - public string GetUrl(string culture = null) => throw new NotSupportedException(); - public PublishedItemType ItemType => PublishedItemType.Content; public bool IsDraft(string culture = null) @@ -297,6 +282,8 @@ namespace Umbraco.Web.Macros public IEnumerable Children => throw new NotImplementedException(); + public IEnumerable ChildrenForAllCultures => throw new NotImplementedException(); + public IEnumerable Properties => _properties; public IPublishedProperty GetProperty(string alias) diff --git a/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs new file mode 100644 index 0000000000..1069de749c --- /dev/null +++ b/src/Umbraco.Web/Media/EmbedProviders/Giphy.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Media.EmbedProviders +{ + /// + /// Embed Provider for Giphy.com the popular online GIFs and animated sticker provider. + /// + public class Giphy : EmbedProviderBase + { + public override string ApiEndpoint => "https://giphy.com/services/oembed?url="; + + public override string[] UrlSchemeRegex => new string[] + { + @"giphy\.com/*", + @"gph\.is/*" + }; + + public override Dictionary RequestParams => new Dictionary(); + + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + var oembed = base.GetJsonResponse(requestUrl); + + return oembed.GetHtml(); + } + } +} diff --git a/src/Umbraco.Web/Models/AnchorsModel.cs b/src/Umbraco.Web/Models/AnchorsModel.cs new file mode 100644 index 0000000000..9edcf3466b --- /dev/null +++ b/src/Umbraco.Web/Models/AnchorsModel.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Web.Models +{ + public class AnchorsModel + { + public string RteContent { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs index dc72b0a81f..d4156d6db5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs @@ -38,6 +38,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "updater")] public UserProfile Updater { get; set; } + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 80358bfc7a..b6a90a93c3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -108,6 +108,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "treeNodeUrl")] public string TreeNodeUrl { get; set; } + [DataMember(Name = "contentTypeId")] + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs index d9d730956a..c5c22484ad 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core.PropertyEditors; @@ -21,6 +22,10 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public int Id { get; set; } + [DataMember(Name = "dataTypeKey", IsRequired = false)] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } + [DataMember(Name = "value")] public object Value { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs b/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs index 3a8496ac3f..1f526a50f3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MessagesExtensions.cs @@ -1,10 +1,15 @@  +using System.Linq; +using Umbraco.Core; + namespace Umbraco.Web.Models.ContentEditing { public static class MessagesExtensions { public static void AddNotification(this INotificationModel model, string header, string msg, NotificationStyle type) { + if (model.Exists(header, msg, type)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -15,6 +20,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddSuccessNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Success)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -25,6 +32,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddErrorNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Error)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -35,6 +44,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddWarningNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Warning)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -45,6 +56,8 @@ namespace Umbraco.Web.Models.ContentEditing public static void AddInfoNotification(this INotificationModel model, string header, string msg) { + if (model.Exists(header, msg, NotificationStyle.Info)) return; + model.Notifications.Add(new Notification() { Header = header, @@ -52,5 +65,7 @@ namespace Umbraco.Web.Models.ContentEditing NotificationType = NotificationStyle.Info }); } + + private static bool Exists(this INotificationModel model, string header, string message, NotificationStyle notificationType) => model.Notifications.Any(x => x.Header.InvariantEquals(header) && x.Message.InvariantEquals(message) && x.NotificationType == notificationType); } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs index cde9d0dabc..d180a68a2c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; @@ -43,6 +44,10 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public int DataTypeId { get; set; } + [DataMember(Name = "dataTypeKey")] + [ReadOnly(true)] + public Guid DataTypeKey { get; set; } + //SD: Is this really needed ? [DataMember(Name = "groupId")] public int GroupId { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs new file mode 100644 index 0000000000..86642a8d65 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "urlAndAnchors", Namespace = "")] + public class UrlAndAnchors + { + public UrlAndAnchors(string url, IEnumerable anchorValues) + { + Url = url; + AnchorValues = anchorValues; + } + + [DataMember(Name = "url")] + public string Url { get; } + + [DataMember(Name = "anchorValues")] + public IEnumerable AnchorValues { get; } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 2b30b0ac5a..dc0df4ca96 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -73,6 +73,7 @@ namespace Umbraco.Web.Models.Mapping target.AllowedActions = GetActions(source); target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(source.ContentType.Name); target.DocumentType = _commonMapper.GetContentType(source, context); @@ -117,6 +118,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Alias private void Map(IContent source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Edited = source.Edited; diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index e7c53f5728..36c1b360b2 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -16,15 +16,17 @@ namespace Umbraco.Web.Models.Mapping internal class ContentPropertyBasicMapper where TDestination : ContentPropertyBasic, new() { + private readonly IEntityService _entityService; private readonly ILogger _logger; private readonly PropertyEditorCollection _propertyEditors; protected IDataTypeService DataTypeService { get; } - public ContentPropertyBasicMapper(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors) + public ContentPropertyBasicMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) { _logger = logger; _propertyEditors = propertyEditors; DataTypeService = dataTypeService; + _entityService = entityService; } /// @@ -48,6 +50,7 @@ namespace Umbraco.Web.Models.Mapping dest.Alias = property.Alias; dest.PropertyEditor = editor; dest.Editor = editor.Alias; + dest.DataTypeKey = property.PropertyType.DataTypeKey; // if there's a set of property aliases specified, we will check if the current property's value should be mapped. // if it isn't one of the ones specified in 'includeProperties', we will just return the result without mapping the Value. diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs index 8a45548e9c..f68c5d8b44 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -14,8 +14,8 @@ namespace Umbraco.Web.Models.Mapping { private readonly ILocalizedTextService _textService; - public ContentPropertyDisplayMapper(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, logger, propertyEditors) + public ContentPropertyDisplayMapper(IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { _textService = textService; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs index f192cd32ce..72107c6201 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs @@ -12,8 +12,8 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyDtoMapper : ContentPropertyBasicMapper { - public ContentPropertyDtoMapper(IDataTypeService dataTypeService, ILogger logger, PropertyEditorCollection propertyEditors) - : base(dataTypeService, logger, propertyEditors) + public ContentPropertyDtoMapper(IDataTypeService dataTypeService, IEntityService entityService, ILogger logger, PropertyEditorCollection propertyEditors) + : base(dataTypeService, entityService, logger, propertyEditors) { } public override void Map(Property property, ContentPropertyDto dest, MapperContext context) diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs index 226560c516..e6290cc19e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapDefinition.cs @@ -17,11 +17,11 @@ namespace Umbraco.Web.Models.Mapping private readonly ContentPropertyDtoMapper _contentPropertyDtoConverter; private readonly ContentPropertyDisplayMapper _contentPropertyDisplayMapper; - public ContentPropertyMapDefinition(IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) + public ContentPropertyMapDefinition(IDataTypeService dataTypeService, IEntityService entityService, ILocalizedTextService textService, ILogger logger, PropertyEditorCollection propertyEditors) { - _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, logger, propertyEditors); - _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, logger, propertyEditors); - _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(dataTypeService, textService, logger, propertyEditors); + _contentPropertyBasicConverter = new ContentPropertyBasicMapper(dataTypeService, entityService, logger, propertyEditors); + _contentPropertyDtoConverter = new ContentPropertyDtoMapper(dataTypeService, entityService, logger, propertyEditors); + _contentPropertyDisplayMapper = new ContentPropertyDisplayMapper(dataTypeService, entityService, textService, logger, propertyEditors); } public void DefineMaps(UmbracoMapper mapper) diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index a438f04781..fc029eabe4 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -219,6 +219,7 @@ namespace Umbraco.Web.Models.Mapping { target.Name = source.Label; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Mandatory = source.Validation.Mandatory; target.ValidationRegExp = source.Validation.Pattern; target.Variations = source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; @@ -334,6 +335,7 @@ namespace Umbraco.Web.Models.Mapping target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; target.GroupId = source.GroupId; target.Id = source.Id; @@ -349,6 +351,7 @@ namespace Umbraco.Web.Models.Mapping target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; target.DataTypeId = source.DataTypeId; + target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; target.GroupId = source.GroupId; target.Id = source.Id; diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 560d398a2c..c279ae2c70 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -59,7 +59,7 @@ namespace Umbraco.Web.Models.Mapping variants.Remove(defaultLang); //Sort the remaining languages a-z - variants = variants.OrderBy(x => x.Name).ToList(); + variants = variants.OrderBy(x => x.Language.Name).ToList(); //Insert the default language as the first item variants.Insert(0, defaultLang); diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs index 4c4a03939d..6b5797e05b 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeMapDefinition.cs @@ -146,19 +146,26 @@ namespace Umbraco.Web.Models.Mapping var fields = context.MapEnumerable(configurationEditor.Fields); var configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.Configuration); - MapConfigurationFields(fields, configurationDictionary); + MapConfigurationFields(dataType, fields, configurationDictionary); return fields; } - - private void MapConfigurationFields(List fields, IDictionary configuration) + + private void MapConfigurationFields(IDataType dataType, List fields, IDictionary configuration) { if (fields == null) throw new ArgumentNullException(nameof(fields)); if (configuration == null) throw new ArgumentNullException(nameof(configuration)); // now we need to wire up the pre-values values with the actual fields defined - foreach (var field in fields) + foreach (var field in fields.ToList()) { + //filter out the not-supported pre-values for built-in data types + if (dataType != null && dataType.IsBuildInDataType() && field.Key.InvariantEquals(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)) + { + fields.Remove(field); + continue; + } + if (configuration.TryGetValue(field.Key, out var value)) { field.Value = value; @@ -194,7 +201,7 @@ namespace Umbraco.Web.Models.Mapping var defaultConfiguration = configurationEditor.DefaultConfiguration; if (defaultConfiguration != null) - MapConfigurationFields(fields, defaultConfiguration); + MapConfigurationFields(null, fields, defaultConfiguration); return fields; } diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 2598523bd5..38ec557fdb 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -43,12 +43,12 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; - if (source.NodeObjectType == Constants.ObjectTypes.Media && source is IContentEntitySlim contentSlim) + if (source is IContentEntitySlim contentSlim) source.AdditionalData["ContentTypeAlias"] = contentSlim.ContentTypeAlias; - if (source.NodeObjectType == Constants.ObjectTypes.Media && source is IMediaEntitySlim mediaSlim) + if (source is IMediaEntitySlim mediaSlim) source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; // NOTE: we're mapping the objects in AdditionalData by object reference here. @@ -89,7 +89,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(IUser source, EntityBasic target, MapperContext context) { target.Alias = source.Username; - target.Icon = "icon-user"; + target.Icon = Constants.Icons.User; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; @@ -101,7 +101,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(ITemplate source, EntityBasic target, MapperContext context) { target.Alias = source.Alias; - target.Icon = "icon-layout"; + target.Icon = Constants.Icons.Template; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; @@ -144,15 +144,15 @@ namespace Umbraco.Web.Models.Mapping if (target.Icon.IsNullOrWhiteSpace()) { if (source.NodeObjectType == Constants.ObjectTypes.Member) - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; else if (source.NodeObjectType == Constants.ObjectTypes.DataType) - target.Icon = "icon-autofill"; + target.Icon = Constants.Icons.DataType; else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) - target.Icon = "icon-item-arrangement"; + target.Icon = Constants.Icons.ContentType; else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) - target.Icon = "icon-thumbnails"; + target.Icon = Constants.Icons.MediaType; else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) - target.Icon = "icon-newspaper-alt"; + target.Icon = Constants.Icons.Template; } } @@ -167,7 +167,7 @@ namespace Umbraco.Web.Models.Mapping //get the icon if there is one target.Icon = source.Values.ContainsKey(UmbracoExamineIndex.IconFieldName) ? source.Values[UmbracoExamineIndex.IconFieldName] - : "icon-document"; + : Constants.Icons.DefaultIcon; target.Name = source.Values.ContainsKey("nodeName") ? source.Values["nodeName"] : "[no name]"; diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs index 089f5d5d71..e5bca22287 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapDefinition.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Models.Mapping private static void Map(IMacro source, EntityBasic target, MapperContext context) { target.Alias = source.Alias; - target.Icon = "icon-settings-alt"; + target.Icon = Constants.Icons.Macro; target.Id = source.Id; target.Key = source.Key; target.Name = source.Name; diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs index 3da61bc9c0..05c006ec41 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs @@ -50,6 +50,7 @@ namespace Umbraco.Web.Models.Mapping { target.ContentApps = _commonMapper.GetContentApps(source); target.ContentType = _commonMapper.GetContentType(source, context); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; @@ -75,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Edited -Updater -Alias private void Map(IMedia source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Icon = source.ContentType.Icon; diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs index b230dcfe15..96ca16e18b 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs @@ -76,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture private void Map(IMember source, MemberDisplay target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; @@ -101,6 +102,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -Edited -Updater -Alias -VariesByCulture private void Map(IMember source, MemberBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Email = source.Email; @@ -121,12 +123,12 @@ namespace Umbraco.Web.Models.Mapping //TODO: SD: I can't remember why this mapping is here? // Umbraco.Code.MapAll -Udi -Properties -ParentId -Path -SortOrder -Edited -Updater - // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeAlias -VariesByCulture + // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeId -ContentTypeAlias -VariesByCulture private void Map(MembershipUser source, MemberBasic target, MapperContext context) { target.CreateDate = source.CreationDate; target.Email = source.Email; - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; target.Id = int.MaxValue; target.Key = source.ProviderUserKey.TryConvertTo().Result; target.Name = source.UserName; diff --git a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 6ee37ea443..8744b068a7 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -96,7 +96,7 @@ namespace Umbraco.Web.Models.Mapping linkText = source.ContentType.Name, url = memberTypeLink, target = "_self", - icon = "icon-item-arrangement" + icon = Constants.Icons.ContentType } }; docTypeProperty.View = "urllist"; diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs index 8c5f347799..a184ac92cf 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs @@ -231,6 +231,7 @@ namespace Umbraco.Web.Models.Mapping GroupId = groupId, Inherited = inherited, DataTypeId = p.DataTypeId, + DataTypeKey = p.DataTypeKey, SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, diff --git a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs index 73123a0407..e773fcfee5 100644 --- a/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapDefinition.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentId = source.ContentId; target.CreateDateUtc = source.CreateDateUtc; target.Culture = source.Culture; - target.DestinationUrl = source.ContentId > 0 ? UmbracoContext?.UrlProvider?.GetUrl(source.ContentId, source.Culture) : "#"; + target.DestinationUrl = source.ContentId > 0 ? UmbracoContext?.UrlProvider?.GetUrl(source.ContentId, culture: source.Culture) : "#"; target.OriginalUrl = UmbracoContext?.UrlProvider?.GetUrlFromRoute(source.ContentId, source.Url, source.Culture); target.RedirectId = source.Key; } diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 1b245cdce2..3860d5d525 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -179,7 +179,7 @@ namespace Umbraco.Web.Models.Mapping target.DefaultPermissions = MapUserGroupDefaultPermissions(source); if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-users"; + target.Icon = Constants.Icons.UserGroup; } // Umbraco.Code.MapAll -Trashed -Alias -AssignedPermissions @@ -194,7 +194,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-user"; + target.Icon = Constants.Icons.Member; } // Umbraco.Code.MapAll -ContentStartNode -MediaStartNode -Sections -Notifications -Udi @@ -350,7 +350,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot")); if (target.Icon.IsNullOrWhiteSpace()) - target.Icon = "icon-users"; + target.Icon = Constants.Icons.UserGroup; } private IDictionary> MapUserGroupDefaultPermissions(IUserGroup source) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index cdcfd8a0cd..148bab11c0 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors.ValueConverters; namespace Umbraco.Web.Models { @@ -12,19 +11,12 @@ namespace Umbraco.Web.Models /// /// This base class does which (a) consistently resolves and caches the Url, (b) provides an implementation /// for this[alias], and (c) provides basic content set management. - [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] + [DebuggerDisplay("Content Id: {Id}")] public abstract class PublishedContentBase : IPublishedContent { - protected PublishedContentBase(IUmbracoContextAccessor umbracoContextAccessor) - { - UmbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - } - - protected IUmbracoContextAccessor UmbracoContextAccessor { get; } - #region ContentType - public abstract PublishedContentType ContentType { get; } + public abstract IPublishedContentType ContentType { get; } #endregion @@ -41,10 +33,10 @@ namespace Umbraco.Web.Models public abstract int Id { get; } /// - public abstract string Name { get; } + public virtual string Name => this.Name(); /// - public abstract string UrlSegment { get; } + public virtual string UrlSegment => this.UrlSegment(); /// public abstract int SortOrder { get; } @@ -77,34 +69,7 @@ namespace Umbraco.Web.Models public abstract DateTime UpdateDate { get; } /// - public virtual string Url => GetUrl(); - - /// - /// - /// The url of documents are computed by the document url providers. The url of medias are computed by the media url providers - /// - public virtual string GetUrl(string culture = null) // TODO: consider .GetCulture("fr-FR").Url - { - var umbracoContext = UmbracoContextAccessor.UmbracoContext; - - if (umbracoContext == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); - if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); - - switch (ItemType) - { - case PublishedItemType.Content: - return umbracoContext.UrlProvider.GetUrl(this, culture); - case PublishedItemType.Media: - return umbracoContext.UrlProvider.GetMediaUrl(this, Constants.Conventions.Media.File, culture); - default: - throw new NotSupportedException(); - } - } - - /// - public abstract PublishedCultureInfo GetCulture(string culture = null); + public virtual string Url => this.Url(); /// public abstract IReadOnlyDictionary Cultures { get; } @@ -126,7 +91,10 @@ namespace Umbraco.Web.Models public abstract IPublishedContent Parent { get; } /// - public abstract IEnumerable Children { get; } + public virtual IEnumerable Children => this.Children(); + + /// + public abstract IEnumerable ChildrenForAllCultures { get; } #endregion diff --git a/src/Umbraco.Web/Models/PublishedProperty.cs b/src/Umbraco.Web/Models/PublishedProperty.cs deleted file mode 100644 index d32612c54c..0000000000 --- a/src/Umbraco.Web/Models/PublishedProperty.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Models -{ - public static class PublishedProperty - { - /// - /// Maps a collection of Property to a collection of IPublishedProperty for a specified collection of PublishedPropertyType. - /// - /// The published property types. - /// The properties. - /// A mapping function. - /// A collection of IPublishedProperty corresponding to the collection of PublishedPropertyType - /// and taking values from the collection of Property. - /// Ensures that all conversions took place correctly. - internal static IEnumerable MapProperties( - IEnumerable propertyTypes, IEnumerable properties, - Func map) - { - var propertyEditors = Current.PropertyEditors; - var dataTypeService = Current.Services.DataTypeService; - - // TODO: not dealing with variants - // but the entire thing should die anyways - - return propertyTypes.Select(x => - { - var p = properties.SingleOrDefault(xx => xx.Alias == x.Alias); - var v = p == null || p.GetValue() == null ? null : p.GetValue(); - if (v != null) - { - var e = propertyEditors[x.EditorAlias]; - - // We are converting to string, even for database values which are integer or - // DateTime, which is not optimum. Doing differently would require that we have a way to tell - // whether the conversion to XML string changes something or not... which we don't, and we - // don't want to implement it as PropertyValueEditor.ConvertDbToXml/String should die anyway. - - // Don't think about improving the situation here: this is a corner case and the real - // thing to do is to get rig of PropertyValueEditor.ConvertDbToXml/String. - - // Use ConvertDbToString to keep it simple, although everywhere we use ConvertDbToXml and - // nothing ensures that the two methods are consistent. - - if (e != null) - v = e.GetValueEditor().ConvertDbToString(p.PropertyType, v, dataTypeService); - } - - return map(x, v); - }); - } - } -} diff --git a/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs b/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs new file mode 100644 index 0000000000..b9161dbea0 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ModelBindingExceptionFilter.cs @@ -0,0 +1,54 @@ +using System; +using System.Net; +using System.Text.RegularExpressions; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// An exception filter checking if we get a or with the same model. in which case it returns a redirect to the same page after 1 sec. + /// + internal class ModelBindingExceptionFilter : FilterAttribute, IExceptionFilter + { + private static readonly Regex GetPublishedModelsTypesRegex = new Regex("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled); + + public void OnException(ExceptionContext filterContext) + { + if (!filterContext.ExceptionHandled + && ((filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException) + && IsMessageAboutTheSameModelType(filterContext.Exception.Message))) + { + filterContext.HttpContext.Response.Headers.Add(HttpResponseHeader.RetryAfter.ToString(), "1"); + filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.RawUrl, false); + + filterContext.ExceptionHandled = true; + } + } + + /// + /// Returns true if the message is about two models with the same name. + /// + /// + /// Message could be something like: + /// + /// InvalidCastException: + /// [A]Umbraco.Web.PublishedModels.Home cannot be cast to [B]Umbraco.Web.PublishedModels.Home. Type A originates from 'App_Web_all.generated.cs.8f9494c4.rtdigm_z, Version=0.0.0.3, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rtdigm_z.dll'. Type B originates from 'App_Web_all.generated.cs.8f9494c4.rbyqlplu, Version=0.0.0.5, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\User\AppData\Local\Temp\Temporary ASP.NET Files\root\c5c63f4d\c168d9d4\App_Web_all.generated.cs.8f9494c4.rbyqlplu.dll'. + /// + /// + /// ModelBindingException: + /// Cannot bind source content type Umbraco.Web.PublishedModels.Home to model type Umbraco.Web.PublishedModels.Home. Both view and content models are PureLive, with different versions. The application is in an unstable state and is going to be restarted. The application is restarting now. + /// + /// + private bool IsMessageAboutTheSameModelType(string exceptionMessage) + { + var matches = GetPublishedModelsTypesRegex.Matches(exceptionMessage); + + if (matches.Count >= 2) + { + return string.Equals(matches[0].Value, matches[1].Value, StringComparison.InvariantCulture); + } + + return false; + } + } +} diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index 52d168620b..437eec4e5c 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.Mvc if (_publishedContent != null) return _publishedContent; //need to get the URL for the page - _publishedContent = Current.UmbracoContext.ContentCache.GetById(_pageId); + _publishedContent = Current.UmbracoContext.Content.GetById(_pageId); return _publishedContent; } diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs index c2aa3bd8ed..64c9ad52c4 100644 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/RenderMvcController.cs @@ -10,10 +10,12 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { + /// /// Represents the default front-end rendering controller. /// [PreRenderViewActionFilter] + [ModelBindingExceptionFilter] public class RenderMvcController : UmbracoController, IRenderMvcController { private PublishedRequest _publishedRequest; diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 54215c2e8c..284c1344b6 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Mvc public class RenderRouteHandler : IRouteHandler { // Define reserved dictionary keys for controller, action and area specified in route additional values data - private static class ReservedAdditionalKeys + internal static class ReservedAdditionalKeys { internal const string Controller = "c"; internal const string Action = "a"; @@ -134,36 +134,7 @@ namespace Umbraco.Web.Mvc return null; } - - string decryptedString; - try - { - decryptedString = encodedVal.DecryptWithMachineKey(); - } - catch (FormatException) - { - Current.Logger.Warn("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); - return null; - } - - var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); - var decodedParts = new Dictionary(); - - foreach (var key in parsedQueryString.AllKeys) - { - decodedParts[key] = parsedQueryString[key]; - } - - //validate all required keys exist - - //the controller - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Controller)) - return null; - //the action - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Action)) - return null; - //the area - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Area)) + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(encodedVal, out var decodedParts)) return null; foreach (var item in decodedParts.Where(x => new[] { @@ -184,6 +155,8 @@ namespace Umbraco.Web.Mvc }; } + + /// /// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that /// the right DataTokens are set. @@ -417,7 +390,7 @@ namespace Umbraco.Web.Mvc return new UmbracoMvcHandler(requestContext); } - + private SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext, string controllerName) { return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs index 304f59b64b..0ada310b12 100644 --- a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeByIdRouteHandler.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Mvc protected sealed override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext) { - var byId = umbracoContext.ContentCache.GetById(_realNodeId); + var byId = umbracoContext.Content.GetById(_realNodeId); return byId == null ? null : FindContent(requestContext, umbracoContext, byId); } diff --git a/src/Umbraco.Web/Profiling/WebProfilingController.cs b/src/Umbraco.Web/Profiling/WebProfilingController.cs new file mode 100644 index 0000000000..b3d580bc38 --- /dev/null +++ b/src/Umbraco.Web/Profiling/WebProfilingController.cs @@ -0,0 +1,19 @@ +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Profiling +{ + /// + /// The API controller used to display the state of the web profiler + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class WebProfilingController : UmbracoAuthorizedJsonController + { + public object GetStatus() + { + return new + { + Enabled = Core.Configuration.GlobalSettings.DebugMode + }; + } + }} diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index 359e8ac3b6..9a1b248284 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -32,9 +32,6 @@ using System.Runtime.InteropServices; // Umbraco Headless [assembly: InternalsVisibleTo("Umbraco.Headless")] -// Umbraco ModelsBuilder -[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] - // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")] diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs index 7879e2b42b..021d416781 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs @@ -3,12 +3,17 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - public class ContentPickerConfiguration + public class ContentPickerConfiguration : IIgnoreUserStartNodesConfig { - [ConfigurationField("showOpenButton", "Show open button (this feature is in beta!)", "boolean", Description = "Opens the node in a dialog")] + [ConfigurationField("showOpenButton", "Show open button", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpenButton { get; set; } [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor public Udi StartNodeId { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 81239caec0..942f53b561 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath diff --git a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs index e2b46b360d..b7a76ac960 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the grid value editor. /// - public class GridConfiguration + public class GridConfiguration : IIgnoreUserStartNodesConfig { // TODO: Make these strongly typed, for now this works though [ConfigurationField("items", "Grid", "views/propertyeditors/grid/grid.prevalues.html", Description = "Grid configuration")] @@ -15,5 +15,10 @@ namespace Umbraco.Web.PropertyEditors // TODO: Make these strongly typed, for now this works though [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public JObject Rte { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 7bea542521..4aac8f54aa 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath @@ -174,7 +174,12 @@ namespace Umbraco.Web.PropertyEditors // more magic here ;-( var configuration = dataTypeService.GetDataType(propertyType.DataTypeId).ConfigurationAs(); var crops = configuration?.Crops ?? Array.Empty(); - return "{src: '" + val + "', crops: " + crops + "}"; + + return JsonConvert.SerializeObject(new + { + src = val, + crops = crops + }); } } } diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index 8e2732cc0f..f170608545 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a list-view editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.ListView, "List view", "listview", HideLabel = true, Group = "lists", Icon = "icon-item-arrangement")] + [DataEditor(Constants.PropertyEditors.Aliases.ListView, "List view", "listview", HideLabel = true, Group = "lists", Icon = Constants.Icons.ListView)] public class ListViewPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs index d4d23da3a4..99a9f44487 100644 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs @@ -5,7 +5,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { // TODO: MacroContainerPropertyEditor is deprecated, but what's the alternative? - [DataEditor(Constants.PropertyEditors.Aliases.MacroContainer, "(Obsolete) Macro Picker", "macrocontainer", ValueType = ValueTypes.Text, Group="rich content", Icon="icon-settings-alt", IsDeprecated = true)] + [DataEditor(Constants.PropertyEditors.Aliases.MacroContainer, "(Obsolete) Macro Picker", "macrocontainer", ValueType = ValueTypes.Text, Group = "rich content", Icon = Constants.Icons.Macro, IsDeprecated = true)] public class MacroContainerPropertyEditor : DataEditor { public MacroContainerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index 4844e2f822..b8b9476184 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the media picker value editor. /// - public class MediaPickerConfiguration + public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] public bool Multiple { get; set; } @@ -19,5 +19,10 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "mediapicker")] public Udi StartNodeId { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 6c768f4932..52e616ffbd 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.PropertyEditors /// Represents a media picker property editor. /// [DataEditor(Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "Media Picker", "mediapicker", ValueType = ValueTypes.Text, Group = "media", Icon = "icon-picture")] + "Media Picker", "mediapicker", ValueType = ValueTypes.Text, Group = "media", Icon = Constants.Icons.MediaImage)] public class MediaPickerPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs index b917145dbd..5d89024692 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -4,7 +4,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", ValueType = ValueTypes.Text, Group = "People", Icon = "icon-users")] + [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", ValueType = ValueTypes.Text, Group = "People", Icon = Constants.Icons.MemberGroup)] public class MemberGroupPickerPropertyEditor : DataEditor { public MemberGroupPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs index a0705fb373..858582ab72 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs @@ -4,7 +4,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberPicker, "Member Picker", "memberpicker", ValueType = ValueTypes.String, Group = "People", Icon = "icon-user")] + [DataEditor(Constants.PropertyEditors.Aliases.MemberPicker, "Member Picker", "memberpicker", ValueType = ValueTypes.String, Group = "People", Icon = Constants.Icons.Member)] public class MemberPickerPropertyEditor : DataEditor { public MemberPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index b6333c3140..b099573b9f 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -6,12 +6,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the multinode picker value editor. /// - public class MultiNodePickerConfiguration + public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("startNode", "Node type", "treesource")] public MultiNodePickerConfigurationTreeSource TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "textstring", Description = "Separate with comma")] + [ConfigurationField("filter", "Allow items of type", "contenttypepicker", Description = "Select the applicable content types")] public string Filter { get; set; } [ConfigurationField("minNumber", "Minimum number of items", "number")] @@ -22,5 +22,10 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpen { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs index 515512eff8..16aff6e0bf 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -2,12 +2,20 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerConfiguration + + public class MultiUrlPickerConfiguration : IIgnoreUserStartNodesConfig { [ConfigurationField("minNumber", "Minimum number of items", "number")] public int MinNumber { get; set; } [ConfigurationField("maxNumber", "Maximum number of items", "number")] public int MaxNumber { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 129df2bacf..aa8fa73c7a 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -81,7 +81,7 @@ namespace Umbraco.Web.PropertyEditors icon = documentEntity.ContentTypeIcon; published = culture == null ? documentEntity.Published : documentEntity.PublishedCultures.Contains(culture); udi = new GuidUdi(Constants.UdiEntityType.Document, documentEntity.Key); - url = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(entity.Key)?.Url ?? "#"; + url = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(entity.Key)?.Url() ?? "#"; trashed = documentEntity.Trashed; } else if(entity is IContentEntitySlim contentEntity) @@ -89,7 +89,7 @@ namespace Umbraco.Web.PropertyEditors icon = contentEntity.ContentTypeIcon; published = !contentEntity.Trashed; udi = new GuidUdi(Constants.UdiEntityType.Media, contentEntity.Key); - url = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(entity.Key)?.Url ?? "#"; + url = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(entity.Key)?.Url() ?? "#"; trashed = contentEntity.Trashed; } else diff --git a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs index 13bf269bcd..bd153c8e2f 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents the configuration for the rich text value editor. /// - public class RichTextConfiguration + public class RichTextConfiguration : IIgnoreUserStartNodesConfig { // TODO: Make these strongly typed, for now this works though [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] @@ -14,5 +14,10 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("hideLabel", "Hide Label", "boolean")] public bool HideLabel { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs index 5394aca5ba..6855ab3bb8 100644 --- a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Services; -using Umbraco.Core.Configuration; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Composing; @@ -16,13 +16,27 @@ namespace Umbraco.Web.PropertyEditors { public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { - if (!(value is JObject jobject) || jobject["selectedFiles"] == null) yield break; - - var fileNames = jobject["selectedFiles"].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var fileName in fileNames) + string selectedFiles = null; + if (value is JObject jobject && jobject["selectedFiles"] is JToken jToken) { - if (ValidateFileExtension(fileName) == false) + selectedFiles = jToken.ToString(); + } + else if (valueType?.InvariantEquals(ValueTypes.String) == true) + { + selectedFiles = value as string; + + if (string.IsNullOrWhiteSpace(selectedFiles)) + yield break; + } + + var fileNames = selectedFiles?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (fileNames == null || !fileNames.Any()) + yield break; + + foreach (string filename in fileNames) + { + if (IsValidFileExtension(filename) == false) { //we only store a single value for this editor so the 'member' or 'field' // we'll associate this error with will simply be called 'value' @@ -30,11 +44,11 @@ namespace Umbraco.Web.PropertyEditors } } } - - internal static bool ValidateFileExtension(string fileName) + + internal static bool IsValidFileExtension(string fileName) { if (fileName.IndexOf('.') <= 0) return false; - var extension = Path.GetExtension(fileName).TrimStart("."); + var extension = new FileInfo(fileName).Extension.TrimStart("."); return Current.Configs.Settings().Content.IsFileAllowedForUpload(extension); } } diff --git a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs index 8cb8b64594..1d3ab05e96 100644 --- a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs @@ -6,7 +6,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.UserPicker, "User picker", "entitypicker", ValueType = ValueTypes.Integer, Group = "People", Icon = "icon-user")] + [DataEditor(Constants.PropertyEditors.Aliases.UserPicker, "User picker", "entitypicker", ValueType = ValueTypes.Integer, Group = "People", Icon = Constants.Icons.User)] public class UserPickerPropertyEditor : DataEditor { public UserPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index d30762f13f..8b3655f0cc 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -23,16 +23,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.ContentPicker); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IPublishedContent); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -45,7 +45,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; @@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (udi == null) return null; content = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(udi.Guid); - if (content != null && content.ItemType == PublishedItemType.Content) + if (content != null && content.ContentType.ItemType == PublishedItemType.Content) return content; } } @@ -73,7 +73,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return inter; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; return inter.ToString(); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs index 43add34327..0d6089f0f4 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -11,12 +11,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class FlexibleDropdownPropertyValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DropDownListFlexible); } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if(source == null) return Array.Empty(); @@ -24,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return JsonConvert.DeserializeObject(source.ToString()) ?? Array.Empty(); } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) return null; @@ -43,7 +43,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters : string.Empty; } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { return propertyType.DataType.ConfigurationAs().Multiple ? typeof(IEnumerable) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs index e6e66b79ff..ef356e3604 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs @@ -28,13 +28,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _macroRenderer = macroRenderer ?? throw new ArgumentNullException(nameof(macroRenderer)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.MacroContainer; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; // NOT thread-safe over a request because it modifies the @@ -63,7 +63,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index cb9709157f..e11f3e0d3a 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -12,16 +12,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor == propertyType.EditorAlias; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IHtmlString); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); @@ -33,7 +33,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return sourceString; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // convert markup to HTML for frontend rendering. // source should come from ConvertSource and be a string (or null) already diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 0218867bb4..d2272a25b5 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -29,12 +29,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedModelFactory = publishedModelFactory; } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MediaPicker); } - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var isMultiple = IsMultipleDataType(propertyType.DataType); var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); @@ -48,7 +48,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters : typeof(IPublishedContent); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; private bool IsMultipleDataType(PublishedDataType dataType) @@ -63,7 +63,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.OnlyImages; } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -75,7 +75,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return nodeIds; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { var isMultiple = IsMultipleDataType(propertyType.DataType); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index b4c7f99a75..cd69fd9de6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -18,18 +18,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.MemberPicker); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (IPublishedContent); - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { var attemptConvertInt = source.TryConvertTo(); if (attemptConvertInt.Success) @@ -40,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index 4bb33fd2da..47f8797295 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -34,20 +34,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); } - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => IsSingleNodePicker(propertyType) ? typeof(IPublishedContent) : typeof(IEnumerable); - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; @@ -62,7 +62,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) { if (source == null) { @@ -102,7 +102,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters break; } - if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ItemType != PublishedItemType.Element) + if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element) { multiNodeTreePicker.Add(multiNodeTreePickerItem); if (isSingleNodePicker) @@ -153,7 +153,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return content; } - private static bool IsSingleNodePicker(PublishedPropertyType propertyType) + private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) { return propertyType.DataType.ConfigurationAs().MaxNumber == 1; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index 48cec2a3d2..2ceac6cfa8 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -23,20 +23,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _proflog = proflog ?? throw new ArgumentNullException(nameof(proflog)); } - public override bool IsConverter(PublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultiUrlPicker.Equals(propertyType.EditorAlias); + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MultiUrlPicker.Equals(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) => + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => propertyType.DataType.ConfigurationAs().MaxNumber == 1 ? typeof(Link) : typeof(IEnumerable); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { using (_proflog.DebugDuration($"ConvertPropertyToLinks ({propertyType.DataType.Id})")) { @@ -65,11 +65,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.Udi.Guid) : _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(preview, dto.Udi.Guid); - if (content == null || content.ItemType == PublishedItemType.Element) + if (content == null || content.ContentType.ItemType == PublishedItemType.Element) { continue; } - url = content.Url; + url = content.Url(); } links.Add( diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 2181c4fd49..559777786f 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -28,11 +28,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => IsNestedMany(propertyType); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; return contentTypes.Length == 1 @@ -41,19 +41,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } /// - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) + using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index e084b3a343..06aa0b42fb 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -27,11 +27,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => IsNestedSingle(propertyType); /// - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; return contentTypes.Length > 1 @@ -40,19 +40,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } /// - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { return source?.ToString(); } /// - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) + using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { var value = (string) inter; if (string.IsNullOrWhiteSpace(value)) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs index e3723e2221..7c18d8ebca 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs @@ -20,12 +20,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters protected IPublishedModelFactory PublishedModelFactory { get; } - public static bool IsNested(PublishedPropertyType publishedProperty) + public static bool IsNested(IPublishedPropertyType publishedProperty) { return publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); } - public static bool IsNestedSingle(PublishedPropertyType publishedProperty) + public static bool IsNestedSingle(IPublishedPropertyType publishedProperty) { if (!IsNested(publishedProperty)) return false; @@ -34,7 +34,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.MinItems == 1 && config.MaxItems == 1; } - public static bool IsNestedMany(PublishedPropertyType publishedProperty) + public static bool IsNestedMany(IPublishedPropertyType publishedProperty) { return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index fa14bd8488..cb6ce6dd6d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { // because that version of RTE converter parses {locallink} and executes macros, its value has // to be cached at the published snapshot level, because we have no idea what the macros may depend on actually. @@ -63,7 +63,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) { diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2d5c322f58..b8ad1477b4 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -17,16 +17,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextArea }; - public override bool IsConverter(PublishedPropertyType propertyType) + public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); - public override Type GetPropertyValueType(PublishedPropertyType propertyType) + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (string); - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { if (source == null) return null; var sourceString = source.ToString(); @@ -38,13 +38,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return sourceString; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter ?? string.Empty; } - public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { // source should come from ConvertSource and be a string (or null) already return inter; diff --git a/src/Umbraco.Web/PublishedCache/IDomainCache.cs b/src/Umbraco.Web/PublishedCache/IDomainCache.cs index dbee8908a0..3ec84c9d48 100644 --- a/src/Umbraco.Web/PublishedCache/IDomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/IDomainCache.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache @@ -6,20 +8,29 @@ namespace Umbraco.Web.PublishedCache public interface IDomainCache { /// - /// Returns all in the current domain cache including any domains that may be referenced by content items that are no longer published + /// Gets all in the current domain cache, including any domains that may be referenced by documents that are no longer published. /// /// /// IEnumerable GetAll(bool includeWildcards); /// - /// Returns all assigned for the content id specified even if the content item is not published + /// Gets all assigned for specified document, even if it is not published. /// - /// - /// - /// - IEnumerable GetAssigned(int contentId, bool includeWildcards); + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + IEnumerable GetAssigned(int documentId, bool includeWildcards = false); + /// + /// Determines whether a document has domains. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + bool HasAssigned(int documentId, bool includeWildcards = false); + + /// + /// Gets the system default culture. + /// string DefaultCulture { get; } } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 3cd7b924fb..0370088f77 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -84,16 +84,18 @@ namespace Umbraco.Web.PublishedCache /// Gets contents at root. /// /// A value indicating whether to consider unpublished content. + /// A culture. /// The contents. /// The value of overrides defaults. - IEnumerable GetAtRoot(bool preview); + IEnumerable GetAtRoot(bool preview, string culture = null); /// /// Gets contents at root. /// + /// A culture. /// The contents. /// Considers published or unpublished content depending on defaults. - IEnumerable GetAtRoot(); + IEnumerable GetAtRoot(string culture = null); /// /// Gets a content resulting from an XPath query. @@ -217,7 +219,7 @@ namespace Umbraco.Web.PublishedCache /// /// The content type unique identifier. /// The content type, or null. - PublishedContentType GetContentType(int id); + IPublishedContentType GetContentType(int id); /// /// Gets a content type identified by its alias. @@ -225,13 +227,13 @@ namespace Umbraco.Web.PublishedCache /// The content type alias. /// The content type, or null. /// The alias is case-insensitive. - PublishedContentType GetContentType(string alias); + IPublishedContentType GetContentType(string alias); /// /// Gets contents of a given content type. /// /// The content type. /// The contents. - IEnumerable GetByContentType(PublishedContentType contentType); + IEnumerable GetByContentType(IPublishedContentType contentType); } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs index 53d37a8d31..0ea812db83 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedMemberCache.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.PublishedCache /// /// The content type unique identifier. /// The content type, or null. - PublishedContentType GetContentType(int id); + IPublishedContentType GetContentType(int id); /// /// Gets a content type identified by its alias. @@ -30,6 +30,6 @@ namespace Umbraco.Web.PublishedCache /// The content type alias. /// The content type, or null. /// The alias is case-insensitive. - PublishedContentType GetContentType(string alias); + IPublishedContentType GetContentType(string alias); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 4bd3fcf247..84edb9113c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -1,20 +1,15 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Core.Xml.XPath; using Umbraco.Web.PublishedCache.NuCache.Navigable; -using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache { @@ -23,9 +18,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore.Snapshot _snapshot; private readonly IAppCache _snapshotCache; private readonly IAppCache _elementsCache; - private readonly DomainHelper _domainHelper; + private readonly IDomainCache _domainCache; private readonly IGlobalSettings _globalSettings; - private readonly ILocalizationService _localizationService; + private readonly IVariationContextAccessor _variationContextAccessor; #region Constructor @@ -34,15 +29,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // it's too late for UmbracoContext which has captured previewDefault and stuff into these ctor vars // but, no, UmbracoContext returns snapshot.Content which comes from elements SO a resync should create a new cache - public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, DomainHelper domainHelper, IGlobalSettings globalSettings, ILocalizationService localizationService) + public ContentCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache, IDomainCache domainCache, IGlobalSettings globalSettings, IVariationContextAccessor variationContextAccessor) : base(previewDefault) { _snapshot = snapshot; _snapshotCache = snapshotCache; _elementsCache = elementsCache; - _domainHelper = domainHelper; + _domainCache = domainCache; _globalSettings = globalSettings; - _localizationService = localizationService; + _variationContextAccessor = variationContextAccessor; } private bool HideTopLevelNodeFromPath => _globalSettings.HideTopLevelNodeFromPath; @@ -109,8 +104,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // hideTopLevelNode = support legacy stuff, look for /*/path/to/node // else normal, look for /path/to/node content = hideTopLevelNode.Value - ? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]) - : GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]); + ? GetAtRoot(preview).SelectMany(x => x.Children(culture)).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]) + : GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]); content = FollowRoute(content, parts, 1, culture); } @@ -119,7 +114,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). if (content == null && hideTopLevelNode.Value && parts.Length == 1) { - content = GetAtRoot(preview).FirstOrDefault(x => x.GetUrlSegment(culture) == parts[0]); + content = GetAtRoot(preview).FirstOrDefault(x => x.UrlSegment(culture) == parts[0]); } return content; @@ -149,8 +144,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // or we reach the content root, collecting urls in the way var pathParts = new List(); var n = node; - var urlSegment = n.GetUrlSegment(culture); - var hasDomains = _domainHelper.NodeHasDomains(n.Id); + var urlSegment = n.UrlSegment(culture); + var hasDomains = _domainCache.HasAssigned(n.Id); while (hasDomains == false && n != null) // n is null at root { // no segment indicates this is not published when this is a variant @@ -161,9 +156,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // move to parent node n = n.Parent; if (n != null) - urlSegment = n.GetUrlSegment(culture); + urlSegment = n.UrlSegment(culture); - hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id); + hasDomains = n != null && _domainCache.HasAssigned(n.Id); } // at this point this will be the urlSegment of the root, no segment indicates this is not published when this is a variant @@ -189,9 +184,9 @@ namespace Umbraco.Web.PublishedCache.NuCache while (content != null && i < parts.Count) { var part = parts[i++]; - content = content.Children.FirstOrDefault(x => + content = content.Children(culture).FirstOrDefault(x => { - var urlSegment = x.GetUrlSegment(culture); + var urlSegment = x.UrlSegment(culture); return urlSegment == part; }); } @@ -243,7 +238,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var guidUdi = contentId as GuidUdi; if (guidUdi == null) throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); - + if (guidUdi.EntityType != Constants.UdiEntityType.Document) throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); @@ -258,30 +253,30 @@ namespace Umbraco.Web.PublishedCache.NuCache return preview || n.PublishedModel != null; } - public override IEnumerable GetAtRoot(bool preview) + IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); + + public override IEnumerable GetAtRoot(bool preview, string culture = null) { - if (PublishedSnapshotService.CacheContentCacheRoots == false) - return GetAtRootNoCache(preview); - - var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - - if (cache == null) - return GetAtRootNoCache(preview); - - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get( - CacheKeys.ContentCacheRoots(preview), - () => GetAtRootNoCache(preview).ToArray()); - } - - private IEnumerable GetAtRootNoCache(bool preview) - { - var c = _snapshot.GetAtRoot(); + // handle context culture for variant + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + // _snapshot.GetAtRoot() returns all ContentNode at root // both .Draft and .Published cannot be null at the same time - return c.Select(n => GetNodePublishedContent(n, preview)).WhereNotNull().OrderBy(x => x.SortOrder); + // root is already sorted by sortOrder, and does not contain nulls + // + // GetNodePublishedContent may return null if !preview and there is no + // published model, so we need to filter these nulls out + + var atRoot = _snapshot.GetAtRoot() + .Select(n => GetNodePublishedContent(n, preview)) + .WhereNotNull(); + + // if a culture is specified, we must ensure that it is avail/published + if (culture != "*") + atRoot = atRoot.Where(x => x.IsInvariantOrHasCulture(culture)); + + return atRoot; } private static IPublishedContent GetNodePublishedContent(ContentNode node, bool preview) @@ -388,12 +383,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _snapshot.GetContentType(id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _snapshot.GetContentType(alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index db7aa0d5d1..1e8d5ddfc9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -9,11 +8,19 @@ namespace Umbraco.Web.PublishedCache.NuCache // internal, never exposed, to be accessed from ContentStore (only!) internal class ContentNode { + // special ctor for root pseudo node + public ContentNode() + { + FirstChildContentId = -1; + NextSiblingContentId = -1; + } + // special ctor with no content data - for members - public ContentNode(int id, Guid uid, PublishedContentType contentType, + public ContentNode(int id, Guid uid, IPublishedContentType contentType, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId) + : this() { Id = id; Uid = uid; @@ -24,21 +31,18 @@ namespace Umbraco.Web.PublishedCache.NuCache ParentContentId = parentContentId; CreateDate = createDate; CreatorId = creatorId; - - ChildContentIds = new List(); } - public ContentNode(int id, Guid uid, PublishedContentType contentType, + public ContentNode(int id, Guid uid, IPublishedContentType contentType, int level, string path, int sortOrder, int parentContentId, DateTime createDate, int creatorId, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) : this(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId) { - SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); + SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor); } // 2-phases ctor, phase 1 @@ -53,14 +57,14 @@ namespace Umbraco.Web.PublishedCache.NuCache Path = path; SortOrder = sortOrder; ParentContentId = parentContentId; + FirstChildContentId = -1; + NextSiblingContentId = -1; CreateDate = createDate; CreatorId = creatorId; - - ChildContentIds = new List(); } // two-phase ctor, phase 2 - public void SetContentTypeAndData(PublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) + public void SetContentTypeAndData(IPublishedContentType contentType, ContentData draftData, ContentData publishedData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { ContentType = contentType; @@ -69,79 +73,53 @@ namespace Umbraco.Web.PublishedCache.NuCache if (draftData != null) { - DraftContent = new PublishedContent(this, draftData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); + DraftContent = new PublishedContent(this, draftData, publishedSnapshotAccessor, variationContextAccessor); DraftModel = DraftContent.CreateModel(); } if (publishedData != null) { - PublishedContent = new PublishedContent(this, publishedData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); + PublishedContent = new PublishedContent(this, publishedData, publishedSnapshotAccessor, variationContextAccessor); PublishedModel = PublishedContent.CreateModel(); } } - // clone parent - private ContentNode(ContentNode origin, IUmbracoContextAccessor umbracoContextAccessor) + // clone + public ContentNode(ContentNode origin, IPublishedContentType contentType = null) { - // everything is the same, except for the child items - // list which is a clone of the original list - Id = origin.Id; Uid = origin.Uid; - ContentType = origin.ContentType; + ContentType = contentType ?? origin.ContentType; Level = origin.Level; Path = origin.Path; SortOrder = origin.SortOrder; ParentContentId = origin.ParentContentId; + FirstChildContentId = origin.FirstChildContentId; + NextSiblingContentId = origin.NextSiblingContentId; CreateDate = origin.CreateDate; CreatorId = origin.CreatorId; var originDraft = origin.DraftContent; var originPublished = origin.PublishedContent; + DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft); + PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished); - DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft, umbracoContextAccessor); - PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished, umbracoContextAccessor); DraftModel = DraftContent?.CreateModel(); PublishedModel = PublishedContent?.CreateModel(); - - ChildContentIds = new List(origin.ChildContentIds); // needs to be *another* list - } - - // clone with new content type - public ContentNode(ContentNode origin, PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) - { - Id = origin.Id; - Uid = origin.Uid; - ContentType = contentType; // change! - Level = origin.Level; - Path = origin.Path; - SortOrder = origin.SortOrder; - ParentContentId = origin.ParentContentId; - CreateDate = origin.CreateDate; - CreatorId = origin.CreatorId; - - var originDraft = origin.DraftContent; - var originPublished = origin.PublishedContent; - - DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft.ContentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - DraftModel = DraftContent?.CreateModel(); - PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished.ContentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor); - PublishedModel = PublishedContent?.CreateModel(); - - ChildContentIds = origin.ChildContentIds; // can be the *same* list } // everything that is common to both draft and published versions // keep this as small as possible public readonly int Id; public readonly Guid Uid; - public PublishedContentType ContentType; + public IPublishedContentType ContentType; public readonly int Level; public readonly string Path; public readonly int SortOrder; public readonly int ParentContentId; - public List ChildContentIds; + public int FirstChildContentId; + public int NextSiblingContentId; public readonly DateTime CreateDate; public readonly int CreatorId; @@ -155,23 +133,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public IPublishedContent DraftModel; public IPublishedContent PublishedModel; - public ContentNode CloneParent( - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IUmbracoContextAccessor umbracoContextAccessor) - { - return new ContentNode(this, umbracoContextAccessor); - } - public ContentNodeKit ToKit() - { - return new ContentNodeKit - { - Node = this, - ContentTypeId = ContentType.Id, + => new ContentNodeKit + { + Node = this, + ContentTypeId = ContentType.Id, - DraftData = DraftContent?.ContentData, - PublishedData = PublishedContent?.ContentData - }; - } + DraftData = DraftContent?.ContentData, + PublishedData = PublishedContent?.ContentData + }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index 753ba5cc94..61fb5c12a3 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -18,11 +18,10 @@ namespace Umbraco.Web.PublishedCache.NuCache public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; public void Build( - PublishedContentType contentType, + IPublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - bool canBePublished, - IUmbracoContextAccessor umbracoContextAccessor) + bool canBePublished) { var draftData = DraftData; @@ -35,7 +34,16 @@ namespace Umbraco.Web.PublishedCache.NuCache if (draftData == null && !canBePublished) draftData = PublishedData; - Node.SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor,umbracoContextAccessor); + Node.SetContentTypeAndData(contentType, draftData, publishedData, publishedSnapshotAccessor, variationContextAccessor); } + + public ContentNodeKit Clone() + => new ContentNodeKit + { + ContentTypeId = ContentTypeId, + DraftData = DraftData, + PublishedData = PublishedData, + Node = new ContentNode(Node) + }; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 48c68ab9bf..2d501fa3b5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CSharpTest.Net.Collections; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; @@ -21,11 +22,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly ConcurrentDictionary> _contentNodes; - private readonly ConcurrentDictionary> _contentRootNodes; - private readonly ConcurrentDictionary> _contentTypesById; - private readonly ConcurrentDictionary> _contentTypesByAlias; + private LinkedNode _root; + private readonly ConcurrentDictionary> _contentTypesById; + private readonly ConcurrentDictionary> _contentTypesByAlias; private readonly ConcurrentDictionary _xmap; private readonly ILogger _logger; @@ -49,20 +49,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public ContentStore( IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, BPlusTree localDb = null) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _variationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; _localDb = localDb; _contentNodes = new ConcurrentDictionary>(); - _contentRootNodes = new ConcurrentDictionary>(); - _contentTypesById = new ConcurrentDictionary>(); - _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); + _root = new LinkedNode(new ContentNode(), 0); + _contentTypesById = new ConcurrentDictionary>(); + _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); _xmap = new ConcurrentDictionary(); _genObjs = new ConcurrentQueue(); @@ -176,7 +174,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } Rollback(_contentNodes); - Rollback(_contentRootNodes); + RollbackRoot(); Rollback(_contentTypesById); Rollback(_contentTypesByAlias); } @@ -202,6 +200,14 @@ namespace Umbraco.Web.PublishedCache.NuCache if (lockInfo.Taken) Monitor.Exit(_rlocko); } + private void RollbackRoot() + { + if (_root.Gen <= _liveGen) return; + + if (_root.Next != null) + _root = _root.Next; + } + private void Rollback(ConcurrentDictionary> dictionary) where TValue : class { @@ -249,7 +255,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public void NewContentTypes(IEnumerable types) + public void NewContentTypes(IEnumerable types) { var lockInfo = new WriteLockInfo(); try @@ -268,8 +274,11 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void UpdateContentTypes(IEnumerable types) + public void UpdateContentTypes(IEnumerable types) { + //nothing to do if this is empty, no need to lock/allocate/iterate/etc... + if (!types.Any()) return; + var lockInfo = new WriteLockInfo(); try { @@ -288,8 +297,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var node = link.Value; if (node == null) continue; var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out PublishedContentType contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType, _publishedSnapshotAccessor, _variationContextAccessor, _umbracoContextAccessor)); + if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); } } finally @@ -298,13 +307,43 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void UpdateContentTypes(IEnumerable removedIds, IEnumerable refreshedTypes, IEnumerable kits) + public void SetAllContentTypes(IEnumerable types) { - var removedIdsA = removedIds?.ToArray() ?? Array.Empty(); - var refreshedTypesA = refreshedTypes?.ToArray() ?? Array.Empty(); - var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToArray(); + var lockInfo = new WriteLockInfo(); + try + { + Lock(lockInfo); + + // clear all existing content types + ClearLocked(_contentTypesById); + ClearLocked(_contentTypesByAlias); + + // set all new content types + foreach (var type in types) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + } + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! + } + finally + { + Release(lockInfo); + } + } + + public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) + { + var removedIdsA = removedIds ?? Array.Empty(); + var refreshedTypesA = refreshedTypes ?? Array.Empty(); + var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); kits = kits ?? Array.Empty(); + if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) + return; //exit - there is nothing to do here + var lockInfo = new WriteLockInfo(); try { @@ -347,18 +386,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & unbuildable kits - what else could we do? - // kits are ordered by level, so ParentExits is ok here + // skip missing type, skip missing parents & un-buildable kits - what else could we do? + // kits are ordered by level, so ParentExists is ok here var visited = new List(); foreach (var kit in kits.Where(x => refreshedIdsA.Contains(x.ContentTypeId) && - ParentExistsLocked(x) && BuildKit(x))) { // replacing the node: must preserve the parents var node = GetHead(_contentNodes, kit.Node.Id)?.Value; if (node != null) - kit.Node.ChildContentIds = node.ChildContentIds; + kit.Node.FirstChildContentId = node.FirstChildContentId; SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -377,7 +415,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) + public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) { var lockInfo = new WriteLockInfo(); try @@ -412,10 +450,10 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var id in contentTypeNodes[contentType.Id]) { - _contentNodes.TryGetValue(id, out LinkedNode link); + _contentNodes.TryGetValue(id, out var link); if (link?.Value == null) continue; - var node = new ContentNode(link.Value, contentType, _publishedSnapshotAccessor, _variationContextAccessor, _umbracoContextAccessor); + var node = new ContentNode(link.Value, contentType); SetValueLocked(_contentNodes, id, node); if (_localDb != null) RegisterChange(id, node.ToKit()); } @@ -429,19 +467,32 @@ namespace Umbraco.Web.PublishedCache.NuCache private bool BuildKit(ContentNodeKit kit) { + // make sure parent exists + if (!ParentExistsLocked(kit)) + { + _logger.Warn($"Skip item id={kit.Node.Id}, could not find parent id={kit.Node.ParentContentId}."); + return false; + } + // make sure the kit is valid if (kit.DraftData == null && kit.PublishedData == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, both draft and published data are null."); return false; + } // unknown = bad - if (_contentTypesById.TryGetValue(kit.ContentTypeId, out LinkedNode link) == false || link.Value == null) + if (_contentTypesById.TryGetValue(kit.ContentTypeId, out var link) == false || link.Value == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, could not find content type id={kit.ContentTypeId}."); return false; + } // check whether parent is published var canBePublished = ParentPublishedLocked(kit); // and use - kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished, _umbracoContextAccessor); + kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished); return true; } @@ -455,16 +506,16 @@ namespace Umbraco.Web.PublishedCache.NuCache private static LinkedNode GetHead(ConcurrentDictionary> dict, TKey key) where TValue : class { - dict.TryGetValue(key, out LinkedNode link); // else null + dict.TryGetValue(key, out var link); // else null return link; } - public void Set(ContentNodeKit kit) + public bool Set(ContentNodeKit kit) { // ReSharper disable LocalizableElement if (kit.IsEmpty) throw new ArgumentException("Kit is empty.", nameof(kit)); - if (kit.Node.ChildContentIds.Count > 0) + if (kit.Node.FirstChildContentId > 0) throw new ArgumentException("Kit content cannot have children.", nameof(kit)); // ReSharper restore LocalizableElement @@ -476,19 +527,18 @@ namespace Umbraco.Web.PublishedCache.NuCache Lock(lockInfo); // get existing - _contentNodes.TryGetValue(kit.Node.Id, out LinkedNode link); + _contentNodes.TryGetValue(kit.Node.Id, out var link); var existing = link?.Value; - // else ignore, what else could we do? - if (ParentExistsLocked(kit) == false || BuildKit(kit) == false) - return; + if (!BuildKit(kit)) + return false; // moving? var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; // manage children if (existing != null) - kit.Node.ChildContentIds = existing.ChildContentIds; + kit.Node.FirstChildContentId = existing.FirstChildContentId; // set SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -498,13 +548,18 @@ namespace Umbraco.Web.PublishedCache.NuCache if (existing == null) { // new, add to parent - AddToParentLocked(kit.Node); + AddNodeLocked(kit.Node); } - else if (moving) + else if (moving || existing.SortOrder != kit.Node.SortOrder) { // moved, remove existing from its parent, add content to its parent - RemoveFromParentLocked(existing); - AddToParentLocked(kit.Node); + RemoveNodeLocked(existing); + AddNodeLocked(kit.Node); + } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; } _xmap[kit.Node.Uid] = kit.Node.Id; @@ -513,29 +568,47 @@ namespace Umbraco.Web.PublishedCache.NuCache { Release(lockInfo); } + + return true; + } + + private void ClearRootLocked() + { + if (_root.Gen < _liveGen) + _root = new LinkedNode(new ContentNode(), _liveGen, _root); + else + _root.Value.FirstChildContentId = -1; } // IMPORTANT kits must be sorted out by LEVEL - public void SetAll(IEnumerable kits) + public bool SetAll(IEnumerable kits, bool fromLocalDb = false) { var lockInfo = new WriteLockInfo(); + var ok = true; try { Lock(lockInfo); ClearLocked(_contentNodes); - ClearLocked(_contentRootNodes); + ClearRootLocked(); // do NOT clear types else they are gone! //ClearLocked(_contentTypesById); //ClearLocked(_contentTypesByAlias); - // skip missing parents & unbuildable kits - what else could we do? - foreach (var kit in kits.Where(x => ParentExistsLocked(x) && BuildKit(x))) + foreach (var kit in kits) { + if (!BuildKit(kit)) + { + ok = false; + continue; // skip that one + } + _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddToParentLocked(kit.Node); + + // don't refresh _localDb if we are reading from _localDb + if (!fromLocalDb && _localDb != null) RegisterChange(kit.Node.Id, kit); + AddNodeLocked(kit.Node); _xmap[kit.Node.Uid] = kit.Node.Id; } @@ -544,34 +617,41 @@ namespace Umbraco.Web.PublishedCache.NuCache { Release(lockInfo); } + + return ok; } - // IMPORTANT kits must be sorted out by LEVEL - public void SetBranch(int rootContentId, IEnumerable kits) + // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER + public bool SetBranch(int rootContentId, IEnumerable kits) { var lockInfo = new WriteLockInfo(); + var ok = true; try { Lock(lockInfo); // get existing - _contentNodes.TryGetValue(rootContentId, out LinkedNode link); + _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; // clear if (existing != null) { ClearBranchLocked(existing); - RemoveFromParentLocked(existing); + RemoveNodeLocked(existing); } // now add them all back - // skip missing parents & unbuildable kits - what else could we do? - foreach (var kit in kits.Where(x => ParentExistsLocked(x) && BuildKit(x))) + foreach (var kit in kits) { + if (!BuildKit(kit)) + { + ok = false; + continue; // skip that one + } SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddToParentLocked(kit.Node); + AddNodeLocked(kit.Node); _xmap[kit.Node.Uid] = kit.Node.Id; } @@ -580,6 +660,8 @@ namespace Umbraco.Web.PublishedCache.NuCache { Release(lockInfo); } + + return ok; } public bool Clear(int id) @@ -591,7 +673,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // try to find the content // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out LinkedNode link); // else null + _contentNodes.TryGetValue(id, out var link); // else null if (link?.Value == null) return false; var content = link.Value; @@ -601,7 +683,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ClearBranchLocked(content); // manage the tree - RemoveFromParentLocked(content); + RemoveNodeLocked(content); return true; } @@ -626,13 +708,25 @@ namespace Umbraco.Web.PublishedCache.NuCache _xmap.TryRemove(content.Uid, out _); - foreach (var childId in content.ChildContentIds) + var id = content.FirstChildContentId; + while (id > 0) { - if (_contentNodes.TryGetValue(childId, out LinkedNode link) == false || link.Value == null) continue; + var link = GetLinkedNode(id, "child"); ClearBranchLocked(link.Value); + id = link.Value.NextSiblingContentId; } } + // gets the link node + // throws (panic) if not found, or no value + private LinkedNode GetLinkedNode(int id, string description) + { + if (_contentNodes.TryGetValue(id, out var link) && link.Value != null) + return link; + + throw new Exception($"panic: failed to get {description} with id={id}"); + } + private LinkedNode GetParentLink(ContentNode content) { _contentNodes.TryGetValue(content.ParentContentId, out var link); // else null @@ -641,24 +735,35 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - private void RemoveFromParentLocked(ContentNode content) + private void RemoveNodeLocked(ContentNode content) { - // remove from root content index, - // or parent's children index - if (content.ParentContentId < 0) + var parentLink = content.ParentContentId < 0 + ? _root + : GetLinkedNode(content.ParentContentId, "parent"); + + var parent = parentLink.Value; + + // must have children + if (parent.FirstChildContentId < 0) + throw new Exception("panic: no children"); + + // if first, clone parent + remove first child + if (parent.FirstChildContentId == content.Id) { - SetValueLocked(_contentRootNodes, content.Id, null); + parent = GenCloneLocked(parentLink); + parent.FirstChildContentId = content.NextSiblingContentId; } else { - // obviously parent has to exist - var link = GetParentLink(content); - var parent = link.Value; - if (link.Gen < _liveGen) - parent = parent.CloneParent(_publishedSnapshotAccessor, _umbracoContextAccessor); - parent.ChildContentIds.Remove(content.Id); - if (link.Gen < _liveGen) - SetValueLocked(_contentNodes, parent.Id, parent); + // iterate children until the previous child + var link = GetLinkedNode(parent.FirstChildContentId, "first child"); + + while (link.Value.NextSiblingContentId != content.Id) + link = GetLinkedNode(link.Value.NextSiblingContentId, "next child"); + + // clone the previous child and replace next child + var prevChild = GenCloneLocked(link); + prevChild.NextSiblingContentId = content.NextSiblingContentId; } } @@ -679,28 +784,90 @@ namespace Umbraco.Web.PublishedCache.NuCache return node?.PublishedModel != null; } - private void AddToParentLocked(ContentNode content) + private ContentNode GenCloneLocked(LinkedNode link) { - // add to root content index, - // or parent's children index - if (content.ParentContentId < 0) + var node = link.Value; + + if (node != null && link.Gen < _liveGen) { - // need an object reference... just use this... - SetValueLocked(_contentRootNodes, content.Id, this); + node = new ContentNode(link.Value); + if (link == _root) + SetRootLocked(node); + else + SetValueLocked(_contentNodes, node.Id, node); + } + + return node; + } + + private void AddNodeLocked(ContentNode content) + { + var parentLink = content.ParentContentId < 0 + ? _root + : GetLinkedNode(content.ParentContentId, "parent"); + + var parent = parentLink.Value; + + // if parent has no children, clone parent + add as first child + if (parent.FirstChildContentId < 0) + { + parent = GenCloneLocked(parentLink); + parent.FirstChildContentId = content.Id; + return; + } + + // get parent's first child + var childLink = GetLinkedNode(parent.FirstChildContentId, "first child"); + var child = childLink.Value; + + // if first, clone parent + insert as first child + if (child.SortOrder > content.SortOrder) + { + content.NextSiblingContentId = parent.FirstChildContentId; + parent = GenCloneLocked(parentLink); + parent.FirstChildContentId = content.Id; + return; + } + + // else lookup position + while (child.NextSiblingContentId > 0) + { + // get next child + var nextChildLink = GetLinkedNode(child.NextSiblingContentId, "next child"); + var nextChild = nextChildLink.Value; + + // if here, clone previous + append/insert + if (nextChild.SortOrder > content.SortOrder) + { + content.NextSiblingContentId = nextChild.Id; + child = GenCloneLocked(childLink); + child.NextSiblingContentId = content.Id; + return; + } + + childLink = nextChildLink; + child = nextChild; + } + + // if last, clone previous + append + child = GenCloneLocked(childLink); + child.NextSiblingContentId = content.Id; + } + + // replaces the root node + private void SetRootLocked(ContentNode node) + { + if (_root.Gen != _liveGen) + { + _root = new LinkedNode(node, _liveGen, _root); } else { - // assume parent has been validated and exists - var link = GetParentLink(content); - var parent = link.Value; - if (link.Gen < _liveGen) - parent = parent.CloneParent(_publishedSnapshotAccessor, _umbracoContextAccessor); - parent.ChildContentIds.Add(content.Id); - if (link.Gen < _liveGen) - SetValueLocked(_contentNodes, parent.Id, parent); + _root.Value = node; } } + // set a node (just the node, not the tree) private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) where TValue : class { @@ -764,18 +931,23 @@ namespace Umbraco.Web.PublishedCache.NuCache public IEnumerable GetAtRoot(long gen) { - // look ma, no lock! - foreach (var kvp in _contentRootNodes) + var z = _root; + while (z != null) { - var link = kvp.Value; - while (link != null) - { - if (link.Gen <= gen) - break; - link = link.Next; - } - if (link?.Value != null) - yield return Get(kvp.Key, gen); + if (z.Gen <= gen) + break; + z = z.Next; + } + if (z == null) + yield break; + + var id = z.Value.FirstChildContentId; + + while (id > 0) + { + var link = GetLinkedNode(id, "sibling"); + yield return link.Value; + id = link.Value.NextSiblingContentId; } } @@ -830,12 +1002,12 @@ namespace Umbraco.Web.PublishedCache.NuCache return has == false; } - public PublishedContentType GetContentType(int id, long gen) + public IPublishedContentType GetContentType(int id, long gen) { return GetValue(_contentTypesById, id, gen); } - public PublishedContentType GetContentType(string alias, long gen) + public IPublishedContentType GetContentType(string alias, long gen) { return GetValue(_contentTypesByAlias, alias, gen); } @@ -928,7 +1100,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return _collectTask; // ReSharper disable InconsistentlySynchronizedField - var task = _collectTask = Task.Run(() => Collect()); + var task = _collectTask = Task.Run(Collect); _collectTask.ContinueWith(_ => { lock (_rlocko) @@ -957,11 +1129,19 @@ namespace Umbraco.Web.PublishedCache.NuCache } Collect(_contentNodes); - Collect(_contentRootNodes); + CollectRoot(); Collect(_contentTypesById); Collect(_contentTypesByAlias); } + private void CollectRoot() + { + var link = _root; + while (link.Next != null && link.Next.Gen > _floorGen) + link = link.Next; + link.Next = null; + } + private void Collect(ConcurrentDictionary> dict) where TValue : class { @@ -1151,14 +1331,14 @@ namespace Umbraco.Web.PublishedCache.NuCache return _store.GetAll(_gen); } - public PublishedContentType GetContentType(int id) + public IPublishedContentType GetContentType(int id) { if (_gen < 0) throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); return _store.GetContentType(id, _gen); } - public PublishedContentType GetContentType(string alias) + public IPublishedContentType GetContentType(string alias) { if (_gen < 0) throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index 958f6302fa..4521311302 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using CSharpTest.Net.Serialization; using Umbraco.Core; @@ -14,7 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (pcount == 0) return Empty; // read each variation - var dict = new Dictionary(); + var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); for (var i = 0; i < pcount; i++) { var languageId = PrimitiveSerializer.String.ReadFrom(stream); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 19c8beedb5..aa5dc9eb30 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using CSharpTest.Net.Serialization; using Umbraco.Core; @@ -9,7 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(); + var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 60dcbb989b..cf75b1f346 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -98,6 +98,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) { + if (!ids.Any()) return Enumerable.Empty(); + var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) @@ -169,6 +171,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) { + if (!ids.Any()) return Enumerable.Empty(); + var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs index 323d954980..d4702cf7aa 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDataSource.cs @@ -9,13 +9,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal interface IDataSource { ContentNodeKit GetContentSource(IScope scope, int id); - IEnumerable GetAllContentSources(IScope scope); - IEnumerable GetBranchContentSources(IScope scope, int id); + IEnumerable GetAllContentSources(IScope scope); // must order by level + IEnumerable GetBranchContentSources(IScope scope, int id); // must order by level, sortOrder IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids); ContentNodeKit GetMediaSource(IScope scope, int id); - IEnumerable GetAllMediaSources(IScope scope); - IEnumerable GetBranchMediaSources(IScope scope, int id); + IEnumerable GetAllMediaSources(IScope scope); // must order by level + IEnumerable GetBranchMediaSources(IScope scope, int id); // must order by level, sortOrder IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs index 896a04a0b3..6bc0de7268 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs @@ -4,16 +4,23 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache { + /// + /// Implements for NuCache. + /// internal class DomainCache : IDomainCache { private readonly SnapDictionary.Snapshot _snapshot; + /// + /// Initializes a new instance of the class. + /// public DomainCache(SnapDictionary.Snapshot snapshot, string defaultCulture) { _snapshot = snapshot; - DefaultCulture = defaultCulture; // capture - fast + DefaultCulture = defaultCulture; } + /// public IEnumerable GetAll(bool includeWildcards) { var list = _snapshot.GetAll(); @@ -21,17 +28,23 @@ namespace Umbraco.Web.PublishedCache.NuCache return list; } - public IEnumerable GetAssigned(int contentId, bool includeWildcards) + /// + public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) { // probably this could be optimized with an index // but then we'd need a custom DomainStore of some sort var list = _snapshot.GetAll(); - list = list.Where(x => x.ContentId == contentId); + list = list.Where(x => x.ContentId == documentId); if (includeWildcards == false) list = list.Where(x => x.IsWildcard == false); return list; } + /// + public bool HasAssigned(int documentId, bool includeWildcards = false) + => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + + /// public string DefaultCulture { get; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index 112ccd9931..182086ed7f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -14,17 +14,15 @@ namespace Umbraco.Web.PublishedCache.NuCache internal class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; - private readonly IAppCache _snapshotCache; - private readonly IAppCache _elementsCache; + private readonly IVariationContextAccessor _variationContextAccessor; #region Constructors - public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IAppCache snapshotCache, IAppCache elementsCache) + public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor) : base(previewDefault) { _snapshot = snapshot; - _snapshotCache = snapshotCache; - _elementsCache = elementsCache; + _variationContextAccessor = variationContextAccessor; } #endregion @@ -65,30 +63,16 @@ namespace Umbraco.Web.PublishedCache.NuCache return n != null; } - public override IEnumerable GetAtRoot(bool preview) + IEnumerable INavigableData.GetAtRoot(bool preview) => GetAtRoot(preview); + + public override IEnumerable GetAtRoot(bool preview, string culture = null) { - if (PublishedSnapshotService.CacheContentCacheRoots == false) - return GetAtRootNoCache(); + // handle context culture for variant + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; - var cache = preview == false || PublishedSnapshotService.FullCacheWhenPreviewing - ? _elementsCache - : _snapshotCache; - - if (cache == null) - return GetAtRootNoCache(); - - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get( - CacheKeys.MediaCacheRoots(false), // ignore preview, only 1 key! - () => GetAtRootNoCache().ToArray()); - } - - private IEnumerable GetAtRootNoCache() - { - var c = _snapshot.GetAtRoot(); - - // ignore preview, there's only draft for media - return c.Select(n => n.PublishedModel); + var atRoot = _snapshot.GetAtRoot().Select(x => x.PublishedModel); + return culture == "*" ? atRoot : atRoot.Where(x => x.IsInvariantOrHasCulture(culture)); } public override bool HasContent(bool preview) @@ -171,12 +155,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override PublishedContentType GetContentType(int id) + public override IPublishedContentType GetContentType(int id) { return _snapshot.GetContentType(id); } - public override PublishedContentType GetContentType(string alias) + public override IPublishedContentType GetContentType(string alias) { return _snapshot.GetContentType(alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs index f7ffe73109..2e196f629e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs @@ -8,7 +8,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; using Umbraco.Core.Xml.XPath; using Umbraco.Web.PublishedCache.NuCache.Navigable; @@ -19,7 +18,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; public readonly IVariationContextAccessor VariationContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IEntityXmlSerializer _entitySerializer; private readonly IAppCache _snapshotCache; private readonly IMemberService _memberService; @@ -27,12 +25,11 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly bool _previewDefault; public MemberCache(bool previewDefault, IAppCache snapshotCache, IMemberService memberService, PublishedContentTypeCache contentTypeCache, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor, IEntityXmlSerializer entitySerializer) + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IEntityXmlSerializer entitySerializer) { _snapshotCache = snapshotCache; _publishedSnapshotAccessor = publishedSnapshotAccessor; VariationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; _entitySerializer = entitySerializer; _memberService = memberService; _previewDefault = previewDefault; @@ -68,14 +65,14 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = _memberService.GetById(memberId); return member == null ? null - : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor); + : PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); }); } private IPublishedContent /*IPublishedMember*/ GetById(IMember member, bool previewing) { return GetCacheItem(CacheKeys.MemberCacheMember("ById", _previewDefault, member.Id), () => - PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor)); + PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing, _publishedSnapshotAccessor, VariationContextAccessor)); } public IPublishedContent /*IPublishedMember*/ GetByProviderKey(object key) @@ -110,7 +107,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public IPublishedContent /*IPublishedMember*/ GetByMember(IMember member) { - return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor); + return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault, _publishedSnapshotAccessor, VariationContextAccessor); } public IEnumerable GetAtRoot(bool preview) @@ -118,7 +115,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // because members are flat (not a tree) everything is at root // because we're loading everything... let's just not cache? var members = _memberService.GetAllMembers(); - return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor)); + return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview, _publishedSnapshotAccessor, VariationContextAccessor)); } public XPathNavigator CreateNavigator() @@ -151,12 +148,12 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public PublishedContentType GetContentType(int id) + public IPublishedContentType GetContentType(int id) { return _contentTypeCache.Get(PublishedItemType.Member, id); } - public PublishedContentType GetContentType(string alias) + public IPublishedContentType GetContentType(string alias) { return _contentTypeCache.Get(PublishedItemType.Member, alias); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs index 18bf3ead13..310dae9dd2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Navigable/NavigableContentType.cs @@ -22,10 +22,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable // changes, but they are replaced by a new instance, so our map here will clean itself automatically and // we don't have to manage cache - ConditionalWeakTable does not prevent keys from being GCed - private static readonly ConditionalWeakTable TypesMap - = new ConditionalWeakTable(); + private static readonly ConditionalWeakTable TypesMap + = new ConditionalWeakTable(); - public static NavigableContentType GetContentType(PublishedContentType contentType) + public static NavigableContentType GetContentType(IPublishedContentType contentType) { return TypesMap.GetOrCreateValue(contentType).EnsureInitialized(contentType); } @@ -49,7 +49,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.Navigable }; } - private NavigableContentType EnsureInitialized(PublishedContentType contentType) + private NavigableContentType EnsureInitialized(IPublishedContentType contentType) { lock (_locko) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 2c8bc94d90..0254b815c1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -38,12 +38,12 @@ namespace Umbraco.Web.PublishedCache.NuCache private string _valuesCacheKey; // initializes a published content property with no value - public Property(PublishedPropertyType propertyType, PublishedContent content, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) + public Property(IPublishedPropertyType propertyType, PublishedContent content, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) : this(propertyType, content, null, publishedSnapshotAccessor, referenceCacheLevel) { } // initializes a published content property with a value - public Property(PublishedPropertyType propertyType, PublishedContent content, PropertyData[] sourceValues, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) + public Property(IPublishedPropertyType propertyType, PublishedContent content, PropertyData[] sourceValues, IPublishedSnapshotAccessor publishedSnapshotAccessor, PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) : base(propertyType, referenceCacheLevel) { if (sourceValues != null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 057823f29c..cb7acdc164 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -22,9 +22,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) { _contentNode = contentNode ?? throw new ArgumentNullException(nameof(contentNode)); ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); @@ -67,12 +65,8 @@ namespace Umbraco.Web.PublishedCache.NuCache return user?.Name; } - // (see ContentNode.CloneParent) - public PublishedContent( - ContentNode contentNode, - PublishedContent origin, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + // used when cloning in ContentNode + public PublishedContent(ContentNode contentNode, PublishedContent origin) { _contentNode = contentNode; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; @@ -89,10 +83,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } // clone for previewing as draft a published content that is published and has no draft - private PublishedContent( - PublishedContent origin, - IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor) + private PublishedContent(PublishedContent origin) { _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; VariationContextAccessor = origin.VariationContextAccessor; @@ -119,45 +110,16 @@ namespace Umbraco.Web.PublishedCache.NuCache internal static Func GetMediaByIdFunc { get; set; } = (publishedShapshot, previewing, id) => publishedShapshot.Media.GetById(previewing, id); - private IPublishedContent GetContentById(bool previewing, int id) + private Func GetGetterById() { - return GetContentByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); - } - - private IEnumerable GetContentByIds(bool previewing, IEnumerable ids) - { - var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; - - // beware! the loop below CANNOT be converted to query such as: - //return ids.Select(x => _getContentByIdFunc(publishedSnapshot, previewing, x)).Where(x => x != null); - // because it would capture the published snapshot and cause all sorts of issues - // - // we WANT to get the actual current published snapshot each time we run - - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var id in ids) + switch (ContentType.ItemType) { - var content = GetContentByIdFunc(publishedSnapshot, previewing, id); - if (content != null) yield return content; - } - } - - private IPublishedContent GetMediaById(bool previewing, int id) - { - return GetMediaByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); - } - - private IEnumerable GetMediaByIds(bool previewing, IEnumerable ids) - { - var publishedShapshot = _publishedSnapshotAccessor.PublishedSnapshot; - - // see note above for content - - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var id in ids) - { - var content = GetMediaByIdFunc(publishedShapshot, previewing, id); - if (content != null) yield return content; + case PublishedItemType.Content: + return GetContentByIdFunc; + case PublishedItemType.Media: + return GetMediaByIdFunc; + default: + throw new Exception("panic: invalid item type"); } } @@ -166,7 +128,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content Type /// - public override PublishedContentType ContentType => _contentNode.ContentType; + public override IPublishedContentType ContentType => _contentNode.ContentType; #endregion @@ -184,38 +146,6 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public override int Id => _contentNode.Id; - /// - public override string Name - { - get - { - if (!ContentType.VariesByCulture()) - return ContentData.Name; - - var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - if (culture == "") - return ContentData.Name; - - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.Name : null; - } - } - - /// - public override string UrlSegment - { - get - { - if (!ContentType.VariesByCulture()) - return _urlSegment; - - var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - if (culture == "") - return _urlSegment; - - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos.UrlSegment : null; - } - } - /// public override int SortOrder => _contentNode.SortOrder; @@ -246,38 +176,27 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public override DateTime UpdateDate => ContentData.VersionDate; - private IReadOnlyDictionary _cultureInfos; - - private static readonly IReadOnlyDictionary NoCultureInfos = new Dictionary(); - - /// - public override PublishedCultureInfo GetCulture(string culture = null) - { - // handle context culture - if (culture == null) - culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; - - // no invariant culture infos - if (culture == "") return null; - - // get - return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; - } + // ReSharper disable once CollectionNeverUpdated.Local + private static readonly IReadOnlyDictionary EmptyCultures = new Dictionary(); + private IReadOnlyDictionary _cultures; /// public override IReadOnlyDictionary Cultures { get { - if (!ContentType.VariesByCulture()) - return NoCultureInfos; + if (_cultures != null) return _cultures; - if (_cultureInfos != null) return _cultureInfos; + if (!ContentType.VariesByCulture()) + return _cultures = new Dictionary + { + { "", new PublishedCultureInfo("", ContentData.Name, _urlSegment, CreateDate) } + }; if (ContentData.CultureInfos == null) - throw new Exception("oops: _contentDate.CultureInfos is null."); + throw new Exception("panic: _contentDate.CultureInfos is null."); - return _cultureInfos = ContentData.CultureInfos + return _cultures = ContentData.CultureInfos .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date), StringComparer.OrdinalIgnoreCase); } } @@ -339,61 +258,48 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - // have to use the "current" cache because a PublishedContent can be shared - // amongst many snapshots and other content depend on the snapshots - switch (_contentNode.ContentType.ItemType) - { - case PublishedItemType.Content: - return GetContentById(IsPreviewing, _contentNode.ParentContentId); - case PublishedItemType.Media: - return GetMediaById(IsPreviewing, _contentNode.ParentContentId); - default: - throw new Exception($"Panic: unsupported item type \"{_contentNode.ContentType.ItemType}\"."); - } + var getById = GetGetterById(); + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; + return getById(publishedSnapshot, IsPreviewing, ParentId); } } /// - public override IEnumerable Children + public override IEnumerable ChildrenForAllCultures { get { - var cache = GetAppropriateCache(); - if (cache == null || PublishedSnapshotService.CachePublishedContentChildren == false) - return GetChildren(); + var getById = GetGetterById(); + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; + var id = _contentNode.FirstChildContentId; - // note: ToArray is important here, we want to cache the result, not the function! - return (IEnumerable)cache.Get(ChildrenCacheKey, () => GetChildren().ToArray()); + while (id > 0) + { + // is IsPreviewing is false, then this can return null + var content = getById(publishedSnapshot, IsPreviewing, id); + + if (content != null) + { + yield return content; + } + else + { + // but if IsPreviewing is true, we should have a child + if (IsPreviewing) + throw new Exception($"panic: failed to get content with id={id}"); + + // if IsPreviewing is false, get the unpublished child nevertheless + // we need it to keep enumerating children! but we don't return it + content = getById(publishedSnapshot, true, id); + if (content == null) + throw new Exception($"panic: failed to get content with id={id}"); + } + + id = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + } } } - private string _childrenCacheKey; - - private string ChildrenCacheKey => _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, IsPreviewing)); - - private IEnumerable GetChildren() - { - IEnumerable c; - switch (_contentNode.ContentType.ItemType) - { - case PublishedItemType.Content: - c = GetContentByIds(IsPreviewing, _contentNode.ChildContentIds); - break; - case PublishedItemType.Media: - c = GetMediaByIds(IsPreviewing, _contentNode.ChildContentIds); - break; - default: - throw new Exception("oops"); - } - - return c.OrderBy(x => x.SortOrder); - - // notes: - // _contentNode.ChildContentIds is an unordered int[] - // needs to fetch & sort - do it only once, lazily, though - // Q: perfs-wise, is it better than having the store managed an ordered list - } - #endregion #region Properties @@ -423,7 +329,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; var cache = publishedSnapshot == null ? null - : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ItemType != PublishedItemType.Member) + : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ContentType.ItemType != PublishedItemType.Member) ? publishedSnapshot.ElementsCache : publishedSnapshot.SnapshotCache); return cache; @@ -451,7 +357,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // used by navigable content // includes all children, published or unpublished // NavigableNavigator takes care of selecting those it wants - internal IList ChildIds => _contentNode.ChildContentIds; + // note: this is not efficient - we do not try to be (would require a double-linked list) + internal IList ChildIds => Children.Select(x => x.Id).ToList(); // used by Property // gets a value indicating whether the content or media exists in @@ -470,18 +377,16 @@ namespace Umbraco.Web.PublishedCache.NuCache return this; var cache = GetAppropriateCache(); - if (cache == null) return new PublishedContent(this, UmbracoContextAccessor).CreateModel(); - return (IPublishedContent)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this, UmbracoContextAccessor).CreateModel()); + if (cache == null) return new PublishedContent(this).CreateModel(); + return (IPublishedContent)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this).CreateModel()); } // used by Navigable.Source,... internal static PublishedContent UnwrapIPublishedContent(IPublishedContent content) { - PublishedContentWrapped wrapped; - while ((wrapped = content as PublishedContentWrapped) != null) + while (content is PublishedContentWrapped wrapped) content = wrapped.Unwrap(); - var inner = content as PublishedContent; - if (inner == null) + if (!(content is PublishedContent inner)) throw new InvalidOperationException("Innermost content is not PublishedContent."); return inner; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs index 11ca169300..dc608fe391 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs @@ -20,21 +20,18 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor - ) - : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) + : base(contentNode, contentData, publishedSnapshotAccessor, variationContextAccessor) { _member = member; } public static IPublishedContent Create( IMember member, - PublishedContentType contentType, + IPublishedContentType contentType, bool previewing, IPublishedSnapshotAccessor publishedSnapshotAccessor, - IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor) + IVariationContextAccessor variationContextAccessor) { var d = new ContentData { @@ -50,10 +47,10 @@ namespace Umbraco.Web.PublishedCache.NuCache member.Level, member.Path, member.SortOrder, member.ParentId, member.CreateDate, member.CreatorId); - return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor).CreateModel(); + return new PublishedMember(member, n, d, publishedSnapshotAccessor, variationContextAccessor).CreateModel(); } - private static Dictionary GetPropertyValues(PublishedContentType contentType, IMember member) + private static Dictionary GetPropertyValues(IPublishedContentType contentType, IMember member) { // see node in PublishedSnapshotService // we do not (want to) support ConvertDbToXml/String @@ -91,7 +88,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return properties; } - private static void AddIf(PublishedContentType contentType, IDictionary properties, string alias, object value) + private static void AddIf(IPublishedContentType contentType, IDictionary properties, string alias, object value) { var propertyType = contentType.GetPropertyType(alias); if (propertyType == null || propertyType.IsUserProperty) return; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 073428336d..dad9811af8 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -4,13 +4,11 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Web; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -36,7 +34,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IScopeProvider _scopeProvider; private readonly IDataSource _dataSource; private readonly ILogger _logger; @@ -44,8 +41,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IMediaRepository _mediaRepository; private readonly IMemberRepository _memberRepository; private readonly IGlobalSettings _globalSettings; - private readonly ISiteDomainHelper _siteDomainHelper; private readonly IEntityXmlSerializer _entitySerializer; + private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; @@ -67,30 +64,18 @@ namespace Umbraco.Web.PublishedCache.NuCache // so making it configurable. public static readonly bool FullCacheWhenPreviewing = true; - // define constant - determines whether to cache the published content - // objects (in the elements cache, or snapshot cache, depending on preview) - // or to re-fetch them all the time. caching is faster but uses more - // memory. not sure what we want. - public static readonly bool CachePublishedContentChildren = true; - - // define constant - determines whether to cache the content cache root - // objects (in the elements cache, or snapshot cache, depending on preview) - // or to re-fetch them all the time. caching is faster but uses more - // memory - not sure what we want. - public static readonly bool CacheContentCacheRoots = true; - #region Constructors //private static int _singletonCheck; public PublishedSnapshotService(Options options, IMainDom mainDom, IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IScopeProvider scopeProvider, + IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, ILogger logger, IScopeProvider scopeProvider, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, IDefaultCultureAccessor defaultCultureAccessor, - IDataSource dataSource, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper, - IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, + IDataSource dataSource, IGlobalSettings globalSettings, + IEntityXmlSerializer entitySerializer, + IPublishedModelFactory publishedModelFactory, UrlSegmentProviderCollection urlSegmentProviders) : base(publishedSnapshotAccessor, variationContextAccessor) { @@ -99,7 +84,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; - _umbracoContextAccessor = umbracoContextAccessor; _dataSource = dataSource; _logger = logger; _scopeProvider = scopeProvider; @@ -108,12 +92,12 @@ namespace Umbraco.Web.PublishedCache.NuCache _memberRepository = memberRepository; _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings; - _siteDomainHelper = siteDomainHelper; _urlSegmentProviders = urlSegmentProviders; // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member _entitySerializer = entitySerializer; + _publishedModelFactory = publishedModelFactory; // we always want to handle repository events, configured or not // assuming no repository event will trigger before the whole db is ready @@ -158,13 +142,13 @@ namespace Umbraco.Web.PublishedCache.NuCache // stores are created with a db so they can write to it, but they do not read from it, // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to // figure out whether it can read the databases or it should populate them from sql - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localContentDb); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localMediaDb); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); } else { - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger); + _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); + _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); } _domainStore = new SnapDictionary(); @@ -189,16 +173,24 @@ namespace Umbraco.Web.PublishedCache.NuCache try { + var okContent = false; + var okMedia = false; + if (_localDbExists) { - LockAndLoadContent(LoadContentFromLocalDbLocked); - LockAndLoadMedia(LoadMediaFromLocalDbLocked); + okContent = LockAndLoadContent(LoadContentFromLocalDbLocked); + if (!okContent) + _logger.Warn("Loading content from local db raised warnings, will reload from database."); + okMedia = LockAndLoadMedia(LoadMediaFromLocalDbLocked); + if (!okMedia) + _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - else - { + + if (!okContent) LockAndLoadContent(LoadContentFromDatabaseLocked); + + if (!okMedia) LockAndLoadMedia(LoadMediaFromDatabaseLocked); - } LockAndLoadDomains(); } @@ -342,6 +334,21 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private bool LockAndLoadContent(Func action) + { + // first get a writer, then a scope + // if there already is a scope, the writer will attach to it + // otherwise, it will only exist here - cheap + using (_contentStore.GetScopedWriteLock(_scopeProvider)) + using (var scope = _scopeProvider.CreateScope()) + { + scope.ReadLock(Constants.Locks.ContentTree); + var ok = action(scope); + scope.Complete(); + return ok; + } + } + private void LoadContentFromDatabaseLocked(IScope scope) { // locks: @@ -350,7 +357,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.UpdateContentTypes(null, contentTypes, null); + _contentStore.SetAllContentTypes(contentTypes); + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! _localContentDb?.Clear(); @@ -363,19 +373,23 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loaded content from database ({Duration}ms)", sw.ElapsedMilliseconds); } - private void LoadContentFromLocalDbLocked(IScope scope) + private bool LoadContentFromLocalDbLocked(IScope scope) { var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.UpdateContentTypes(null, contentTypes, null); + _contentStore.SetAllContentTypes(contentTypes); + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! _logger.Debug("Loading content from local db..."); var sw = Stopwatch.StartNew(); var kits = _localContentDb.Select(x => x.Value) .OrderBy(x => x.Node.Level); // IMPORTANT sort by level - _contentStore.SetAll(kits); + var ok = _contentStore.SetAll(kits, true); sw.Stop(); _logger.Debug("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds); + return ok; } // keep these around - might be useful @@ -412,13 +426,29 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private bool LockAndLoadMedia(Func action) + { + // see note in LockAndLoadContent + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) + using (var scope = _scopeProvider.CreateScope()) + { + scope.ReadLock(Constants.Locks.MediaTree); + var ok = action(scope); + scope.Complete(); + return ok; + } + } + private void LoadMediaFromDatabaseLocked(IScope scope) { // locks & notes: see content var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.UpdateContentTypes(null, mediaTypes, null); + _mediaStore.SetAllContentTypes(mediaTypes); + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! _localMediaDb?.Clear(); @@ -431,19 +461,23 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loaded media from database ({Duration}ms)", sw.ElapsedMilliseconds); } - private void LoadMediaFromLocalDbLocked(IScope scope) + private bool LoadMediaFromLocalDbLocked(IScope scope) { var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.UpdateContentTypes(null, mediaTypes, null); + _mediaStore.SetAllContentTypes(mediaTypes); + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! _logger.Debug("Loading media from local db..."); var sw = Stopwatch.StartNew(); var kits = _localMediaDb.Select(x => x.Value) .OrderBy(x => x.Node.Level); // IMPORTANT sort by level - _mediaStore.SetAll(kits); + var ok = _mediaStore.SetAll(kits, true); sw.Stop(); _logger.Debug("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds); + return ok; } // keep these around - might be useful @@ -651,7 +685,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level + // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchContentSources(scope, capture.Id); _contentStore.SetBranch(capture.Id, kits); } @@ -677,6 +711,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) { // no cache, trash everything @@ -743,7 +778,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? - // IMPORTANT GetbranchContentSources sorts kits by level + // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); _mediaStore.SetBranch(capture.Id, kits); } @@ -769,6 +804,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) { // no cache, nothing we can do @@ -781,33 +817,49 @@ namespace Umbraco.Web.PublishedCache.NuCache Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); + if (_publishedModelFactory.IsLiveFactory()) + { + //In the case of Pure Live - we actually need to refresh all of the content and the media + //see https://github.com/umbraco/Umbraco-CMS/issues/5671 + //The underlying issue is that in Pure Live the ILivePublishedModelFactory will re-compile all of the classes/models + //into a new DLL for the application which includes both content types and media types. + //Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated + //to use the newest version of the class. + using (_contentStore.GetScopedWriteLock(_scopeProvider)) + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out var draftChanged, out var publishedChanged); + NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out var anythingChanged); + } + } + ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } - private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, IEnumerable, IEnumerable, IEnumerable> action) + private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, List, List, List> action) + where T : IContentTypeComposition { + if (payloads.Length == 0) return; //nothing to do + var nameOfT = typeof(T).Name; - var removedIds = new List(); - var refreshedIds = new List(); - var otherIds = new List(); - var newIds = new List(); + List removedIds = null, refreshedIds = null, otherIds = null, newIds = null; foreach (var payload in payloads) { if (payload.ItemType != nameOfT) continue; if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove)) - removedIds.Add(payload.Id); + AddToList(ref removedIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) - refreshedIds.Add(payload.Id); + AddToList(ref refreshedIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther)) - otherIds.Add(payload.Id); + AddToList(ref otherIds, payload.Id); else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Create)) - newIds.Add(payload.Id); + AddToList(ref newIds, payload.Id); } - if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return; + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) return; using (store.GetScopedWriteLock(_scopeProvider)) { @@ -894,15 +946,19 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + //Methods used to prevent allocations of lists + private void AddToList(ref List list, int val) => GetOrCreateList(ref list).Add(val); + private List GetOrCreateList(ref List list) => list ?? (list = new List()); + #endregion #region Content Types - private IEnumerable CreateContentTypes(PublishedItemType itemType, int[] ids) + private IReadOnlyCollection CreateContentTypes(PublishedItemType itemType, int[] ids) { // XxxTypeService.GetAll(empty) returns everything! if (ids.Length == 0) - return Enumerable.Empty(); + return Array.Empty(); IEnumerable contentTypes; switch (itemType) @@ -922,10 +978,10 @@ namespace Umbraco.Web.PublishedCache.NuCache // some may be missing - not checking here - return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x)); + return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x)).ToList(); } - private PublishedContentType CreateContentType(PublishedItemType itemType, int id) + private IPublishedContentType CreateContentType(PublishedItemType itemType, int id) { IContentTypeComposition contentType; switch (itemType) @@ -946,44 +1002,58 @@ namespace Umbraco.Web.PublishedCache.NuCache return contentType == null ? null : _publishedContentTypeFactory.CreateContentType(contentType); } - private void RefreshContentTypesLocked(IEnumerable removedIds, IEnumerable refreshedIds, IEnumerable otherIds, IEnumerable newIds) + private void RefreshContentTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + return; + // locks: // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - var refreshedIdsA = refreshedIds.ToArray(); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTypes); - var typesA = CreateContentTypes(PublishedItemType.Content, refreshedIdsA).ToArray(); - var kits = _dataSource.GetTypeContentSources(scope, refreshedIdsA); + var typesA = refreshedIds.IsCollectionEmpty() + ? Array.Empty() + : CreateContentTypes(PublishedItemType.Content, refreshedIds.ToArray()).ToArray(); + + var kits = refreshedIds.IsCollectionEmpty() + ? Array.Empty() + : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); _contentStore.UpdateContentTypes(removedIds, typesA, kits); - _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray()).ToArray()); - _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray()).ToArray()); + if (!otherIds.IsCollectionEmpty()) + _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + if (!newIds.IsCollectionEmpty()) + _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); scope.Complete(); } } - private void RefreshMediaTypesLocked(IEnumerable removedIds, IEnumerable refreshedIds, IEnumerable otherIds, IEnumerable newIds) + private void RefreshMediaTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + return; + // locks: // media (and content types) are read-locked while reading media // mediaStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - var refreshedIdsA = refreshedIds.ToArray(); - using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTypes); - var typesA = CreateContentTypes(PublishedItemType.Media, refreshedIdsA).ToArray(); - var kits = _dataSource.GetTypeMediaSources(scope, refreshedIdsA); + var typesA = refreshedIds == null + ? Array.Empty() + : CreateContentTypes(PublishedItemType.Media, refreshedIds.ToArray()).ToArray(); + + var kits = refreshedIds == null + ? Array.Empty() + : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); _mediaStore.UpdateContentTypes(removedIds, typesA, kits); _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); @@ -1074,13 +1144,12 @@ namespace Umbraco.Web.PublishedCache.NuCache var defaultCulture = _defaultCultureAccessor.DefaultCulture; var domainCache = new DomainCache(domainSnap, defaultCulture); - var domainHelper = new DomainHelper(domainCache, _siteDomainHelper); return new PublishedSnapshot.PublishedSnapshotElements { - ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainHelper, _globalSettings, _serviceContext.LocalizationService), - MediaCache = new MediaCache(previewDefault, mediaSnap, snapshotCache, elementsCache), - MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor, _entitySerializer), + ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainCache, _globalSettings, VariationContextAccessor), + MediaCache = new MediaCache(previewDefault, mediaSnap, VariationContextAccessor), + MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor, _entitySerializer), DomainCache = domainCache, SnapshotCache = snapshotCache, ElementsCache = elementsCache diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs index 20d7e7ddcd..78e0ec8d33 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -17,4 +17,4 @@ public volatile TValue Value; public volatile LinkedNode Next; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index b88ae26704..1f637663e5 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -37,11 +37,11 @@ namespace Umbraco.Web.PublishedCache public bool HasById(int contentId) => HasById(PreviewDefault, contentId); - public abstract IEnumerable GetAtRoot(bool preview); + public abstract IEnumerable GetAtRoot(bool preview, string culture = null); - public IEnumerable GetAtRoot() + public IEnumerable GetAtRoot(string culture = null) { - return GetAtRoot(PreviewDefault); + return GetAtRoot(PreviewDefault, culture); } public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars); @@ -88,11 +88,11 @@ namespace Umbraco.Web.PublishedCache return HasContent(PreviewDefault); } - public abstract PublishedContentType GetContentType(int id); + public abstract IPublishedContentType GetContentType(int id); - public abstract PublishedContentType GetContentType(string alias); + public abstract IPublishedContentType GetContentType(string alias); - public virtual IEnumerable GetByContentType(PublishedContentType contentType) + public virtual IEnumerable GetByContentType(IPublishedContentType contentType) { // this is probably not super-efficient, but works // some cache implementation may want to override it, though diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs index ca30370598..e453471bb8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -15,8 +15,8 @@ namespace Umbraco.Web.PublishedCache /// This cache is not snapshotted, so it refreshes any time things change. public class PublishedContentTypeCache { - private readonly Dictionary _typesByAlias = new Dictionary(); - private readonly Dictionary _typesById = new Dictionary(); + private readonly Dictionary _typesByAlias = new Dictionary(); + private readonly Dictionary _typesById = new Dictionary(); private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; @@ -136,7 +136,7 @@ namespace Umbraco.Web.PublishedCache /// An item type. /// An alias. /// The published content type corresponding to the item type and alias. - public PublishedContentType Get(PublishedItemType itemType, string alias) + public IPublishedContentType Get(PublishedItemType itemType, string alias) { var aliasKey = GetAliasKey(itemType, alias); @@ -174,7 +174,7 @@ namespace Umbraco.Web.PublishedCache /// An item type. /// An identifier. /// The published content type corresponding to the item type and identifier. - public PublishedContentType Get(PublishedItemType itemType, int id) + public IPublishedContentType Get(PublishedItemType itemType, int id) { try { @@ -204,7 +204,7 @@ namespace Umbraco.Web.PublishedCache } } - private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) { if (GetPublishedContentTypeByAlias != null) return GetPublishedContentTypeByAlias(alias); @@ -231,7 +231,7 @@ namespace Umbraco.Web.PublishedCache return _publishedContentTypeFactory.CreateContentType(contentType); } - private PublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id) + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, int id) { if (GetPublishedContentTypeById != null) return GetPublishedContentTypeById(id); @@ -259,8 +259,8 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously - private Func _getPublishedContentTypeByAlias; - internal Func GetPublishedContentTypeByAlias + private Func _getPublishedContentTypeByAlias; + internal Func GetPublishedContentTypeByAlias { get => _getPublishedContentTypeByAlias; set @@ -282,8 +282,8 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously - private Func _getPublishedContentTypeById; - internal Func GetPublishedContentTypeById + private Func _getPublishedContentTypeById; + internal Func GetPublishedContentTypeById { get => _getPublishedContentTypeById; set @@ -326,7 +326,7 @@ namespace Umbraco.Web.PublishedCache return k + ":" + alias; } - private static string GetAliasKey(PublishedContentType contentType) + private static string GetAliasKey(IPublishedContentType contentType) { return GetAliasKey(contentType.ItemType, contentType.Alias); } diff --git a/src/Umbraco.Web/PublishedCache/PublishedElement.cs b/src/Umbraco.Web/PublishedCache/PublishedElement.cs index 41902e3e26..618c075b9b 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElement.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElement.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PublishedCache { // initializes a new instance of the PublishedElement class // within the context of a published snapshot service (eg a published content property value) - public PublishedElement(PublishedContentType contentType, Guid key, Dictionary values, bool previewing, + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing, PropertyCacheLevel referenceCacheLevel, IPublishedSnapshotAccessor publishedSnapshotAccessor) { if (key == Guid.Empty) throw new ArgumentException("Empty guid."); @@ -46,7 +46,7 @@ namespace Umbraco.Web.PublishedCache // + using an initial reference cache level of .None ensures that everything will be // cached at .Content level - and that reference cache level will propagate to all // properties - public PublishedElement(PublishedContentType contentType, Guid key, Dictionary values, bool previewing) + public PublishedElement(IPublishedContentType contentType, Guid key, Dictionary values, bool previewing) : this(contentType, key, values, previewing, PropertyCacheLevel.None, null) { } @@ -60,7 +60,7 @@ namespace Umbraco.Web.PublishedCache #region ContentType - public PublishedContentType ContentType { get; } + public IPublishedContentType ContentType { get; } #endregion diff --git a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs index 62f72a27aa..ef62a3254d 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.PublishedCache // so making it configurable. private const bool FullCacheWhenPreviewing = true; - public PublishedElementPropertyBase(PublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null, IPublishedSnapshotAccessor publishedSnapshotAccessor = null) + public PublishedElementPropertyBase(IPublishedPropertyType propertyType, IPublishedElement element, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null, IPublishedSnapshotAccessor publishedSnapshotAccessor = null) : base(propertyType, referenceCacheLevel) { _sourceValue = sourceValue; diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 419c279d46..6e9ec61c62 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -17,13 +17,11 @@ namespace Umbraco.Web.PublishedCache private readonly IMember _member; private readonly IMembershipUser _membershipUser; private readonly IPublishedProperty[] _properties; - private readonly PublishedContentType _publishedMemberType; + private readonly IPublishedContentType _publishedMemberType; public PublishedMember( IMember member, - PublishedContentType publishedMemberType, - IUmbracoContextAccessor umbracoContextAccessor) - :base(umbracoContextAccessor) + IPublishedContentType publishedMemberType) { _member = member ?? throw new ArgumentNullException(nameof(member)); _membershipUser = member; @@ -35,16 +33,13 @@ namespace Umbraco.Web.PublishedCache // if they are not part of the member type properties - in which case they are created as // simple raw properties - which are completely invariant - var _properties = PublishedProperty.MapProperties(_publishedMemberType.PropertyTypes, _member.Properties, - (t, v) => new RawValueProperty(t, this, v ?? string.Empty)); - var properties = new List(); foreach (var propertyType in _publishedMemberType.PropertyTypes) { var property = _member.Properties[propertyType.Alias]; if (property == null) continue; - //properties.Add(new FooProperty(propertyType, this, property.Values)); + properties.Add(new RawValueProperty(propertyType, this, property.GetValue())); } EnsureMemberProperties(properties); _properties = properties.ToArray(); @@ -88,6 +83,8 @@ namespace Umbraco.Web.PublishedCache public override IEnumerable Children => Enumerable.Empty(); + public override IEnumerable ChildrenForAllCultures => Enumerable.Empty(); + public override IEnumerable Properties => _properties; public override IPublishedProperty GetProperty(string alias) @@ -126,7 +123,7 @@ namespace Umbraco.Web.PublishedCache properties.Add(property); } - public override PublishedContentType ContentType => _publishedMemberType; + public override IPublishedContentType ContentType => _publishedMemberType; public override int Id => _member.Id; @@ -138,8 +135,6 @@ namespace Umbraco.Web.PublishedCache public override string Name => _member.Name; - public override PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); - public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); public override string UrlSegment => throw new NotSupportedException(); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 6ac21e047b..d7283a1e90 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; namespace Umbraco.Web { @@ -20,137 +21,12 @@ namespace Umbraco.Web public static class PublishedContentExtensions { // see notes in PublishedElementExtensions + // (yes, this is not pretty, but works for now) // private static IPublishedValueFallback PublishedValueFallback => Current.PublishedValueFallback; private static IPublishedSnapshot PublishedSnapshot => Current.PublishedSnapshot; - - #region Urls - - /// - /// Gets the url for the content. - /// - /// The content. - /// The url for the content. - /// Better use the Url property but that method is here to complement UrlAbsolute(). - public static string Url(this IPublishedContent content) - { - return content.Url; - } - - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content) => content.UrlAbsolute(null); - - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The culture to get the url for - /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content, string culture) - { - // adapted from PublishedContentBase.Url - - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - switch (content.ItemType) - { - case PublishedItemType.Content: - return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true, culture); - case PublishedItemType.Media: - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, Constants.Conventions.Media.File, true); - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Gets the url for the media. - /// - /// The content. - /// The property alias to resolve the url from. - /// The variation language. - /// The url for the content. - /// Better use the GetMediaUrl method but that method is here to complement MediaUrlAbsolute(). - public static string MediaUrl(this IPublishedContent content, string propertyAlias, string culture = null) - { - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, culture); - } - - /// - /// Gets the absolute url for the media. - /// - /// The content. - /// The property alias to resolve the url from. - /// The variation language. - /// The absolute url for the media. - public static string MediaUrlAbsolute(this IPublishedContent content, string propertyAlias, string culture = null) - { - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - - return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, true, culture); - } - - /// - /// Gets the Url segment. - /// - /// - /// Gets the url segment for the document, taking its content type and a specified - /// culture in account. For invariant content types, the culture is ignored, else it is - /// used to try and find the segment corresponding to the culture. May return null. - /// - public static string GetUrlSegment(this IPublishedContent content, string culture = null) - { - // for invariant content, return the invariant url segment - if (!content.ContentType.VariesByCulture()) - return content.UrlSegment; - - // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' - // is null (meaning, 'current') - and can return 'null' if that culture is not published - and - // will return 'null' if the content is variant and culture is invariant - - // else try and get the culture info - // return the corresponding url segment, or null if none - var cultureInfo = content.GetCulture(culture); - return cultureInfo?.UrlSegment; - } - - public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) - { - if (Current.Configs.Settings().WebRouting.DisableAlternativeTemplates) - return content.TemplateId == templateId; - - if (content.TemplateId == templateId || !Current.Configs.Settings().WebRouting.ValidateAlternativeTemplates) - return true; - - var publishedContentContentType = Current.Services.ContentTypeService.Get(content.ContentType.Id); - if (publishedContentContentType == null) - throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); - - return publishedContentContentType.IsAllowedTemplate(templateId); - - } - public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) - { - var template = Current.Services.FileService.GetTemplate(templateAlias); - return template != null && content.IsAllowedTemplate(template.Id); - } - - #endregion + private static UmbracoContext UmbracoContext => Current.UmbracoContext; + private static ISiteDomainHelper SiteDomainHelper => Current.Factory.GetInstance(); #region IsComposedOf @@ -185,6 +61,27 @@ namespace Umbraco.Web return template == null ? string.Empty : template.Alias; } + public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) + { + if (Current.Configs.Settings().WebRouting.DisableAlternativeTemplates) + return content.TemplateId == templateId; + + if (content.TemplateId == templateId || !Current.Configs.Settings().WebRouting.ValidateAlternativeTemplates) + return true; + + var publishedContentContentType = Current.Services.ContentTypeService.Get(content.ContentType.Id); + if (publishedContentContentType == null) + throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); + + return publishedContentContentType.IsAllowedTemplate(templateId); + + } + public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) + { + var template = Current.Services.FileService.GetTemplate(templateAlias); + return template != null && content.IsAllowedTemplate(template.Id); + } + #endregion #region HasValue, Value, Value @@ -271,26 +168,24 @@ namespace Umbraco.Web #region Variations /// - /// Determines whether the content has a culture. + /// Gets the culture assigned to a document by domains, in the context of a current Uri. /// - /// Culture is case-insensitive. - public static bool HasCulture(this IPublishedContent content, string culture) - => content.Cultures.ContainsKey(culture ?? string.Empty); - - /// - /// Filters a sequence of to return invariant items, and items that are published for the specified culture. - /// - /// The content items. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null). - internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, string culture = null) - where T : class, IPublishedContent + /// The document. + /// An optional current Uri. + /// The culture assigned to the document by domains. + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + public static string GetCultureFromDomains(this IPublishedContent content, Uri current = null) { - if (contents == null) throw new ArgumentNullException(nameof(contents)); + var umbracoContext = UmbracoContext; - culture = culture ?? Current.VariationContextAccessor.VariationContext?.Culture ?? ""; + if (umbracoContext == null) + throw new InvalidOperationException("A current UmbracoContext is required."); - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || x.HasCulture(culture)); + return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, SiteDomainHelper); } #endregion @@ -315,7 +210,7 @@ namespace Umbraco.Web .And() .ManagedQuery(term); - return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.ContentCache); + return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.Content); } public static IEnumerable SearchChildren(this IPublishedContent content, string term, string indexName = null) @@ -336,7 +231,7 @@ namespace Umbraco.Web .And() .ManagedQuery(term); - return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.ContentCache); + return query.Execute().ToPublishedSearchResults(Current.UmbracoContext.Content); } #endregion @@ -938,7 +833,7 @@ namespace Umbraco.Web if (content == null) throw new ArgumentNullException(nameof(content)); if (orSelf) yield return content; - foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants(culture))) yield return desc; } @@ -946,7 +841,7 @@ namespace Umbraco.Web { yield return content; - foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants(culture))) yield return desc; } @@ -973,23 +868,6 @@ namespace Umbraco.Web #region Axes: children - /// - /// Gets the children of the content. - /// - /// The content. - /// The specific culture to filter for. If null is used the current culture is used. (Default is null) - /// The children of the content. - /// - /// Children are sorted by their sortOrder. - /// This method exists for consistency, it is the same as calling content.Children as a property. - /// - public static IEnumerable Children(this IPublishedContent content, string culture = null) - { - if (content == null) throw new ArgumentNullException(nameof(content)); - - return content.Children.WhereIsInvariantOrHasCulture(culture); - } - /// /// Gets the children of the content, filtered by a predicate. /// @@ -1111,7 +989,7 @@ namespace Umbraco.Web //create all row data var tableData = Core.DataTableExtensions.CreateTableData(); //loop through each child and create row data for it - foreach (var n in content.Children.OrderBy(x => x.SortOrder)) + foreach (var n in content.Children().OrderBy(x => x.SortOrder)) { if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { @@ -1122,13 +1000,13 @@ namespace Umbraco.Web var standardVals = new Dictionary { { "Id", n.Id }, - { "NodeName", n.Name }, + { "NodeName", n.Name() }, { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, { "CreatorName", n.CreatorName }, { "WriterName", n.WriterName }, - { "Url", n.Url } + { "Url", n.Url() } }; var userVals = new Dictionary(); @@ -1300,5 +1178,43 @@ namespace Umbraco.Web } #endregion + + #region Url + + /// + /// Gets the url of the content item. + /// + /// + /// If the content item is a document, then this method returns the url of the + /// document. If it is a media, then this methods return the media url for the + /// 'umbracoFile' property. Use the MediaUrl() method to get the media url for other + /// properties. + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Auto) + { + var umbracoContext = Composing.Current.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); + + switch (content.ContentType.ItemType) + { + case PublishedItemType.Content: + return umbracoContext.UrlProvider.GetUrl(content, mode, culture); + + case PublishedItemType.Media: + return umbracoContext.UrlProvider.GetMediaUrl(content, mode, culture, Constants.Conventions.Media.File); + + default: + throw new NotSupportedException(); + } + } + + #endregion } } diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index de7c72d21a..c35c85c606 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -145,7 +145,7 @@ namespace Umbraco.Web } #endregion - + #region ToIndexedArray public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) @@ -188,5 +188,34 @@ namespace Umbraco.Web } #endregion + + #region MediaUrl + + /// + /// Gets the url for a media. + /// + /// The content item. + /// The culture (use current culture by default). + /// The url mode (use site configuration by default). + /// The alias of the property (use 'umbracoFile' by default). + /// The url for the media. + /// + /// The value of this property is contextual. It depends on the 'current' request uri, + /// if any. In addition, when the content type is multi-lingual, this is the url for the + /// specified culture. Otherwise, it is the invariant url. + /// + public static string MediaUrl(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default, string propertyAlias = Constants.Conventions.Media.File) + { + var umbracoContext = Composing.Current.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url when Current.UmbracoContext.UrlProvider is null."); + + return umbracoContext.UrlProvider.GetMediaUrl(content, mode, culture, propertyAlias); + } + + #endregion } } diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs index 4c879e931f..efd48af2d4 100644 --- a/src/Umbraco.Web/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -31,7 +30,7 @@ namespace Umbraco.Web.Routing #region GetUrl /// - public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) + public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current) { return null; // we have nothing to say } @@ -53,23 +52,21 @@ namespace Umbraco.Web.Routing /// public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - var node = umbracoContext.ContentCache.GetById(id); + var node = umbracoContext.Content.GetById(id); if (node == null) yield break; if (!node.HasProperty(Constants.Conventions.Content.UrlAlias)) yield break; - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // look for domains, walking up the tree var n = node; - var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, false); while (domainUris == null && n != null) // n is null at root { // move to parent node n = n.Parent; - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: false); + domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, excludeDefault: false); } // determine whether the alias property varies diff --git a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs index 75af1fed84..eae198bb59 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs @@ -47,13 +47,13 @@ namespace Umbraco.Web.Routing while (pos > 1) { route = route.Substring(0, pos); - node = frequest.UmbracoContext.ContentCache.GetByRoute(route, culture: frequest?.Culture?.Name); + node = frequest.UmbracoContext.Content.GetByRoute(route, culture: frequest?.Culture?.Name); if (node != null) break; pos = route.LastIndexOf('/'); } if (node != null) { - var d = DomainHelper.FindWildcardDomainInPath(frequest.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), node.Path, null); + var d = DomainUtilities.FindWildcardDomainInPath(frequest.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), node.Path, null); if (d != null) errorCulture = d.Culture; } @@ -71,7 +71,7 @@ namespace Umbraco.Web.Routing { _logger.Debug("Got id={ErrorNodeId}.", error404.Value); - content = frequest.UmbracoContext.ContentCache.GetById(error404.Value); + content = frequest.UmbracoContext.Content.GetById(error404.Value); _logger.Debug(content == null ? "Could not find content with that id." diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index b3c060fe35..b339198928 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Routing if (nodeId > 0) { _logger.Debug("Id={NodeId}", nodeId); - node = frequest.UmbracoContext.ContentCache.GetById(nodeId); + node = frequest.UmbracoContext.Content.GetById(nodeId); if (node != null) { diff --git a/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs b/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs index 54745b2529..70a920f6f0 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByPageIdQuery.cs @@ -14,7 +14,7 @@ int pageId; if (int.TryParse(frequest.UmbracoContext.HttpContext.Request["umbPageID"], out pageId)) { - var doc = frequest.UmbracoContext.ContentCache.GetById(pageId); + var doc = frequest.UmbracoContext.Content.GetById(pageId); if (doc != null) { diff --git a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs index 254dedf979..46e44dc5a3 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Routing public bool TryFindContent(PublishedRequest frequest) { var route = frequest.HasDomain - ? frequest.Domain.ContentId + DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()) + ? frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()) : frequest.Uri.GetAbsolutePathDecoded(); var redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route); @@ -44,8 +44,8 @@ namespace Umbraco.Web.Routing return false; } - var content = frequest.UmbracoContext.ContentCache.GetById(redirectUrl.ContentId); - var url = content == null ? "#" : content.GetUrl(redirectUrl.Culture); + var content = frequest.UmbracoContext.Content.GetById(redirectUrl.ContentId); + var url = content == null ? "#" : content.Url(redirectUrl.Culture); if (url.StartsWith("#")) { _logger.Debug("Route {Route} matches content {ContentId} which has no url.", route, redirectUrl.ContentId); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs index 11f2717455..0a14dc97fe 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrl.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Routing { string route; if (frequest.HasDomain) - route = frequest.Domain.ContentId + DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()); + route = frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.Uri.GetAbsolutePathDecoded()); else route = frequest.Uri.GetAbsolutePathDecoded(); @@ -48,7 +48,7 @@ namespace Umbraco.Web.Routing Logger.Debug("Test route {Route}", route); - var node = docreq.UmbracoContext.ContentCache.GetByRoute(docreq.UmbracoContext.InPreviewMode, route, culture: docreq.Culture?.Name); + var node = docreq.UmbracoContext.Content.GetByRoute(docreq.UmbracoContext.InPreviewMode, route, culture: docreq.Culture?.Name); if (node != null) { docreq.PublishedContent = node; diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index a98dbf8a80..cf71611047 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Routing if (frequest.Uri.AbsolutePath != "/") // no alias if "/" { - node = FindContentByAlias(frequest.UmbracoContext.ContentCache, + node = FindContentByAlias(frequest.UmbracoContext.Content, frequest.HasDomain ? frequest.Domain.ContentId : 0, frequest.Culture.Name, frequest.Uri.GetAbsolutePathDecoded()); diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs index 00cd5566de..16dfa63596 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.Routing var path = frequest.Uri.GetAbsolutePathDecoded(); if (frequest.HasDomain) - path = DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, path); + path = DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, path); // no template if "/" if (path == "/") diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index a9c961c280..18d10e577d 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Routing /// public virtual UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, - UrlProviderMode mode, string culture, Uri current) + UrlMode mode, string culture, Uri current) { var prop = content.GetProperty(propertyAlias); var value = prop?.GetValue(culture); @@ -40,7 +40,7 @@ namespace Umbraco.Web.Routing return url == null ? null : UrlInfo.Url(url.ToString(), culture); } - private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) + private Uri AssembleUrl(string path, Uri current, UrlMode mode) { if (string.IsNullOrEmpty(path)) return null; @@ -48,15 +48,15 @@ namespace Umbraco.Web.Routing Uri uri; if (current == null) - mode = UrlProviderMode.Relative; // best we can do + mode = UrlMode.Relative; // best we can do switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); break; - case UrlProviderMode.Relative: - case UrlProviderMode.Auto: + case UrlMode.Relative: + case UrlMode.Auto: uri = new Uri(path, UriKind.Relative); break; default: diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index 9e13d3b9c1..4092538481 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -29,17 +29,17 @@ namespace Umbraco.Web.Routing #region GetUrl /// - public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current) + public virtual UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current) { if (!current.IsAbsoluteUri) throw new ArgumentException("Current url must be absolute.", nameof(current)); // will not use cache if previewing - var route = umbracoContext.ContentCache.GetRouteById(content.Id, culture); + var route = umbracoContext.Content.GetRouteById(content.Id, culture); return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); } - internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode, string culture) + internal UrlInfo GetUrlFromRoute(string route, UmbracoContext umbracoContext, int id, Uri current, UrlMode mode, string culture) { if (string.IsNullOrWhiteSpace(route)) { @@ -47,15 +47,13 @@ namespace Umbraco.Web.Routing return null; } - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // extract domainUri and path // route is / or / var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); var domainUri = pos == 0 ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current, culture); + : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, int.Parse(route.Substring(0, pos)), current, culture); // assemble the url from domainUri (maybe null) and path var url = AssembleUrl(domainUri, path, current, mode).ToString(); @@ -80,19 +78,17 @@ namespace Umbraco.Web.Routing /// public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - var node = umbracoContext.ContentCache.GetById(id); + var node = umbracoContext.Content.GetById(id); if (node == null) yield break; - var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // look for domains, walking up the tree var n = node; - var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + var domainUris = DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, false); while (domainUris == null && n != null) // n is null at root { n = n.Parent; // move to parent node - domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: true); + domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainHelper, n.Id, current, excludeDefault: true); } // no domains = exit @@ -104,7 +100,7 @@ namespace Umbraco.Web.Routing var culture = d?.Culture?.Name; //although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok - var route = umbracoContext.ContentCache.GetRouteById(id, culture); + var route = umbracoContext.Content.GetRouteById(id, culture); if (route == null) continue; //need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) @@ -121,7 +117,7 @@ namespace Umbraco.Web.Routing #region Utilities - Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlProviderMode mode) + Uri AssembleUrl(DomainAndUri domainUri, string path, Uri current, UrlMode mode) { Uri uri; @@ -130,15 +126,15 @@ namespace Umbraco.Web.Routing if (domainUri == null) // no domain was found { if (current == null) - mode = UrlProviderMode.Relative; // best we can do + mode = UrlMode.Relative; // best we can do switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(current.GetLeftPart(UriPartial.Authority) + path); break; - case UrlProviderMode.Relative: - case UrlProviderMode.Auto: + case UrlMode.Relative: + case UrlMode.Auto: uri = new Uri(path, UriKind.Relative); break; default: @@ -147,21 +143,21 @@ namespace Umbraco.Web.Routing } else // a domain was found { - if (mode == UrlProviderMode.Auto) + if (mode == UrlMode.Auto) { //this check is a little tricky, we can't just compare domains if (current != null && domainUri.Uri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) - mode = UrlProviderMode.Relative; + mode = UrlMode.Relative; else - mode = UrlProviderMode.Absolute; + mode = UrlMode.Absolute; } switch (mode) { - case UrlProviderMode.Absolute: + case UrlMode.Absolute: uri = new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path)); break; - case UrlProviderMode.Relative: + case UrlMode.Relative: uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); break; default: diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs index 1151055621..46dc085998 100644 --- a/src/Umbraco.Web/Routing/DomainAndUri.cs +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Routing { try { - Uri = DomainHelper.ParseUriFromDomainName(Name, currentUri); + Uri = DomainUtilities.ParseUriFromDomainName(Name, currentUri); } catch (UriFormatException) { diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainUtilities.cs similarity index 81% rename from src/Umbraco.Web/Routing/DomainHelper.cs rename to src/Umbraco.Web/Routing/DomainUtilities.cs index 95d97653a0..fb0c56b28d 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainUtilities.cs @@ -2,29 +2,69 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; -using Umbraco.Web.PublishedCache; // published snapshot +using Umbraco.Web.PublishedCache; namespace Umbraco.Web.Routing { /// /// Provides utilities to handle domains. /// - public class DomainHelper + public static class DomainUtilities { - private readonly IDomainCache _domainCache; - private readonly ISiteDomainHelper _siteDomainHelper; + #region Document Culture - public DomainHelper(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper) + /// + /// Gets the culture assigned to a document by domains, in the context of a current Uri. + /// + /// The document identifier. + /// The document path. + /// An optional current Uri. + /// An Umbraco context. + /// The site domain helper. + /// The culture assigned to the document by domains. + /// + /// In 1:1 multilingual setup, a document contains several cultures (there is not + /// one document per culture), and domains, withing the context of a current Uri, assign + /// a culture to that document. + /// + internal static string GetCultureFromDomains(int contentId, string contentPath, Uri current, UmbracoContext umbracoContext, ISiteDomainHelper siteDomainHelper) { - _domainCache = domainCache; - _siteDomainHelper = siteDomainHelper; + if (umbracoContext == null) + throw new InvalidOperationException("A current UmbracoContext is required."); + + if (current == null) + current = umbracoContext.CleanedUmbracoUrl; + + // get the published route, else the preview route + // if both are null then the content does not exist + var route = umbracoContext.Content.GetRouteById(contentId) ?? + umbracoContext.Content.GetRouteById(true, contentId); + + if (route == null) + return null; + + var pos = route.IndexOf('/'); + var domain = pos == 0 + ? null + : DomainForNode(umbracoContext.Domains, siteDomainHelper, int.Parse(route.Substring(0, pos)), current); + + var rootContentId = domain?.ContentId ?? -1; + var wcDomain = FindWildcardDomainInPath(umbracoContext.Domains.GetAll(true), contentPath, rootContentId); + + if (wcDomain != null) return wcDomain.Culture.Name; + if (domain != null) return domain.Culture.Name; + return umbracoContext.Domains.DefaultCulture; } - #region Domain for Node + #endregion + + #region Domain for Document /// /// Finds the domain for the specified node, if any, that best matches a specified uri. /// + /// A domain cache. + /// The site domain helper. /// The node identifier. /// The uri, or null. /// The culture, or null. @@ -35,14 +75,14 @@ namespace Umbraco.Web.Routing /// If culture is null, uses the default culture for the installation instead. Otherwise, /// will try with the specified culture, else return null. /// - internal DomainAndUri DomainForNode(int nodeId, Uri current, string culture = null) + internal static DomainAndUri DomainForNode(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper, int nodeId, Uri current, string culture = null) { // be safe if (nodeId <= 0) return null; // get the domains on that node - var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); + var domains = domainCache.GetAssigned(nodeId).ToArray(); // none? if (domains.Length == 0) @@ -50,37 +90,28 @@ namespace Umbraco.Web.Routing // else filter // it could be that none apply (due to culture) - return SelectDomain(domains, current, culture, _domainCache.DefaultCulture, - (cdomainAndUris, ccurrent, cculture, cdefaultCulture) => _siteDomainHelper.MapDomain(cdomainAndUris, ccurrent, cculture, cdefaultCulture)); - } - - /// - /// Gets a value indicating whether a specified node has domains. - /// - /// The node identifier. - /// True if the node has domains, else false. - internal bool NodeHasDomains(int nodeId) - { - return nodeId > 0 && _domainCache.GetAssigned(nodeId, false).Any(); + return SelectDomain(domains, current, culture, domainCache.DefaultCulture, siteDomainHelper.MapDomain); } /// /// Find the domains for the specified node, if any, that match a specified uri. /// + /// A domain cache. + /// The site domain helper. /// The node identifier. /// The uri, or null. /// A value indicating whether to exclude the current/default domain. True by default. /// The domains and their uris, that match the specified uri, else null. /// If at least a domain is set on the node then the method returns the domains that /// best match the specified uri, else it returns null. - internal IEnumerable DomainsForNode(int nodeId, Uri current, bool excludeDefault = true) + internal static IEnumerable DomainsForNode(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper, int nodeId, Uri current, bool excludeDefault = true) { // be safe if (nodeId <= 0) return null; // get the domains on that node - var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); + var domains = domainCache.GetAssigned(nodeId).ToArray(); // none? if (domains.Length == 0) @@ -90,7 +121,7 @@ namespace Umbraco.Web.Routing var domainAndUris = SelectDomains(domains, current).ToArray(); // filter - return _siteDomainHelper.MapDomains(domainAndUris, current, excludeDefault, null, _domainCache.DefaultCulture).ToArray(); + return siteDomainHelper.MapDomains(domainAndUris, current, excludeDefault, null, domainCache.DefaultCulture).ToArray(); } #endregion @@ -252,7 +283,7 @@ namespace Umbraco.Web.Routing } /// - /// Parses a domain name into a URI. + /// Parses a domain name into a URI. /// /// The domain name to parse /// The currently requested URI. If the domain name is relative, the authority of URI will be used. diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs index 419e4d78df..8a81b27415 100644 --- a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -26,6 +26,6 @@ namespace Umbraco.Web.Routing /// e.g. a cdn url provider will most likely always return an absolute url. /// If the provider is unable to provide a url, it returns null. /// - UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlProviderMode mode, string culture, Uri current); + UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlMode mode, string culture, Uri current); } } diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs index 9459c2552d..c0ce1fef39 100644 --- a/src/Umbraco.Web/Routing/IUrlProvider.cs +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it should return null. /// - UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlProviderMode mode, string culture, Uri current); + UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent content, UrlMode mode, string culture, Uri current); /// /// Gets the other urls of a published content. diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index b362b21a0c..6e768c28b6 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -284,7 +284,7 @@ namespace Umbraco.Web.Routing var defaultCulture = domainsCache.DefaultCulture; // try to find a domain matching the current request - var domainAndUri = DomainHelper.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + var domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); // handle domain - always has a contentId and a culture if (domainAndUri != null) @@ -328,7 +328,7 @@ namespace Umbraco.Web.Routing var nodePath = request.PublishedContent.Path; _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); var rootNodeId = request.HasDomain ? request.Domain.ContentId : (int?)null; - var domain = DomainHelper.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); + var domain = DomainUtilities.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); // always has a contentId and a culture if (domain != null) @@ -517,7 +517,7 @@ namespace Umbraco.Web.Routing { // try and get the redirect node from a legacy integer ID valid = true; - internalRedirectNode = request.UmbracoContext.ContentCache.GetById(internalRedirectId); + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); } else { @@ -526,7 +526,7 @@ namespace Umbraco.Web.Routing { // try and get the redirect node from a UDI Guid valid = true; - internalRedirectNode = request.UmbracoContext.ContentCache.GetById(udiInternalRedirectId.Guid); + internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); } } diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index 0d82467179..77aa3c65a1 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -164,19 +164,19 @@ namespace Umbraco.Web.Routing { if (LockedEvents) return; - var contentCache = Current.UmbracoContext.ContentCache; + var contentCache = Current.UmbracoContext.Content; foreach (var entity in args.PublishedEntities) { var entityContent = contentCache.GetById(entity.Id); if (entityContent == null) continue; // 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.Select(c => c.Key).ToArray() + var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? new[] {(string) null}; foreach (var x in entityContent.DescendantsOrSelf()) { // if this entity defines specific cultures, use those instead of the default ones - var cultures = x.Cultures.Any() ? x.Cultures.Select(c => c.Key) : defaultCultures; + var cultures = x.Cultures.Any() ? x.Cultures.Keys : defaultCultures; foreach (var culture in cultures) { @@ -210,7 +210,7 @@ namespace Umbraco.Web.Routing private static void CreateRedirect(int contentId, string culture, Guid contentKey, string oldRoute) { - var contentCache = Current.UmbracoContext.ContentCache; + var contentCache = Current.UmbracoContext.Content; var newRoute = contentCache.GetRouteById(contentId, culture); if (IsNotRoute(newRoute) || oldRoute == newRoute) return; var redirectUrlService = Current.Services.RedirectUrlService; diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 525695c274..59e39fa80a 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -3,11 +3,8 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.Models; namespace Umbraco.Web.Routing { @@ -34,10 +31,10 @@ namespace Umbraco.Web.Routing _urlProviders = urlProviders; _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - var provider = UrlProviderMode.Auto; + var provider = UrlMode.Auto; Mode = provider; - if (Enum.TryParse(routingSettings.UrlProviderMode, out provider)) + if (Enum.TryParse(routingSettings.UrlProviderMode, out provider)) { Mode = provider; } @@ -51,7 +48,7 @@ namespace Umbraco.Web.Routing /// The list of media url providers /// The current variation accessor. /// An optional provider mode. - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlMode mode = UrlMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; @@ -69,65 +66,15 @@ namespace Umbraco.Web.Routing /// /// Gets or sets the provider url mode. /// - public UrlProviderMode Mode { get; set; } + public UrlMode Mode { get; set; } #endregion #region GetUrl - private UrlProviderMode GetMode(bool absolute) => absolute ? UrlProviderMode.Absolute : Mode; - private IPublishedContent GetDocument(int id) => _umbracoContext.ContentCache.GetById(id); - private IPublishedContent GetDocument(Guid id) => _umbracoContext.ContentCache.GetById(id); - - /// - /// Gets the url of a published content. - /// - /// The published content. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(IPublishedContent content, string culture = null, Uri current = null) - => GetUrl(content, Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(IPublishedContent content, bool absolute, string culture = null, Uri current = null) - => GetUrl(content, GetMode(absolute), culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(Guid id, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(Guid id, bool absolute, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), GetMode(absolute), culture, current); + private UrlMode GetMode(bool absolute) => absolute ? UrlMode.Absolute : Mode; + private IPublishedContent GetDocument(int id) => _umbracoContext.Content.GetById(id); + private IPublishedContent GetDocument(Guid id) => _umbracoContext.Content.GetById(id); /// /// Gets the url of a published content. @@ -137,34 +84,9 @@ namespace Umbraco.Web.Routing /// A culture. /// The current absolute url. /// The url for the published content. - public string GetUrl(Guid id, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(Guid id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A culture. - /// The current absolute url. - /// The url for the published content. - public string GetUrl(int id, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), Mode, culture, current); - - /// - /// Gets the url of a published content. - /// - /// The published content identifier. - /// A value indicating whether the url should be absolute in any case. - /// A culture. - /// The current absolute url. - /// The url for the published content. - /// - /// The url is absolute or relative depending on Mode and on current, unless - /// absolute is true, in which case the url is always absolute. - /// - public string GetUrl(int id, bool absolute, string culture = null, Uri current = null) - => GetUrl(GetDocument(id), GetMode(absolute), culture, current); - /// /// Gets the url of a published content. /// @@ -173,7 +95,7 @@ namespace Umbraco.Web.Routing /// A culture. /// The current absolute url. /// The url for the published content. - public string GetUrl(int id, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(int id, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) => GetUrl(GetDocument(id), mode, culture, current); /// @@ -190,11 +112,14 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns "#". /// - public string GetUrl(IPublishedContent content, UrlProviderMode mode, string culture = null, Uri current = null) + public string GetUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, Uri current = null) { - if (content == null || content.ItemType == PublishedItemType.Element) + if (content == null || content.ContentType.ItemType == PublishedItemType.Element) return "#"; + if (mode == UrlMode.Default) + mode = Mode; + // this the ONLY place where we deal with default culture - IUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) @@ -259,40 +184,6 @@ namespace Umbraco.Web.Routing #region GetMediaUrl - /// - /// Gets the url of a media item. - /// - /// The published content. - /// The property alias to resolve the url from. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - public string GetMediaUrl(IPublishedContent content, string propertyAlias, string culture = null, Uri current = null) - => GetMediaUrl(content, propertyAlias, Mode, culture, current); - - /// - /// Gets the url of a media item. - /// - /// The published content. - /// The property alias to resolve the url from. - /// A value indicating whether the url should be absolute in any case. - /// The variation language. - /// The current absolute url. - /// The url for the media. - /// - /// The url is absolute or relative depending on mode and on current. - /// If the media is multi-lingual, gets the url for the specified culture or, - /// when no culture is specified, the current culture. - /// If the provider is unable to provide a url, it returns . - /// - public string GetMediaUrl(IPublishedContent content, string propertyAlias, bool absolute, string culture = null, Uri current = null) - => GetMediaUrl(content, propertyAlias, GetMode(absolute), culture, current); /// /// Gets the url of a media item. /// @@ -308,15 +199,16 @@ namespace Umbraco.Web.Routing /// when no culture is specified, the current culture. /// If the provider is unable to provide a url, it returns . /// - public string GetMediaUrl(IPublishedContent content, - string propertyAlias, UrlProviderMode mode, - string culture = null, Uri current = null) + public string GetMediaUrl(IPublishedContent content, UrlMode mode = UrlMode.Default, string culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri current = null) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (content == null) return ""; + if (mode == UrlMode.Default) + mode = Mode; + // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 2a840828b6..077680d2e2 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -109,7 +109,7 @@ namespace Umbraco.Web.Routing string url; try { - url = umbracoContext.UrlProvider.GetUrl(content.Id, culture); + url = umbracoContext.UrlProvider.GetUrl(content.Id, culture: culture); } catch (Exception ex) { @@ -187,7 +187,7 @@ namespace Umbraco.Web.Routing var l = new List(); while (o != null) { - l.Add(o.Name); + l.Add(o.Name()); o = o.Parent; } l.Reverse(); diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index 42ff0ee5e6..ba606e8d5e 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Web.Helpers; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; @@ -8,6 +9,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Web.Install; using Umbraco.Web.Mvc; +using Umbraco.Web.Security; using Umbraco.Web.WebApi; namespace Umbraco.Web.Runtime @@ -34,6 +36,8 @@ namespace Umbraco.Web.Runtime // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); + + AntiForgeryConfig.AdditionalDataProvider = new UmbracoAntiForgeryAdditionalDataProvider(AntiForgeryConfig.AdditionalDataProvider); } public void Terminate() diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 5a464701e0..e2b6313ca6 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -92,9 +92,9 @@ namespace Umbraco.Web.Runtime // we should stop injecting UmbracoContext and always inject IUmbracoContextAccessor, however at the moment // there are tons of places (controllers...) which require UmbracoContext in their ctor - so let's register - // a way to inject the UmbracoContext - and register it per-request to be more efficient - // TODO: stop doing this - composition.Register(factory => factory.GetInstance().UmbracoContext, Lifetime.Request); + // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context + // in it's own way but we don't want that to happen, we manage its lifetime ourselves. + composition.Register(factory => factory.GetInstance().UmbracoContext); composition.Register(factory => { @@ -264,7 +264,9 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append() - .Append(); + .Append() + .Append(); + // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 43db9ff0ba..729b2c0a9d 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -49,12 +49,13 @@ namespace Umbraco.Web.Search /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// public IEnumerable ExamineSearch( string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, out long totalFound, string searchFrom = null) + long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); @@ -85,12 +86,12 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -288,7 +289,7 @@ namespace Umbraco.Web.Search } } - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService) + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, bool ignoreUserStartNodes, IEntityService entityService) { if (sb == null) throw new ArgumentNullException(nameof(sb)); if (entityService == null) throw new ArgumentNullException(nameof(entityService)); @@ -311,7 +312,7 @@ namespace Umbraco.Web.Search // make sure we don't find anything sb.Append("+__Path:none "); } - else if (startNodeIds.Contains(-1) == false) // -1 = no restriction + else if (startNodeIds.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction { var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); @@ -356,9 +357,9 @@ namespace Umbraco.Web.Search var m = _mapper.Map(result); //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") + if (m.Icon == Constants.Icons.DefaultIcon) { - m.Icon = "icon-user"; + m.Icon = Constants.Icons.Member; } if (result.Values.ContainsKey("email") && result.Values["email"] != null) @@ -389,9 +390,9 @@ namespace Umbraco.Web.Search { var m = _mapper.Map(result); //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") + if (m.Icon == Constants.Icons.DefaultIcon) { - m.Icon = "icon-picture"; + m.Icon = Constants.Icons.MediaImage; } yield return m; } diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..c6ad4c6901 --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,92 @@ +using System; +using Umbraco.Web.Mvc; +using Umbraco.Core; +using System.Web.Helpers; +using System.Web; +using Newtonsoft.Json; +using Umbraco.Web.Composing; + +namespace Umbraco.Web.Security +{ + /// + /// A custom to create a unique antiforgery token/validator per form created with BeginUmbracoForm + /// + public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; + + /// + /// Constructor, allows wrapping a default provider + /// + /// + public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) + { + _defaultProvider = defaultProvider; + } + + public string GetAdditionalData(HttpContextBase context) + { + return JsonConvert.SerializeObject(new AdditionalData + { + Stamp = DateTime.UtcNow.Ticks, + //this value will be here if this is a BeginUmbracoForms form + Ufprt = context.Items["ufprt"]?.ToString(), + //if there was a wrapped provider, add it's value to the json, else just a static value + WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" + }); + } + + public bool ValidateAdditionalData(HttpContextBase context, string additionalData) + { + if (!additionalData.DetectIsJson()) + return false; //must be json + + AdditionalData json; + try + { + json = JsonConvert.DeserializeObject(additionalData); + } + catch + { + return false; //couldn't parse + } + + if (json.Stamp == default) return false; + + //if there was a wrapped provider, validate it, else validate the static value + var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; + if (!validateWrapped) + return false; + + var ufprtRequest = context.Request["ufprt"]?.ToString(); + + //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate + if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) + return true; + + //if one or the other is null then something is wrong + if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; + if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) + return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) + return false; + + //ensure they all match + return additionalDataParts.Count == requestParts.Count + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; + } + + internal class AdditionalData + { + public string Ufprt { get; set; } + public long Stamp { get; set; } + public string WrappedValue { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/Services/DashboardService.cs b/src/Umbraco.Web/Services/DashboardService.cs index 71969df475..794c6fa671 100644 --- a/src/Umbraco.Web/Services/DashboardService.cs +++ b/src/Umbraco.Web/Services/DashboardService.cs @@ -104,8 +104,9 @@ namespace Umbraco.Web.Services } } - if (!hasAccess || denyRules.Length == 0) - return true; + // No need to check denyRules if there aren't any, just return current state + if (denyRules.Length == 0) + return hasAccess; // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 5123eb49ac..4602942be1 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.Templates // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. var contentRequest = _publishedRouter.CreateRequest(_umbracoContextAccessor.UmbracoContext); - var doc = contentRequest.UmbracoContext.ContentCache.GetById(pageId); + var doc = contentRequest.UmbracoContext.Content.GetById(pageId); if (doc == null) { diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 8e6e1dcfd0..b1d9947b9b 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -144,7 +144,7 @@ namespace Umbraco.Web.Templates public static string ResolveMediaFromTextString(string text) { // don't attempt to proceed without a context - if (Current.UmbracoContext == null || Current.UmbracoContext.MediaCache == null) + if (Current.UmbracoContext == null || Current.UmbracoContext.Media == null) { return text; } @@ -162,7 +162,7 @@ namespace Umbraco.Web.Templates { return match.Value; } - var media = Current.UmbracoContext.MediaCache.GetById(guidUdi.Guid); + var media = Current.UmbracoContext.Media.GetById(guidUdi.Guid); if(media == null) { // image does not exist - we could choose to remove the image entirely here (return empty string), diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 8c23a91d5a..ac75fd831d 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -59,7 +59,7 @@ namespace Umbraco.Web.Trees nodes.AddRange(docTypeEntities .Select(entity => { - var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, "icon-item-arrangement", true); + var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, Constants.Icons.ContentType, true); treeNode.Path = $"-1,{entity.Id}"; treeNode.NodeType = "document-type-blueprints"; // TODO: This isn't the best way to ensure a no operation process for clicking a node but it works for now. diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9e481fc4c9..970191e510 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -217,7 +217,6 @@ namespace Umbraco.Web.Trees return result; } - /// /// Returns a collection of all menu items that can be on a content node /// diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 1b8f3b1434..015c91cb81 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -17,7 +17,7 @@ using Umbraco.Core.Models.Entities; using System.Web.Http.ModelBinding; using Umbraco.Web.Actions; using Umbraco.Web.Composing; - +using Umbraco.Core.Security; namespace Umbraco.Web.Trees { @@ -69,7 +69,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); - if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false && IgnoreUserStartNodes(queryStrings) == false) { node.AdditionalData["noAccess"] = true; } @@ -87,20 +87,45 @@ namespace Umbraco.Web.Trees /// /// /// - internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings) + internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings, + int[] startNodeIds, string[] startNodePaths) { - var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out var hasPathAccess); - if (entityIsAncestorOfStartNodes == false) + var entityIsAncestorOfStartNodes = ContentPermissionsHelper.IsInBranchOfStartNode(e.Path, startNodeIds, startNodePaths, out var hasPathAccess); + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + if (ignoreUserStartNodes == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); - if (hasPathAccess == false) + if (treeNode == null) + { + //this means that the user has NO access to this node via permissions! They at least need to have browse permissions to see + //the node so we need to return null; + return null; + } + if (!ignoreUserStartNodes && !hasPathAccess) { treeNode.AdditionalData["noAccess"] = true; } return treeNode; } + private void GetUserStartNodes(out int[] startNodeIds, out string[] startNodePaths) + { + switch (RecycleBinId) + { + case Constants.System.RecycleBinMedia: + startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService); + break; + case Constants.System.RecycleBinContent: + startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService); + break; + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + /// /// Returns the /// @@ -127,6 +152,8 @@ namespace Umbraco.Web.Trees ? queryStrings.GetValue(TreeQueryStringParameters.StartNodeId) : string.Empty; + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + if (string.IsNullOrEmpty(startNodeId) == false && startNodeId != "undefined" && startNodeId != rootIdString) { // request has been made to render from a specific, non-root, start node @@ -134,7 +161,7 @@ namespace Umbraco.Web.Trees // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access - if (HasPathAccess(id, queryStrings) == false) + if (ignoreUserStartNodes == false && HasPathAccess(id, queryStrings) == false) { Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); return nodes; @@ -152,7 +179,11 @@ namespace Umbraco.Web.Trees // get child entities - if id is root, but user's start nodes do not contain the // root node, this returns the start nodes instead of root's children var entities = GetChildEntities(id, queryStrings).ToList(); - nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); + + //get the current user start node/paths + GetUserStartNodes(out var userStartNodes, out var userStartNodePaths); + + nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); // if the user does not have access to the root node, what we have is the start nodes, // but to provide some context we also need to add their topmost nodes when they are not @@ -163,7 +194,7 @@ namespace Umbraco.Web.Trees if (topNodeIds.Length > 0) { var topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray()); - nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); + nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings, userStartNodes, userStartNodePaths)).Where(x => x != null)); } } @@ -187,7 +218,7 @@ namespace Umbraco.Web.Trees { // try to parse id as an integer else use GetEntityFromId // which will grok Guids, Udis, etc and let use obtain the id - if (int.TryParse(id, out var entityId) == false) + if (!int.TryParse(id, out var entityId)) { var entity = GetEntityFromId(id); if (entity == null) @@ -196,11 +227,13 @@ namespace Umbraco.Web.Trees entityId = entity.Id; } + var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); + IEntitySlim[] result; // if a request is made for the root node but user has no access to // root node, return start nodes instead - if (entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) + if (!ignoreUserStartNodes && entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) { result = UserStartNodes.Length > 0 ? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes).ToArray() @@ -214,7 +247,7 @@ namespace Umbraco.Web.Trees return result; } - internal virtual IEnumerable GetChildrenFromEntityService(int entityId) + private IEnumerable GetChildrenFromEntityService(int entityId) => Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToList(); /// @@ -305,7 +338,7 @@ namespace Umbraco.Web.Trees //Here we need to figure out if the node is a container and if so check if the user has a custom start node, then check if that start node is a child // of this container node. If that is true, the HasChildren must be true so that the tree node still renders even though this current node is a container/list view. if (isContainer && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) - { + { var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell // the UI that this node does have children and that it isn't a container @@ -363,7 +396,7 @@ namespace Umbraco.Web.Trees } var menu = new MenuItemCollection(); - // only add empty recycle bin if the current user is allowed to delete by default + // only add empty recycle bin if the current user is allowed to delete by default if (deleteAllowed) { menu.Items.Add(new MenuItem("emptyRecycleBin", Services.TextService) @@ -410,18 +443,7 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { var permission = Services.UserService.GetPermissions(Security.CurrentUser, dd.Path); - // TODO: inject - var actions = Current.Actions.FromEntityPermission(permission) - .ToList(); - - var actionDelete = Current.Actions.GetAction(); - - // A user is allowed to delete their own stuff - var tryGetCurrentUserId = Security.GetUserId(); - if (tryGetCurrentUserId && dd.CreatorId == tryGetCurrentUserId.Result && actions.Contains(actionDelete) == false) - actions.Add(actionDelete); - - return actions.Select(x => new MenuItem(x)); + return Current.Actions.FromEntityPermission(permission).Select(x => new MenuItem(x)); } /// @@ -504,5 +526,22 @@ namespace Umbraco.Web.Trees } private readonly ConcurrentDictionary _entityCache = new ConcurrentDictionary(); + + private bool? _ignoreUserStartNodes; + + /// + /// If the request should allows a user to choose nodes that they normally don't have access to + /// + /// + /// + internal bool IgnoreUserStartNodes(FormDataCollection queryStrings) + { + if (_ignoreUserStartNodes.HasValue) return _ignoreUserStartNodes.Value; + + var dataTypeKey = queryStrings.GetValue(TreeQueryStringParameters.DataTypeKey); + _ignoreUserStartNodes = dataTypeKey.HasValue && Services.DataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); + + return _ignoreUserStartNodes.Value; + } } } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 4a8cfba9a5..bead6aa141 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.Trees // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, "icon-item-arrangement", hasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, Constants.Icons.ContentType, hasChildren); node.Path = dt.Path; return node; diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 75260b586d..2465b4d45a 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -53,11 +53,11 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, "icon-autofill", false); + var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, Constants.Icons.DataType, false); node.Path = dt.Path; if (systemListViewDataTypeIds.Contains(dt.Id)) { - node.Icon = "icon-thumbnail-list"; + node.Icon = Constants.Icons.ListView; } return node; })); @@ -72,7 +72,15 @@ namespace Umbraco.Web.Trees { var systemIds = new[] { - Constants.System.DefaultLabelDataTypeId + Constants.DataTypes.Boolean, // Used by the Member Type: "Member" + Constants.DataTypes.Textarea, // Used by the Member Type: "Member" + Constants.DataTypes.LabelBigint, // Used by the Media Type: "Image"; Used by the Media Type: "File" + Constants.DataTypes.LabelDateTime, // Used by the Member Type: "Member" + Constants.DataTypes.LabelDecimal, // Used by the Member Type: "Member" + Constants.DataTypes.LabelInt, // Used by the Media Type: "Image"; Used by the Member Type: "Member" + Constants.DataTypes.LabelString, // Used by the Media Type: "Image"; Used by the Media Type: "File" + Constants.DataTypes.ImageCropper, // Used by the Media Type: "Image" + Constants.DataTypes.Upload, // Used by the Media Type: "File" }; return systemIds.Concat(GetNonDeletableSystemListViewDataTypeIds()); diff --git a/src/Umbraco.Web/Trees/FilesTreeController.cs b/src/Umbraco.Web/Trees/FilesTreeController.cs index 60adb5048e..ae951bebf4 100644 --- a/src/Umbraco.Web/Trees/FilesTreeController.cs +++ b/src/Umbraco.Web/Trees/FilesTreeController.cs @@ -14,6 +14,6 @@ namespace Umbraco.Web.Trees protected override string[] Extensions => ExtensionsStatic; - protected override string FileIcon => "icon-document"; + protected override string FileIcon => Constants.Icons.MediaFile; } } diff --git a/src/Umbraco.Web/Trees/MacrosTreeController.cs b/src/Umbraco.Web/Trees/MacrosTreeController.cs index fcfd2e0b3b..cbe1946779 100644 --- a/src/Umbraco.Web/Trees/MacrosTreeController.cs +++ b/src/Umbraco.Web/Trees/MacrosTreeController.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Trees id, queryStrings, macro.Name, - "icon-settings-alt", + Constants.Icons.Macro, false)); } } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 22ad4ed355..f4f373f9a4 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -166,6 +166,6 @@ namespace Umbraco.Web.Trees { return _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Media, pageSize, pageIndex, out totalFound, searchFrom); } - + } } diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 5798c546fc..53e30a4ee5 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Trees // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, "icon-thumbnails", hasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, Constants.Icons.MediaType, hasChildren); node.Path = dt.Path; return node; diff --git a/src/Umbraco.Web/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web/Trees/MemberGroupTreeController.cs index ea2412e4bd..54c499d717 100644 --- a/src/Umbraco.Web/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberGroupTreeController.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Trees { return Services.MemberGroupService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", false)); + .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, Constants.Icons.MemberGroup, false)); } protected override TreeNode CreateRootNode(FormDataCollection queryStrings) diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index bb0091af54..2657f13255 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Trees "-1", queryStrings, member.Name, - "icon-user", + Constants.Icons.Member, false, "", Udi.Create(ObjectTypes.GetUdiType(Constants.ObjectTypes.Member), member.Key)); @@ -110,7 +110,7 @@ namespace Umbraco.Web.Trees "-1", queryStrings, member.UserName, - "icon-user", + Constants.Icons.Member, false); return node; @@ -124,14 +124,14 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { nodes.Add( - CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member/allMembers"), "icon-users", true, + CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member/allMembers"), Constants.Icons.MemberType, true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + Constants.Conventions.MemberTypes.AllMembersListId)); if (_isUmbracoProvider) { nodes.AddRange(Services.MemberTypeService.GetAll() .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, "icon-users", true, + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, Constants.Icons.MemberType, true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 3a72460963..bd80f63897 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Trees { return Services.MemberTypeService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, "icon-item-arrangement", false)); + .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, Constants.Icons.MemberType, false)); } } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 4acf807b77..0f9b61469e 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 466aff5a1f..02a198401b 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,6 +8,7 @@ public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; + public const string DataTypeKey = "dataTypeKey"; //public const string OnNodeClick = "OnNodeClick"; //public const string RenderParent = "RenderParent"; } diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index 7da0b689af..55d98def86 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Trees //this will load in a custom UI instead of the dashboard for the root node root.RoutePath = $"{Constants.Applications.Users}/{Constants.Trees.Users}/users"; - root.Icon = "icon-users"; + root.Icon = Constants.Icons.UserGroup; root.HasChildren = false; return root; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2eaf4a4744..e99866258f 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -16,7 +16,7 @@ true - full + portable false bin\Debug\ DEBUG;TRACE @@ -26,7 +26,7 @@ latest - pdbonly + portable true bin\Release\ TRACE @@ -80,6 +80,11 @@ + + 1.0.0-beta2-19324-01 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -152,6 +157,7 @@ + @@ -206,14 +212,18 @@ + + + + @@ -222,6 +232,7 @@ + @@ -782,7 +793,6 @@ - @@ -1024,7 +1034,6 @@ - @@ -1145,7 +1154,7 @@ - + @@ -1168,7 +1177,6 @@ True Reference.map - diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 25c2d78731..f6c3d30da3 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -92,7 +92,7 @@ namespace Umbraco.Web if (contentId == default) throw new ArgumentException("Invalid content id " + contentId); - var content = _umbracoContextAccessor.UmbracoContext.ContentCache?.GetById(true, contentId); + var content = _umbracoContextAccessor.UmbracoContext.Content?.GetById(true, contentId); if (content == null) throw new InvalidOperationException("Cannot render a macro, no content found by id " + contentId); diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index e68e6e2c77..c0306e0a81 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; -using Umbraco.Web.Runtime; using Umbraco.Web.Security; namespace Umbraco.Web @@ -20,7 +18,6 @@ namespace Umbraco.Web { private readonly IGlobalSettings _globalSettings; private readonly Lazy _publishedSnapshot; - private DomainHelper _domainHelper; private string _previewToken; private bool? _previewing; @@ -109,19 +106,33 @@ namespace Umbraco.Web /// public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; - // for unit tests - internal bool HasPublishedSnapshot => _publishedSnapshot.IsValueCreated; + /// + /// Gets the published content cache. + /// + [Obsolete("Use the Content property.")] + public IPublishedContentCache ContentCache => PublishedSnapshot.Content; /// /// Gets the published content cache. /// - public IPublishedContentCache ContentCache => PublishedSnapshot.Content; + public IPublishedContentCache Content => PublishedSnapshot.Content; /// /// Gets the published media cache. /// + [Obsolete("Use the Media property.")] public IPublishedMediaCache MediaCache => PublishedSnapshot.Media; + /// + /// Gets the published media cache. + /// + public IPublishedMediaCache Media => PublishedSnapshot.Media; + + /// + /// Gets the domains cache. + /// + public IDomainCache Domains => PublishedSnapshot.Domains; + /// /// Boolean value indicating whether the current request is a front-end umbraco request /// @@ -147,20 +158,6 @@ namespace Umbraco.Web /// public IVariationContextAccessor VariationContextAccessor { get; } - /// - /// Creates and caches an instance of a DomainHelper - /// - /// - /// We keep creating new instances of DomainHelper, it would be better if we didn't have to do that so instead we can - /// have one attached to the UmbracoContext. This method accepts an external ISiteDomainHelper otherwise the UmbracoContext - /// ctor will have to have another parameter added only for this one method which is annoying and doesn't make a ton of sense - /// since the UmbracoContext itself doesn't use this. - /// - /// TODO: The alternative is to have a IDomainHelperAccessor singleton which is cached per UmbracoContext - /// - internal DomainHelper GetDomainHelper(ISiteDomainHelper siteDomainHelper) - => _domainHelper ?? (_domainHelper = new DomainHelper(PublishedSnapshot.Domains, siteDomainHelper)); - /// /// Gets a value indicating whether the request has debugging enabled /// @@ -174,7 +171,8 @@ namespace Umbraco.Web return GlobalSettings.DebugMode && request != null && (string.IsNullOrEmpty(request["umbdebugshowtrace"]) == false - || string.IsNullOrEmpty(request["umbdebug"]) == false); + || string.IsNullOrEmpty(request["umbdebug"]) == false + || string.IsNullOrEmpty(request.Cookies["UMB-DEBUG"]?.Value) == false); } } @@ -201,7 +199,7 @@ namespace Umbraco.Web /// The url for the content. public string Url(int contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, culture); + return UrlProvider.GetUrl(contentId, culture: culture); } /// @@ -212,7 +210,7 @@ namespace Umbraco.Web /// The url for the content. public string Url(Guid contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, culture); + return UrlProvider.GetUrl(contentId, culture: culture); } /// @@ -222,7 +220,7 @@ namespace Umbraco.Web /// The mode. /// /// The url for the content. - public string Url(int contentId, UrlProviderMode mode, string culture = null) + public string Url(int contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } @@ -234,7 +232,7 @@ namespace Umbraco.Web /// The mode. /// /// The url for the content. - public string Url(Guid contentId, UrlProviderMode mode, string culture = null) + public string Url(Guid contentId, UrlMode mode, string culture = null) { return UrlProvider.GetUrl(contentId, mode, culture); } @@ -245,9 +243,10 @@ namespace Umbraco.Web /// The content identifier. /// /// The absolute url for the content. + [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(int contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, true, culture); + return UrlProvider.GetUrl(contentId, UrlMode.Absolute, culture); } /// @@ -256,9 +255,10 @@ namespace Umbraco.Web /// The content identifier. /// /// The absolute url for the content. + [Obsolete("Use the Url() method with UrlMode.Absolute.")] public string UrlAbsolute(Guid contentId, string culture = null) { - return UrlProvider.GetUrl(contentId, true, culture); + return UrlProvider.GetUrl(contentId, UrlMode.Absolute, culture); } #endregion diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 1c6eb28b92..bf017c73cc 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -5,10 +5,14 @@ using System.Web; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Dictionary; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; +using Umbraco.Web.Composing; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web { @@ -228,7 +232,7 @@ namespace Umbraco.Web #endregion - + #region Member/Content/Media from Udi @@ -495,7 +499,7 @@ namespace Umbraco.Web /// The existing contents corresponding to the identifiers. /// If an identifier does not match an existing content, it will be missing in the returned value. public IEnumerable Content(IEnumerable ids) - { + { return ids.Select(id => ContentQuery.Content(id)).WhereNotNull(); } @@ -809,11 +813,37 @@ namespace Umbraco.Web } #endregion + internal static bool DecryptAndValidateEncryptedRouteString(string ufprt, out IDictionary parts) + { + string decryptedString; + try + { + decryptedString = ufprt.DecryptWithMachineKey(); + } + catch (FormatException) + { + Current.Logger.Warn(typeof(UmbracoHelper), "A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); + parts = null; + return false; + } + var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + parts = new Dictionary(); + foreach (var key in parsedQueryString.AllKeys) + { + parts[key] = parsedQueryString[key]; + } + //validate all required keys exist + //the controller + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Controller)) + return false; + //the action + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Action)) + return false; + //the area + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Area)) + return false; - - - - - + return true; + } } } diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index 6a786ed6bf..c973fafa81 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -300,7 +300,7 @@ namespace Umbraco.Web // if yes, return true private bool EnsureHasContent(UmbracoContext context, HttpContextBase httpContext) { - if (context.ContentCache.HasContent()) + if (context.Content.HasContent()) return true; _logger.Warn("Umbraco has no content"); diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 6f7fbacf7a..48c26c93dd 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -20,6 +20,8 @@ namespace Umbraco.Web public static class UrlHelperRenderExtensions { + private static readonly IHtmlString EmptyHtmlString = new HtmlString(string.Empty); + #region GetCropUrl /// @@ -39,6 +41,8 @@ namespace Umbraco.Web /// public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -65,6 +69,8 @@ namespace Umbraco.Web /// public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -144,6 +150,8 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { + if (mediaItem == null) return EmptyHtmlString; + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); @@ -247,6 +255,8 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { + if (imageCropperValue == null) return EmptyHtmlString; + var imageUrl = imageCropperValue.Src; var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, @@ -371,25 +381,6 @@ namespace Umbraco.Web return url.SurfaceAction(action, typeof (T), additionalRouteVals); } - /// - /// Generates a Absolute Media Item URL based on the current context - /// - /// - /// - /// - public static string GetAbsoluteMediaUrl(this UrlHelper urlHelper, IPublishedContent mediaItem) - { - if (urlHelper == null) throw new ArgumentNullException("urlHelper"); - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); - - if (urlHelper.RequestContext.HttpContext.Request.Url != null) - { - var requestUrl = urlHelper.RequestContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority); - return string.Format("{0}{1}", requestUrl, mediaItem.Url); - } - return null; - } - /// /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. diff --git a/src/umbraco.sln b/src/umbraco.sln index 7e4403627f..bbe750b37c 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -70,7 +70,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E3F9F378 ProjectSection(SolutionItems) = preProject ..\build\NuSpecs\tools\applications.config.install.xdt = ..\build\NuSpecs\tools\applications.config.install.xdt ..\build\NuSpecs\tools\ClientDependency.config.install.xdt = ..\build\NuSpecs\tools\ClientDependency.config.install.xdt - ..\build\NuSpecs\tools\install.core.ps1 = ..\build\NuSpecs\tools\install.core.ps1 ..\build\NuSpecs\tools\install.ps1 = ..\build\NuSpecs\tools\install.ps1 ..\build\NuSpecs\tools\Readme.txt = ..\build\NuSpecs\tools\Readme.txt ..\build\NuSpecs\tools\ReadmeUpgrade.txt = ..\build\NuSpecs\tools\ReadmeUpgrade.txt