From d99d5a0b36ebbcbfb53077bcfce9c73a98901bef Mon Sep 17 00:00:00 2001 From: Patrick Scott Date: Thu, 3 Nov 2016 14:30:57 +0000 Subject: [PATCH 001/124] Issue U4-5572 convert file name to friendly media item name --- src/Umbraco.Core/StringExtensions.cs | 30 +++++++++++++++++++--- src/Umbraco.Web/Editors/MediaController.cs | 10 +------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 036b5b979f..02f2193e20 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -105,8 +105,8 @@ namespace Umbraco.Core //if the resolution was success, return it, otherwise just return the path, we've detected // it's a path but maybe it's relative and resolution has failed, etc... in which case we're just // returning what was given to us. - return resolvedUrlResult.Success - ? resolvedUrlResult + return resolvedUrlResult.Success + ? resolvedUrlResult : Attempt.Succeed(input); } } @@ -128,7 +128,7 @@ namespace Umbraco.Core } internal static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); - internal static readonly string[] JsonEmpties = new [] { "[]", "{}" }; + internal static readonly string[] JsonEmpties = new[] { "[]", "{}" }; internal static bool DetectIsEmptyJson(this string input) { return JsonEmpties.Contains(Whitespace.Replace(input, string.Empty)); @@ -1470,5 +1470,29 @@ namespace Umbraco.Core byte[] hash = md5.ComputeHash(myStringBytes); return new Guid(hash); } + + + /// + /// Converts a file name to a friendly name for a content item + /// + /// + /// + public static string friendlyNameFromFilename(this string fileName) + { + // strip the file extension + fileName = StripFileExtension(fileName); + + // underscores and dashes to spaces + fileName = ReplaceMany(fileName, new char[] { '_', '-' }, ' '); + + // any other conversions ? + + // Pascalcase (to be done last) + fileName = System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(fileName); + + + return fileName; + } + } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 08140f9c66..7f6c9f2580 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -534,15 +534,7 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) mediaType = Constants.Conventions.MediaTypes.Image; - //TODO: make the media item name "nice" since file names could be pretty ugly, we have - // string extensions to do much of this but we'll need: - // * Pascalcase the name (use string extensions) - // * strip the file extension - // * underscores to spaces - // * probably remove 'ugly' characters - let's discuss - // All of this logic should exist in a string extensions method and be unit tested - // http://issues.umbraco.org/issue/U4-5572 - var mediaItemName = fileName; + var mediaItemName = fileName.friendlyNameFromFilename(); var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); From 3b231f5138e470fda9dfa3f7a48f231129a3164b Mon Sep 17 00:00:00 2001 From: Eyescream Date: Fri, 28 Jul 2017 15:49:07 +0200 Subject: [PATCH 002/124] Update fileupload.controller.js Uploading a file with a comma in it resulted in an error (because server side there was a split on the comma character). The comma later was replaced with a dash, so it's safe to replace it before sending it to the server. --- .../views/propertyeditors/fileupload/fileupload.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 3c8170e54b..e2112bdc6a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -118,7 +118,7 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag for (var i = 0; i < args.files.length; i++) { //save the file object to the scope's files collection $scope.files.push({ alias: $scope.model.alias, file: args.files[i] }); - newVal += args.files[i].name + ","; + newVal += args.files[i].name.replace(',','-') + ","; } //this is required to re-validate From a8ea535bb2ef9d24ccd28edbd44b58c28909c75e Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 22 Jan 2018 18:52:54 +0100 Subject: [PATCH 003/124] "Select editor" items shouldn't have a max height --- src/Umbraco.Web.UI.Client/src/less/components/card.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index bceef1767b..ecbd36e1e4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -97,7 +97,6 @@ text-align: center; width: 100px; - height: 105px; box-sizing: border-box; } @@ -115,6 +114,7 @@ width: 100%; height: 100%; border-radius: 3px; + padding-bottom: 5px; } From 477761a81adf317c6f1b94d9d59d0c4ec296e698 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 22 Mar 2018 10:41:14 +0100 Subject: [PATCH 004/124] Added property editor alias to PreValueDisplayResolver warning and updated key lookup --- .../Models/Mapping/DataTypeModelMapper.cs | 2 +- .../Models/Mapping/PreValueDisplayResolver.cs | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index 60d9eb7d3b..c2f8d8e25d 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -116,7 +116,7 @@ namespace Umbraco.Web.Models.Mapping var fields = editor.PreValueEditor.Fields.Select(Mapper.Map).ToArray(); if (defaultVals != null) { - PreValueDisplayResolver.MapPreValueValuesToPreValueFields(fields, defaultVals); + PreValueDisplayResolver.MapPreValueValuesToPreValueFields(fields, defaultVals, editor.Alias); } return fields; }); diff --git a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs index f4e7a78504..9d1331e691 100644 --- a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs @@ -18,27 +18,31 @@ namespace Umbraco.Web.Models.Mapping public PreValueDisplayResolver(IDataTypeService dataTypeService) { _dataTypeService = dataTypeService; - } - + } + /// - /// Maps pre-values in the dictionary to the values for the fields + /// Maps pre-values in the dictionary to the values for the fields. /// - /// - /// - internal static void MapPreValueValuesToPreValueFields(PreValueFieldDisplay[] fields, IDictionary preValues) + /// The fields. + /// The pre-values. + /// The editor alias. + internal static void MapPreValueValuesToPreValueFields(PreValueFieldDisplay[] fields, IDictionary preValues, string editorAlias) { - if (fields == null) throw new ArgumentNullException("fields"); - if (preValues == null) throw new ArgumentNullException("preValues"); - //now we need to wire up the pre-values values with the actual fields defined + if (fields == null) throw new ArgumentNullException(nameof(fields)); + if (preValues == null) throw new ArgumentNullException(nameof(preValues)); + + // Now we need to wire up the pre-values values with the actual fields defined foreach (var field in fields) { - var found = preValues.Any(x => x.Key.InvariantEquals(field.Key)); - if (found == false) + // If the dictionary would be constructed with StringComparer.InvariantCultureIgnoreCase, we could just use TryGetValue + var preValue = preValues.SingleOrDefault(x => x.Key.InvariantEquals(field.Key)); + if (preValue.Key == null) { - LogHelper.Warn("Could not find persisted pre-value for field " + field.Key); + LogHelper.Warn("Could not find persisted pre-value for field {0} on property editor {1}", () => field.Key, () => editorAlias); continue; } - field.Value = preValues.Single(x => x.Key.InvariantEquals(field.Key)).Value; + + field.Value = preValue.Value; } } @@ -54,20 +58,20 @@ namespace Umbraco.Web.Models.Mapping } } - //set up the defaults + // Set up the defaults var dataTypeService = _dataTypeService; var preVals = dataTypeService.GetPreValuesCollectionByDataTypeId(source.Id); IDictionary dictionaryVals = preVals.FormatAsDictionary().ToDictionary(x => x.Key, x => (object)x.Value); var result = Enumerable.Empty().ToArray(); - //if we have a prop editor, then format the pre-values based on it and create it's fields. + // If we have a prop editor, then format the pre-values based on it and create it's fields if (propEd != null) { result = propEd.PreValueEditor.Fields.Select(Mapper.Map).ToArray(); dictionaryVals = propEd.PreValueEditor.ConvertDbToEditor(propEd.DefaultPreValues, preVals); } - MapPreValueValuesToPreValueFields(result, dictionaryVals); + MapPreValueValuesToPreValueFields(result, dictionaryVals, source.PropertyEditorAlias); return result; } From 8ddda5868e5a29bf13ab2ac20e5bf6750e5d6337 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 5 Apr 2018 10:41:08 +0200 Subject: [PATCH 005/124] fixes U4-10816 Umbraco V7.7.1 - Media - File upload taking full path --- src/Umbraco.Web/Editors/MediaController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e22c83cbeb..d2f0d060e0 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -466,6 +466,17 @@ namespace Umbraco.Web.Editors [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + //If we've reached here it means: // * Our model has been bound // * and validated From 08bf48b8b7bbf9b0cad1ac590bc9d46fe5fd6e14 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 5 Apr 2018 11:04:56 +0200 Subject: [PATCH 006/124] adding the fix for content controller - to support when uploads are done via content. --- src/Umbraco.Web/Editors/ContentController.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index be78638a9e..3ef3b8a40b 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -543,6 +544,17 @@ namespace Umbraco.Web.Editors private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + //If we've reached here it means: // * Our model has been bound // * and validated From 592de8bebcba97441a0996f32bbc09102bbadea3 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 16:00:53 +0200 Subject: [PATCH 007/124] Model, retrieval, mapping and display for fall back language editing --- .../Upgrade/V_8_0_0/FallbackLanguage.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Models/ILanguage.cs | 6 +++++ src/Umbraco.Core/Models/Language.cs | 8 +++++++ .../Persistence/Dtos/LanguageDto.cs | 9 +++++++ .../Implement/LanguageRepository.cs | 21 +++++++++++++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/languages/edit.controller.js | 14 +++++++++++ .../src/views/languages/edit.html | 15 ++++++++++-- .../views/languages/overview.controller.js | 2 ++ .../src/views/languages/overview.html | 1 + .../Umbraco/config/lang/en_us.xml | 1 + .../Models/ContentEditing/Language.cs | 3 +++ 12 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs new file mode 100644 index 0000000000..f0d7c02b82 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + /// + /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for + /// a given language. + /// + public class FallbackLanguage : MigrationBase + { + public FallbackLanguage(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) + AddColumn("fallbackLanguageId"); + } + } +} diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 7bf9e9b32c..f02bd33d2b 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -33,5 +33,11 @@ namespace Umbraco.Core.Models /// If true, a variant node cannot be published unless this language variant is created /// bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + ILanguage FallbackLanguage { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index fa1c9dc826..42c305d492 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; + private ILanguage _fallbackLanguage; public Language(string isoCode) { @@ -32,6 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); } /// @@ -71,5 +73,11 @@ namespace Umbraco.Core.Models get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + + public ILanguage FallbackLanguage + { + get => _fallbackLanguage; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 12c9fd0bd4..f69caf6c91 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -38,5 +38,14 @@ namespace Umbraco.Core.Persistence.Dtos [Column("mandatory")] [Constraint(Default = "0")] public bool Mandatory { get; set; } + + /// + /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// content if the requested language does not have it published. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index db2e1124a2..9566247f96 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -52,7 +52,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.OrderBy(dto => dto.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).ToList(); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -74,7 +76,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertFromDto); + var dtos = Database.Fetch(sql); + var languages = dtos.Select(ConvertFromDto).ToList(); + PopulateFallbackLanguages(dtos, languages); + return languages; } #endregion @@ -199,7 +204,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = LanguageFactory.BuildEntity(dto); return entity; } - + + private static void PopulateFallbackLanguages(List dtos, IList languages) + { + foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) + { + var language = languages.Single(x => x.Id == dto.Id); + // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) + language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); + } + } + public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6bd78044f8..67028568eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -334,6 +334,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index afb5333ded..9b554b1785 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -56,6 +56,20 @@ }); + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", + label: "Fall back language" + } + }; + + vm.loading = true; + languageResource.getAll().then(function (languages) { + vm.availableLanguages = languages; + vm.loading = false; + }); + if(!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 6aaf915960..adc38de05a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -14,11 +14,11 @@ hide-alias="true"> - + - + @@ -64,6 +64,17 @@ + +
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c81f93c7d6..2f116ef3cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -22,12 +22,14 @@ "treeHeaders_languages", "general_mandatory", "general_default", + "languages_fallsbackToLabel" ]; localizationService.localizeMany(labelKeys).then(function (values) { vm.labels.languages = values[0]; vm.labels.mandatory = values[1]; vm.labels.general = values[2]; + vm.labels.fallsbackTo = values[3]; // set page name vm.page.name = vm.labels.languages; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 90764d3f67..f53326a491 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,6 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} + {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. + Falls back to diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index f78d2bd28f..309e111e32 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -24,5 +24,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } + + [DataMember(Name = "fallbackLanguage")] + public Language FallbackLanguage { get; set; } } } From 660fe2d7732f32b5c43999666f20141dc0fd1061 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 5 Jul 2018 19:37:59 +0200 Subject: [PATCH 008/124] Localised fallback language related messages. Filtered out current languge from list of available fall back languages for selection. --- .../src/views/languages/edit.controller.js | 28 +++++++++++-------- .../src/views/languages/edit.html | 2 +- .../Umbraco/config/lang/en_us.xml | 5 +++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 9b554b1785..3a4961ca81 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -29,7 +29,10 @@ "languages_mandatoryLanguageHelp", "languages_defaultLanguage", "languages_defaultLanguageHelp", - "languages_addLanguage" + "languages_addLanguage", + "languages_noFallbackLanguageOption", + "languages_fallbackLanguageDescription", + "languages_fallbackLanguage" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -39,6 +42,15 @@ vm.labels.defaultLanguage = values[3]; vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; + vm.labels.noFallbackLanguageOption = values[6]; + + $scope.properties = { + fallbackLanguage: { + alias: "fallbackLanguage", + description: values[7], + label: values[8] + } + }; if($routeParams.create) { vm.page.name = vm.labels.addLanguage; @@ -56,21 +68,15 @@ }); - $scope.properties = { - fallbackLanguage: { - alias: "fallbackLanguage", - description: "To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.", - label: "Fall back language" - } - }; - vm.loading = true; languageResource.getAll().then(function (languages) { - vm.availableLanguages = languages; + vm.availableLanguages = languages.filter(function (l) { + return $routeParams.id != l.id; + }); vm.loading = false; }); - if(!$routeParams.create) { + if (!$routeParams.create) { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index adc38de05a..9ebbc5aede 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -70,7 +70,7 @@ ng-model="vm.language.fallbackLanguage.id" required ng-options="l.id as l.name for l in vm.availableLanguages"> - + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 555d4edfb4..ea1db802d5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1657,11 +1657,14 @@ To manage your website, simply open the Umbraco back office and start adding con Add language Mandatory - Properties on this language has to be filled out before the node can be published. + Properties on this language have to be filled out before the node can be published. Default language An Umbraco site can only have one default langugae set. Switching default language may result in default content missing. Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language From 53e96b25f686b01bac86c8a6f86c84b8df69434f Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:23:21 +0200 Subject: [PATCH 009/124] Save of fall-back language on create and update of language --- .../Persistence/Factories/LanguageFactory.cs | 7 +++++ .../Implement/LanguageRepository.cs | 2 -- .../src/views/languages/edit.controller.js | 18 ++++++++++-- .../src/views/languages/edit.html | 1 - .../views/languages/overview.controller.js | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 28 ++++++++++++++----- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 7b24411498..c805ae7f5a 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -18,7 +18,14 @@ namespace Umbraco.Core.Persistence.Factories { var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory }; if (entity.HasIdentity) + { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + } + + if (entity.FallbackLanguage != null) + { + dto.FallbackLanguageId = entity.FallbackLanguage.Id; + } return dto; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 9566247f96..96bb088f2b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -144,14 +144,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IsolatedCache.ClearAllCache(); } -; var dto = LanguageFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; entity.ResetDirtyProperties(); - } protected override void PersistUpdatedItem(ILanguage entity) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 3a4961ca81..bc5c421b7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -52,7 +52,7 @@ } }; - if($routeParams.create) { + if ($routeParams.create) { vm.page.name = vm.labels.addLanguage; languageResource.getCultures().then(function (culturesDictionary) { var cultures = []; @@ -65,7 +65,6 @@ vm.availableCultures = cultures; }); } - }); vm.loading = true; @@ -99,11 +98,26 @@ }); } + function setCultureForFallbackLanguage(lang) { + for (var i = 0; i < vm.availableLanguages.length; i++) { + if (vm.availableLanguages[i].id === lang.id) { + lang.culture = vm.availableLanguages[i].culture; + break; + } + } + } + function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // We need to attach the ISO code to the fall-back language to pass + // server-side validation. + if (vm.language.fallbackLanguage) { + setCultureForFallbackLanguage(vm.language.fallbackLanguage); + } + languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 9ebbc5aede..5570b901e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -68,7 +68,6 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index 2f116ef3cb..c8a728d3aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -34,7 +34,7 @@ vm.page.name = vm.labels.languages; }); - languageResource.getAll().then(function(languages) { + languageResource.getAll().then(function (languages) { vm.languages = languages; vm.loading = false; }); diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 96019da702..418c8401ff 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -8,8 +7,6 @@ using System.Web.Http; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; @@ -136,21 +133,38 @@ namespace Umbraco.Web.Editors } //create it - var newLang = new Umbraco.Core.Models.Language(culture.Name) + var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, - Mandatory = language.Mandatory + Mandatory = language.Mandatory, }; + + AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + AssociateFallbackLanguage(language, found); Services.LocalizationService.Save(found); return Mapper.Map(found); } - + + private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + { + if (submittedLanguage.FallbackLanguage == null) + { + return; + } + + var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); + languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) + { + Id = submittedLanguage.FallbackLanguage.Id, + CultureName = fallbackLanguageCulture.DisplayName + }; + } } } From 8a34ec886470146ffdbd34cd3060991ce277eb22 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 15:51:13 +0200 Subject: [PATCH 010/124] Prevented creation of circular fall-back language paths. Allowed for update to no fall-back language. --- .../src/views/languages/edit.controller.js | 5 ++++ src/Umbraco.Web/Editors/LanguageController.cs | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index bc5c421b7b..79972725fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -112,6 +112,11 @@ if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; + // Handle selection of no fall-back language (should pass null) + if (!vm.language.fallbackLanguage.id) { + vm.language.fallbackLanguage = null; + } + // We need to attach the ISO code to the fall-back language to pass // server-side validation. if (vm.language.fallbackLanguage) { diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 418c8401ff..76a40ac329 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -148,6 +148,13 @@ namespace Umbraco.Web.Editors found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); + + if (UpdatedFallbackLanguageCreatesCircularPath(found)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + Services.LocalizationService.Save(found); return Mapper.Map(found); } @@ -156,6 +163,7 @@ namespace Umbraco.Web.Editors { if (submittedLanguage.FallbackLanguage == null) { + languageToCreateOrUpdate.FallbackLanguage = null; return; } @@ -166,5 +174,27 @@ namespace Umbraco.Web.Editors CultureName = fallbackLanguageCulture.DisplayName }; } + + private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + { + if (language.FallbackLanguage == null) + { + return false; + } + + var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + if (fallbackLanguage.Id == language.Id) + { + return true; + } + + fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + } + + return false; + } } } From 4e859cd88bd0e23e72cfbf3b169d6304d8ebf4cd Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 16:01:54 +0200 Subject: [PATCH 011/124] Prevent delete of a language used as a fall-back for another language --- src/Umbraco.Web/Editors/LanguageController.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 76a40ac329..0640d16129 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -85,13 +85,23 @@ namespace Umbraco.Web.Editors public IHttpActionResult DeleteLanguage(int id) { var language = Services.LocalizationService.GetLanguageById(id); - if (language == null) return NotFound(); + if (language == null) + { + return NotFound(); + } - var totalLangs = Services.LocalizationService.GetAllLanguages().Count(); + var langs = Services.LocalizationService.GetAllLanguages().ToArray(); + var totalLangs = langs.Length; if (language.IsDefaultVariantLanguage || totalLangs == 1) { - var message = $"Language '{language.IsoCode}' is currently set to 'default' or it is the only installed language and can not be deleted."; + var message = $"Language '{language.CultureName}' is currently set to 'default' or it is the only installed language and cannot be deleted."; + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); + } + + if (language.FallbackLanguage != null && langs.Any(x => x.FallbackLanguage?.Id == language.Id)) + { + var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); } From 3c13b7bd70e79338f2f00aba79104c59472b2a3e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Fri, 6 Jul 2018 23:30:57 +0200 Subject: [PATCH 012/124] Added and updated tests for fall back languages --- .../Persistence/Dtos/LanguageDto.cs | 1 + .../Repositories/LanguageRepositoryTest.cs | 39 ++++++++++++++++--- src/Umbraco.Web/Editors/LanguageController.cs | 8 ++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f69caf6c91..25ca43f918 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -46,6 +46,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index cd1fe47f39..bda899789d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("en-US")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguage, Is.Null); } } @@ -61,7 +62,8 @@ namespace Umbraco.Tests.Persistence.Repositories var au = CultureInfo.GetCultureInfo("en-AU"); var language = (ILanguage)new Language(au.Name) { - CultureName = au.DisplayName + CultureName = au.DisplayName, + FallbackLanguage = repository.Get(1) }; repository.Save(language); @@ -73,6 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo(au.DisplayName)); Assert.That(language.IsoCode, Is.EqualTo(au.Name)); + Assert.That(language.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -182,7 +185,7 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") {CultureName = "pt-BR"}; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR" }; repository.Save(languageBR); // Assert @@ -190,6 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsFalse(languageBR.Mandatory); + Assert.IsNull(languageBR.FallbackLanguage); } } @@ -211,6 +215,31 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsTrue(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageBR.Mandatory); + Assert.IsNull(languageBR.FallbackLanguage); + } + } + + [Test] + public void Can_Perform_Add_On_LanguageRepository_With_Fallback_Language() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + // Act + var languageBR = new Language("pt-BR") + { + CultureName = "pt-BR", + FallbackLanguage = repository.Get(1) + }; + repository.Save(languageBR); + + // Assert + Assert.That(languageBR.HasIdentity, Is.True); + Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 + Assert.That(languageBR.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -232,13 +261,11 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.IsTrue(languageBR.Mandatory); // Act - var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true }; repository.Save(languageNZ); languageBR = repository.Get(languageBR.Id); // Assert - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageNZ.IsDefaultVariantLanguage); } @@ -257,6 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; + language.FallbackLanguage = repository.Get(1); repository.Save(language); @@ -266,6 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageUpdated, Is.Not.Null); Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR")); Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR")); + Assert.That(languageUpdated.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); } } @@ -314,7 +343,7 @@ namespace Umbraco.Tests.Persistence.Repositories base.TearDown(); } - public void CreateTestData() + private void CreateTestData() { var languageDK = new Language("da-DK") { CultureName = "da-DK" }; ServiceContext.LocalizationService.Save(languageDK);//Id 2 diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 0640d16129..9b73b50300 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -66,12 +66,11 @@ namespace Umbraco.Web.Editors } else if (allLangs.All(x => !x.IsDefaultVariantLanguage)) { - //if no language has the default flag, then the defaul language is the one with the lowest id + //if no language has the default flag, then the default language is the one with the lowest id model.IsDefaultVariantLanguage = allLangs[0].Id == lang.Id; model.Mandatory = allLangs[0].Id == lang.Id; } } - return model; } @@ -159,7 +158,7 @@ namespace Umbraco.Web.Editors found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; AssociateFallbackLanguage(language, found); - if (UpdatedFallbackLanguageCreatesCircularPath(found)) + if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); @@ -185,7 +184,7 @@ namespace Umbraco.Web.Editors }; } - private bool UpdatedFallbackLanguageCreatesCircularPath(ILanguage language) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) { if (language.FallbackLanguage == null) { @@ -198,6 +197,7 @@ namespace Umbraco.Web.Editors { if (fallbackLanguage.Id == language.Id) { + // We've found the current language in the path of fall back languages, so we have a circular path. return true; } From aadd843c332c9120064503d9ac2104dd75b8b769 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 15:17:38 +0200 Subject: [PATCH 013/124] Implemented published value fall back via language --- .../IPublishedValueFallback.cs | 10 +- .../NoopPublishedValueFallback.cs | 6 +- .../PublishedContent/PublishedContentTests.cs | 6 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../PublishedValueFallback.cs | 14 +- .../PublishedValueLanguageFallback.cs | 219 ++++++++++++++++++ src/Umbraco.Web/PublishedContentExtensions.cs | 14 +- .../Runtime/WebRuntimeComponent.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 9 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 8e1dcfd543..f154d9ef27 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,6 +2,12 @@ namespace Umbraco.Core.Models.PublishedContent { + public enum PublishedValueFallbackPriority + { + RecursiveTree, + FallbackLanguage + } + /// /// Provides a fallback strategy for getting values. /// @@ -30,8 +36,8 @@ namespace Umbraco.Core.Models.PublishedContent T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index b99b4ad415..75ab9df35a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -21,9 +21,9 @@ public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 02f58a00a3..a09cf6d4ad 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -34,11 +34,11 @@ namespace Umbraco.Tests.PublishedContent Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); Container.RegisterSingleton(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(logger)) { Id = 1}, + new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, new DataType(new RichTextPropertyEditor(logger)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, @@ -323,7 +323,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] - public void GetPropertyValueRecursiveTest() + public void Get_Property_Value_Recursive() { var doc = GetNode(1174); var rVal = doc.Value("testRecursive", recurse: true); diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 5eea6bcf72..2f7fe8700b 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers { base.Compose(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); Container.RegisterSingleton(); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 47e4b3d872..562b8e393b 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -11,45 +11,45 @@ namespace Umbraco.Web.Models.PublishedContent // kinda reproducing what was available in v7 /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse) + public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) { // no fallback here if (!recurse) return defaultValue; // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, recurse); + return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) { // no fallback here if (!recurse) return defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs new file mode 100644 index 0000000000..c404288a0e --- /dev/null +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -0,0 +1,219 @@ +using LightInject; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Models.PublishedContent +{ + /// + /// Provides a default implementation for that allows + /// for use of fall-back languages + /// + /// + /// Inherits from that implments what was available in v7. + /// + public class PublishedValueLanguageFallback : PublishedValueFallback + { + /// + /// Gets or sets the services context. + /// + [Inject] + public ServiceContext Services { get; set; } + + /// + public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + { + object value; + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(property, culture, segment, defaultValue); + } + + /// + public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + { + T value; + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(property, culture, segment, defaultValue); + } + + /// + public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + { + object value; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(content, alias, culture, segment, defaultValue); + } + + /// + public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + { + T value; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + { + return value; + } + + return base.GetValue(content, alias, culture, segment, defaultValue); + } + + /// + public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + /// + public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + { + if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) + { + var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree); + if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) + { + // We've prioritised recursive tree search and found a value, so can return it. + return result; + } + + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + { + return result; + } + + return defaultValue; + } + + if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) + { + T result; + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + { + return result; + } + } + + // No language fall back content found, so use base implementation + return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + } + + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value) + { + if (string.IsNullOrEmpty(culture)) + { + value = defaultValue; + return false; + } + + var localizationService = Services.LocalizationService; + var language = localizationService.GetLanguageByIsoCode(culture); + if (language.FallbackLanguage == null) + { + value = defaultValue; + return false; + } + + var fallbackLanguage = language.FallbackLanguage; + while (fallbackLanguage != null) + { + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; + } + + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + } + + value = defaultValue; + return false; + } + + private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + { + fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + return fallbackLanguage.FallbackLanguage; + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1adfb55ca9..22a0bc8aca 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -159,8 +159,9 @@ namespace Umbraco.Web /// The property alias. /// The variation language. /// The variation segment. - /// A value indicating whether to recurse. /// The default value. + /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -169,14 +170,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } #endregion @@ -193,6 +194,7 @@ namespace Umbraco.Web /// The variation segment. /// The default value. /// A value indicating whether to recurse. + /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. /// The value of the content's property identified by the alias, converted to the specified type. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -201,18 +203,18 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index a4e5db0767..501fb6445d 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); // register properties fallback - composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f266213da0..e0f2554412 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -253,6 +253,7 @@ + From 11871dc4f7d6565022c0174b8ace37d1ef7a0563 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 8 Jul 2018 16:11:26 +0200 Subject: [PATCH 014/124] Changed the services reference in the published value fall back via language implementation to constructor injection --- .../PublishedValueLanguageFallback.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index c404288a0e..e6902c00db 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,5 +1,4 @@ -using LightInject; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -14,11 +13,12 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueLanguageFallback : PublishedValueFallback { - /// - /// Gets or sets the services context. - /// - [Inject] - public ServiceContext Services { get; set; } + private readonly ILocalizationService _localizationService; + + public PublishedValueLanguageFallback(ILocalizationService localizationService) + { + _localizationService = localizationService; + } /// public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) @@ -122,8 +122,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -139,7 +138,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -154,8 +153,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -171,7 +169,7 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; @@ -186,8 +184,7 @@ namespace Umbraco.Web.Models.PublishedContent return false; } - var localizationService = Services.LocalizationService; - var language = localizationService.GetLanguageByIsoCode(culture); + var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguage == null) { value = defaultValue; @@ -203,16 +200,18 @@ namespace Umbraco.Web.Models.PublishedContent return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage, localizationService); + fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); } value = defaultValue; return false; } - private static ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage, ILocalizationService localizationService) + private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) { - fallbackLanguage = localizationService.GetLanguageById(fallbackLanguage.Id); // Ensures reference to next fall-back language is loaded if it exists + // Ensure reference to next fall-back language is loaded if it exists + fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return fallbackLanguage.FallbackLanguage; } } From f868bc9589e679aa8cc280f469fb1f73de1299b8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 09:23:26 +0200 Subject: [PATCH 015/124] Added fall back language database migration to UmbracoPlan --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index b7a77b10ce..9db4710241 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -123,7 +123,7 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{3608CD41-792A-4E9A-A97D-42A5E797EE31}"); // must chain to v8 final state (see at end of file) - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // UPGRADE FROM 7, MORE RECENT @@ -221,10 +221,13 @@ namespace Umbraco.Core.Migrations.Upgrade //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one - need a path to final state Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + // 8.0.0 + Chain("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + // FINAL STATE - MUST MATCH LAST ONE ABOVE ! // whenever this changes, update all references in this file! - Add(string.Empty, "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Add(string.Empty, "{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); } } } From 50c5b2ed9213425db1d6d9b1741868b0ae03458e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 11 Jul 2018 22:15:58 +0100 Subject: [PATCH 016/124] Added test for fallback languages --- .../PublishedContentLanuageVariantTests.cs | 130 ++++++++++ .../PublishedContentMoreTests.cs | 239 ++++++------------ .../PublishedContentSnapshotTestBase.cs | 100 ++++++++ .../SolidPublishedSnapshot.cs | 70 ++++- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../PublishedValueLanguageFallback.cs | 4 +- 6 files changed, 376 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs create mode 100644 src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs new file mode 100644 index 0000000000..ea77310977 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Tests.Testing; +using Umbraco.Web; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] + public class PublishedContentLanuageVariantTests : PublishedContentSnapshotTestBase + { + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(_ => GetServiceContext()); + } + + protected ServiceContext GetServiceContext() + { + var serviceContext = TestObjects.GetServiceContextMock(Container); + MockLocalizationService(serviceContext); + return serviceContext; + } + + private static void MockLocalizationService(ServiceContext serviceContext) + { + // Set up languages. + // Spanish falls back to English and Italian to Spanish (and then to English). + // French has no fall back. + var languages = new List + { + new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, + new Language("fr") { Id = 2, CultureName = "French" }, + new Language("es") { Id = 3, CultureName = "Spanish" }, + new Language("it") { Id = 4, CultureName = "Italian" }, + new Language("de") { Id = 5, CultureName = "German" } + }; + languages[2].FallbackLanguage = languages[0]; + languages[3].FallbackLanguage = languages[2]; + + var localizationService = Mock.Get(serviceContext.LocalizationService); + localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) + .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); + } + + 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 prop1 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop1.SetSourceValue("en-US", "Welcome"); + prop1.SetValue("en-US", "Welcome"); + prop1.SetSourceValue("de", "Willkommen"); + prop1.SetValue("de", "Willkommen"); + + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + prop1 + } + }); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "en-US"); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Populated_Requested_Non_Default_Language() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "de"); + Assert.AreEqual("Willkommen", value); + } + + [Test] + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_Without_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "fr"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es"); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "it"); + Assert.AreEqual("Welcome", value); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 101c7827c3..22965ac141 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,91 +1,94 @@ -using System; +using System.Collections.ObjectModel; using System.Linq; -using System.Collections.ObjectModel; -using System.Web.Routing; -using Moq; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Core.Composing; -using Current = Umbraco.Core.Composing.Current; -using LightInject; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentMoreTests : PublishedContentTestBase + public class PublishedContentMoreTests : PublishedContentSnapshotTestBase { - // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet - // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx - // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - - public override void SetUp() + internal override void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache) { - base.SetUp(); + 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); - var umbracoContext = GetUmbracoContext(); - Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; - } + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlSegment = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - protected override void Compose() - { - base.Compose(); + cache.Add(new SolidPublishedContent(contentType2) + { + Id = 2, + SortOrder = 1, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/2", + Level = 1, + Url = "/content-2", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); - Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); - } - - protected override TypeLoader CreatePluginManager(IServiceFactory f) - { - var pluginManager = base.CreatePluginManager(f); - - // this is so the model factory looks into the test assembly - pluginManager.AssembliesToScan = pluginManager.AssembliesToScan - .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) - .ToList(); - - return pluginManager; - } - - private UmbracoContext GetUmbracoContext() - { - RouteData routeData = null; - - var publishedSnapshot = CreatePublishedSnapshot(); - - var publishedSnapshotService = new Mock(); - publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); - - var globalSettings = TestObjects.GetGlobalSettings(); - - var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; - var umbracoContext = new UmbracoContext( - httpContext, - publishedSnapshotService.Object, - new WebSecurity(httpContext, Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), - Enumerable.Empty(), - globalSettings, - new TestVariationContextAccessor()); - - return umbracoContext; - } - - public override void TearDown() - { - base.TearDown(); - - Current.Reset(); + cache.Add(new SolidPublishedContent(contentType2Sub) + { + Id = 3, + SortOrder = 2, + Name = "Content 2Sub", + UrlSegment = "content-2sub", + Path = "/3", + Level = 1, + Url = "/content-2sub", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + Alias = "prop1", + SolidHasValue = true, + SolidValue = 1234, + SolidSourceValue = "1234" + } + } + }); } [Test] @@ -197,95 +200,5 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, result[0].Id); Assert.AreEqual(2, result[1].Id); } - - private static SolidPublishedSnapshot CreatePublishedSnapshot() - { - var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); - - var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - var caches = new SolidPublishedSnapshot(); - var cache = caches.InnerContentCache; - - 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); - - cache.Add(new SolidPublishedContent(contentType1) - { - Id = 1, - SortOrder = 0, - Name = "Content 1", - UrlSegment = "content-1", - Path = "/1", - Level = 1, - Url = "/content-1", - ParentId = -1, - ChildIds = new int[] {}, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2) - { - Id = 2, - SortOrder = 1, - Name = "Content 2", - UrlSegment = "content-2", - Path = "/2", - Level = 1, - Url = "/content-2", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - cache.Add(new SolidPublishedContent(contentType2Sub) - { - Id = 3, - SortOrder = 2, - Name = "Content 2Sub", - UrlSegment = "content-2sub", - Path = "/3", - Level = 1, - Url = "/content-2sub", - ParentId = -1, - ChildIds = new int[] { }, - Properties = new Collection - { - new SolidPublishedProperty - { - Alias = "prop1", - SolidHasValue = true, - SolidValue = 1234, - SolidSourceValue = "1234" - } - } - }); - - return caches; - } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs new file mode 100644 index 0000000000..623472a023 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Collections.ObjectModel; +using System.Web.Routing; +using Moq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Core.Composing; +using Current = Umbraco.Core.Composing.Current; +using LightInject; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; + +namespace Umbraco.Tests.PublishedContent +{ + public abstract class PublishedContentSnapshotTestBase : PublishedContentTestBase + { + // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet + // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx + // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx + + public override void SetUp() + { + base.SetUp(); + + var umbracoContext = GetUmbracoContext(); + Umbraco.Web.Composing.Current.UmbracoContextAccessor.UmbracoContext = umbracoContext; + } + + protected override void Compose() + { + base.Compose(); + + Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) + .ToList(); + + return pluginManager; + } + + private UmbracoContext GetUmbracoContext() + { + RouteData routeData = null; + + var publishedSnapshot = CreatePublishedSnapshot(); + + var publishedSnapshotService = new Mock(); + publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot); + + var globalSettings = TestObjects.GetGlobalSettings(); + + var httpContext = GetHttpContextFactory("http://umbraco.local/", routeData).HttpContext; + var umbracoContext = new UmbracoContext( + httpContext, + publishedSnapshotService.Object, + new WebSecurity(httpContext, Current.Services.UserService, globalSettings), + TestObjects.GetUmbracoSettings(), + Enumerable.Empty(), + globalSettings, + new TestVariationContextAccessor()); + + return umbracoContext; + } + + public override void TearDown() + { + base.TearDown(); + + Current.Reset(); + } + + private SolidPublishedSnapshot CreatePublishedSnapshot() + { + var dataTypeService = new TestObjects.TestDataTypeService( + new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); + + var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); + var caches = new SolidPublishedSnapshot(); + var cache = caches.InnerContentCache; + PopulateCache(factory, cache); + return caches; + } + + internal abstract void PopulateCache(PublishedContentTypeFactory factory, SolidPublishedContentCache cache); + } +} diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 4f63533693..33e315ebec 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -257,10 +257,72 @@ namespace Umbraco.Tests.PublishedContent public bool SolidHasValue { get; set; } public object SolidXPathValue { get; set; } - public object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; - public object GetValue(string culture = null, string segment = null) => SolidValue; - public object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; - public bool HasValue(string culture = null, string segment = null) => SolidHasValue; + public virtual object GetSourceValue(string culture = null, string segment = null) => SolidSourceValue; + public virtual object GetValue(string culture = null, string segment = null) => SolidValue; + public virtual object GetXPathValue(string culture = null, string segment = null) => SolidXPathValue; + public virtual bool HasValue(string culture = null, string segment = null) => SolidHasValue; + } + + internal class SolidPublishedPropertyWithLanguageVariants : SolidPublishedProperty + { + private readonly IDictionary _solidSourceValues = new Dictionary(); + private readonly IDictionary _solidValues = new Dictionary(); + private readonly IDictionary _solidXPathValues = new Dictionary(); + + public override object GetSourceValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetSourceValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture) ? _solidSourceValues[culture] : null; + } + + public override object GetValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetValue(culture, segment); + } + + return _solidValues.ContainsKey(culture) ? _solidValues[culture] : null; + } + + public override object GetXPathValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.GetXPathValue(culture, segment); + } + + return _solidXPathValues.ContainsKey(culture) ? _solidXPathValues[culture] : null; + } + + public override bool HasValue(string culture = null, string segment = null) + { + if (string.IsNullOrEmpty(culture)) + { + return base.HasValue(culture, segment); + } + + return _solidSourceValues.ContainsKey(culture); + } + + public void SetSourceValue(string culture, object value) + { + _solidSourceValues.Add(culture, value); + } + + public void SetValue(string culture, object value) + { + _solidValues.Add(culture, value); + } + + public void SetXPathValue(string culture, object value) + { + _solidXPathValues.Add(culture, value); + } } [PublishedModel("ContentType2")] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c86cf5d2d0..592527a4b6 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -122,6 +122,8 @@ + + diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index e6902c00db..b6dc9f4244 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Models.PublishedContent { private readonly ILocalizationService _localizationService; - public PublishedValueLanguageFallback(ILocalizationService localizationService) + public PublishedValueLanguageFallback(ServiceContext services) { - _localizationService = localizationService; + _localizationService = services.LocalizationService; } /// From d1a31ad8f8383ed1a860e33c8657901405826841 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 08:12:54 +0100 Subject: [PATCH 017/124] Removed index from fallback language column --- src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 25ca43f918..f87930269a 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Dtos /// [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } From 91a0ee2c93d0a136bad6b0ed8caf44368ea2f625 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 12 Jul 2018 20:52:02 +0100 Subject: [PATCH 018/124] Refactored to object graph reference of fallback language to just use id --- src/Umbraco.Core/Models/ILanguage.cs | 4 +- src/Umbraco.Core/Models/Language.cs | 10 ++-- .../Persistence/Factories/LanguageFactory.cs | 9 +--- .../Implement/LanguageRepository.cs | 15 +----- .../Repositories/LanguageRepositoryTest.cs | 18 +++---- .../PublishedContentLanuageVariantTests.cs | 8 ++-- .../src/views/languages/edit.controller.js | 20 -------- .../src/views/languages/edit.html | 2 +- .../views/languages/overview.controller.js | 10 ++++ .../src/views/languages/overview.html | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 47 ++++++++----------- .../Models/ContentEditing/Language.cs | 4 +- .../PublishedValueLanguageFallback.cs | 46 +++++++++--------- 13 files changed, 80 insertions(+), 115 deletions(-) diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index f02bd33d2b..8d1c092e13 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -35,9 +35,9 @@ namespace Umbraco.Core.Models bool Mandatory { get; set; } /// - /// Defines the fallback language that can be used in multi-lingual scenarios to provide + /// Defines the id of a fallback language that can be used in multi-lingual scenarios to provide /// content if the requested language does not have it published. /// - ILanguage FallbackLanguage { get; set; } + int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 42c305d492..5fcd5cd50e 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; - private ILanguage _fallbackLanguage; + private int? _fallbackLanguageId; public Language(string isoCode) { @@ -33,7 +33,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); - public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguage); + public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } /// @@ -74,10 +74,10 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } - public ILanguage FallbackLanguage + public int? FallbackLanguageId { - get => _fallbackLanguage; - set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguage, Ps.Value.FallbackLanguageSelector); + get => _fallbackLanguageId; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, Ps.Value.FallbackLanguageSelector); } } } diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index c805ae7f5a..7ab36d15d6 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ILanguage BuildEntity(LanguageDto dto) { - var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory }; + var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory, FallbackLanguageId = dto.FallbackLanguageId }; // reset dirty initial properties (U4-1946) lang.ResetDirtyProperties(false); return lang; @@ -16,17 +16,12 @@ namespace Umbraco.Core.Persistence.Factories public static LanguageDto BuildDto(ILanguage entity) { - var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory }; + var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory, FallbackLanguageId = entity.FallbackLanguageId }; if (entity.HasIdentity) { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); } - if (entity.FallbackLanguage != null) - { - dto.FallbackLanguageId = entity.FallbackLanguage.Id; - } - return dto; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 96bb088f2b..af5d28c18e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -54,7 +54,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get languages var dtos = Database.Fetch(sql); var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); // initialize the code-id map lock (_codeIdMap) @@ -77,9 +76,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); var dtos = Database.Fetch(sql); - var languages = dtos.Select(ConvertFromDto).ToList(); - PopulateFallbackLanguages(dtos, languages); - return languages; + return dtos.Select(ConvertFromDto).ToList(); } #endregion @@ -203,16 +200,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return entity; } - private static void PopulateFallbackLanguages(List dtos, IList languages) - { - foreach (var dto in dtos.Where(x => x.FallbackLanguageId.HasValue)) - { - var language = languages.Single(x => x.Id == dto.Id); - // ReSharper disable once PossibleInvalidOperationException (DTOs with fallback languages have already been filtered in the loop condition) - language.FallbackLanguage = languages.Single(x => x.Id == dto.FallbackLanguageId.Value); - } - } - public ILanguage GetByIsoCode(string isoCode) { TypedCachePolicy.GetAllCached(PerformGetAll); // ensure cache is populated, in a non-expensive way diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index bda899789d..a63bf5e08d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo("en-US")); Assert.That(language.IsoCode, Is.EqualTo("en-US")); - Assert.That(language.FallbackLanguage, Is.Null); + Assert.That(language.FallbackLanguageId, Is.Null); } } @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = (ILanguage)new Language(au.Name) { CultureName = au.DisplayName, - FallbackLanguage = repository.Get(1) + FallbackLanguageId = 1 }; repository.Save(language); @@ -75,7 +75,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(language.HasIdentity, Is.True); Assert.That(language.CultureName, Is.EqualTo(au.DisplayName)); Assert.That(language.IsoCode, Is.EqualTo(au.Name)); - Assert.That(language.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(language.FallbackLanguageId, Is.EqualTo(1)); } } @@ -193,7 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsFalse(languageBR.IsDefaultVariantLanguage); Assert.IsFalse(languageBR.Mandatory); - Assert.IsNull(languageBR.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -215,7 +215,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 Assert.IsTrue(languageBR.IsDefaultVariantLanguage); Assert.IsTrue(languageBR.Mandatory); - Assert.IsNull(languageBR.FallbackLanguage); + Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -232,14 +232,14 @@ namespace Umbraco.Tests.Persistence.Repositories var languageBR = new Language("pt-BR") { CultureName = "pt-BR", - FallbackLanguage = repository.Get(1) + FallbackLanguageId = 1 }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.That(languageBR.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageBR.FallbackLanguageId, Is.EqualTo(1)); } } @@ -284,7 +284,7 @@ namespace Umbraco.Tests.Persistence.Repositories var language = repository.Get(5); language.IsoCode = "pt-BR"; language.CultureName = "pt-BR"; - language.FallbackLanguage = repository.Get(1); + language.FallbackLanguageId = 1; repository.Save(language); @@ -294,7 +294,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(languageUpdated, Is.Not.Null); Assert.That(languageUpdated.IsoCode, Is.EqualTo("pt-BR")); Assert.That(languageUpdated.CultureName, Is.EqualTo("pt-BR")); - Assert.That(languageUpdated.FallbackLanguage.IsoCode, Is.EqualTo("en-US")); + Assert.That(languageUpdated.FallbackLanguageId, Is.EqualTo(1)); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index ea77310977..22eb4bd799 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -39,15 +39,15 @@ namespace Umbraco.Tests.PublishedContent { new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, new Language("fr") { Id = 2, CultureName = "French" }, - new Language("es") { Id = 3, CultureName = "Spanish" }, - new Language("it") { Id = 4, CultureName = "Italian" }, + new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, + new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, new Language("de") { Id = 5, CultureName = "German" } }; - languages[2].FallbackLanguage = languages[0]; - languages[3].FallbackLanguage = languages[2]; var localizationService = Mock.Get(serviceContext.LocalizationService); localizationService.Setup(x => x.GetAllLanguages()).Returns(languages); + localizationService.Setup(x => x.GetLanguageById(It.IsAny())) + .Returns((int id) => languages.SingleOrDefault(y => y.Id == id)); localizationService.Setup(x => x.GetLanguageByIsoCode(It.IsAny())) .Returns((string c) => languages.SingleOrDefault(y => y.IsoCode == c)); } diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 79972725fc..523ef867cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -98,31 +98,11 @@ }); } - function setCultureForFallbackLanguage(lang) { - for (var i = 0; i < vm.availableLanguages.length; i++) { - if (vm.availableLanguages[i].id === lang.id) { - lang.culture = vm.availableLanguages[i].culture; - break; - } - } - } - function save() { if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; - // Handle selection of no fall-back language (should pass null) - if (!vm.language.fallbackLanguage.id) { - vm.language.fallbackLanguage = null; - } - - // We need to attach the ISO code to the fall-back language to pass - // server-side validation. - if (vm.language.fallbackLanguage) { - setCultureForFallbackLanguage(vm.language.fallbackLanguage); - } - languageResource.save(vm.language).then(function (lang) { formHelper.resetForm({ scope: $scope }); diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html index 5570b901e2..a2217a6649 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.html @@ -67,7 +67,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js index c8a728d3aa..a5c446dfb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js @@ -13,6 +13,16 @@ vm.editLanguage = editLanguage; vm.deleteLanguage = deleteLanguage; + vm.getLanguageById = function(id) { + for (var i = 0; i < vm.languages.length; i++) { + if (vm.languages[i].id === id) { + return vm.languages[i]; + } + } + + return null; + }; + function init() { vm.loading = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index f53326a491..3b75fa62bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -36,7 +36,7 @@ - {{vm.labels.general}} {{vm.labels.mandatory}} - {{vm.labels.fallsbackTo}}: {{language.fallbackLanguage.name}} + {{vm.labels.fallsbackTo}}: {{vm.getLanguageById(language.fallbackLanguageId).name}} x.FallbackLanguage?.Id == language.Id)) + if (langs.Any(x => x.FallbackLanguageId.HasValue && x.FallbackLanguageId.Value == language.Id)) { var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); @@ -147,20 +147,21 @@ namespace Umbraco.Web.Editors CultureName = culture.DisplayName, IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, Mandatory = language.Mandatory, + FallbackLanguageId = language.FallbackLanguageId }; - AssociateFallbackLanguage(language, newLang); Services.LocalizationService.Save(newLang); return Mapper.Map(newLang); } found.Mandatory = language.Mandatory; found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; - AssociateFallbackLanguage(language, found); + found.FallbackLanguageId = language.FallbackLanguageId; - if (DoesUpdatedFallbackLanguageCreateACircularPath(found)) + string selectedFallbackLanguageCultureName; + if (DoesUpdatedFallbackLanguageCreateACircularPath(found, out selectedFallbackLanguageCultureName)) { - ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + found.FallbackLanguage.CultureName + "' would create a circular path."); + ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + selectedFallbackLanguageCultureName + "' would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } @@ -168,43 +169,35 @@ namespace Umbraco.Web.Editors return Mapper.Map(found); } - private static void AssociateFallbackLanguage(Language submittedLanguage, ILanguage languageToCreateOrUpdate) + private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language, out string selectedFallbackLanguageCultureName) { - if (submittedLanguage.FallbackLanguage == null) - { - languageToCreateOrUpdate.FallbackLanguage = null; - return; - } - - var fallbackLanguageCulture = CultureInfo.GetCultureInfo(submittedLanguage.FallbackLanguage.IsoCode); - languageToCreateOrUpdate.FallbackLanguage = new Core.Models.Language(fallbackLanguageCulture.Name) - { - Id = submittedLanguage.FallbackLanguage.Id, - CultureName = fallbackLanguageCulture.DisplayName - }; - } - - private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language) - { - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { + selectedFallbackLanguageCultureName = string.Empty; return false; } var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { - if (fallbackLanguage.Id == language.Id) + if (fallbackLanguageId.Value == language.Id) { // We've found the current language in the path of fall back languages, so we have a circular path. + selectedFallbackLanguageCultureName = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).CultureName; return true; } - fallbackLanguage = languages.Single(x => x.Id == fallbackLanguage.Id).FallbackLanguage; + fallbackLanguageId = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).FallbackLanguageId; } + selectedFallbackLanguageCultureName = string.Empty; return false; } + + private static ILanguage GetLanguageFromCollectionById(IEnumerable languages, int id) + { + return languages.Single(x => x.Id == id); + } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index 309e111e32..7693ee836e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isMandatory")] public bool Mandatory { get; set; } - [DataMember(Name = "fallbackLanguage")] - public Language FallbackLanguage { get; set; } + [DataMember(Name = "fallbackLanguageId")] + public int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index b6dc9f4244..d6e8db83f4 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -107,13 +107,6 @@ namespace Umbraco.Web.Models.PublishedContent return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); } - private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) - { - return value != null && - string.IsNullOrEmpty(value.ToString()) == false && - value.Equals(defaultValue) == false; - } - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) { if (string.IsNullOrEmpty(culture)) @@ -123,22 +116,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -154,22 +148,23 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; @@ -185,34 +180,39 @@ namespace Umbraco.Web.Models.PublishedContent } var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguage == null) + if (language.FallbackLanguageId.HasValue == false) { value = defaultValue; return false; } - var fallbackLanguage = language.FallbackLanguage; - while (fallbackLanguage != null) + var fallbackLanguageId = language.FallbackLanguageId; + while (fallbackLanguageId.HasValue) { + var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; } - fallbackLanguage = GetNextFallbackLanguage(fallbackLanguage); + fallbackLanguageId = fallbackLanguage.FallbackLanguageId; } value = defaultValue; return false; } - private ILanguage GetNextFallbackLanguage(ILanguage fallbackLanguage) + private ILanguage GetLanguageById(int id) { - // Ensure reference to next fall-back language is loaded if it exists - fallbackLanguage = _localizationService.GetLanguageById(fallbackLanguage.Id); + return _localizationService.GetLanguageById(id); + } - return fallbackLanguage.FallbackLanguage; + private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) + { + return value != null && + string.IsNullOrEmpty(value.ToString()) == false && + value.Equals(defaultValue) == false; } } } From b1ef9c5aa81a20dcf7f1e14901d3556e75254dc5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 18 Jul 2018 12:27:14 +0200 Subject: [PATCH 019/124] U4-11502 misc fixes --- src/Umbraco.Core/Models/ILanguage.cs | 32 ++++-- src/Umbraco.Core/Models/Language.cs | 23 ++-- .../IPublishedValueFallback.cs | 103 ++++++++++++++++-- .../Persistence/Dtos/LanguageDto.cs | 18 ++- .../Persistence/Factories/LanguageFactory.cs | 4 +- .../Implement/LanguageRepository.cs | 23 ++-- .../Services/Implement/ContentService.cs | 2 +- .../Repositories/LanguageRepositoryTest.cs | 22 ++-- ...> PublishedContentLanguageVariantTests.cs} | 4 +- .../Services/ContentServiceTests.cs | 6 +- .../Services/LocalizationServiceTests.cs | 10 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 2 +- src/Umbraco.Web/Editors/LanguageController.cs | 22 ++-- .../Models/ContentEditing/Language.cs | 5 +- .../ContentItemDisplayVariationResolver.cs | 4 +- 16 files changed, 188 insertions(+), 94 deletions(-) rename src/Umbraco.Tests/PublishedContent/{PublishedContentLanuageVariantTests.cs => PublishedContentLanguageVariantTests.cs} (97%) diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 8d1c092e13..c0d2fed839 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -4,40 +4,54 @@ using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { + /// + /// Represents a language. + /// public interface ILanguage : IEntity, IRememberBeingDirty { /// - /// Gets or sets the Iso Code for the Language + /// Gets or sets the ISO code of the language. /// [DataMember] string IsoCode { get; set; } /// - /// Gets or sets the Culture Name for the Language + /// Gets or sets the culture name of the language. /// [DataMember] string CultureName { get; set; } /// - /// Returns a object for the current Language + /// Gets the object for the language. /// [IgnoreDataMember] CultureInfo CultureInfo { get; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// - bool IsDefaultVariantLanguage { get; set; } + [DataMember] + bool IsDefault { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// - bool Mandatory { get; set; } + /// + /// When a language is mandatory, a multi-lingual document cannot be published + /// without that language being published, and unpublishing that language unpublishes + /// the entire document. + /// + [DataMember] + bool IsMandatory { get; set; } /// - /// Defines the id of a fallback language that can be used in multi-lingual scenarios to provide - /// content if the requested language does not have it published. + /// Gets or sets the identifier of a fallback language. /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + [DataMember] int? FallbackLanguageId { get; set; } } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 5fcd5cd50e..940648c4b9 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -31,14 +31,12 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo IsoCodeSelector = ExpressionHelper.GetPropertyInfo(x => x.IsoCode); public readonly PropertyInfo CultureNameSelector = ExpressionHelper.GetPropertyInfo(x => x.CultureName); - public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefaultVariantLanguage); - public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.Mandatory); + public readonly PropertyInfo IsDefaultVariantLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.IsDefault); + public readonly PropertyInfo MandatorySelector = ExpressionHelper.GetPropertyInfo(x => x.IsMandatory); public readonly PropertyInfo FallbackLanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.FallbackLanguageId); } - /// - /// Gets or sets the Iso Code for the Language - /// + /// [DataMember] public string IsoCode { @@ -46,9 +44,7 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _isoCode, Ps.Value.IsoCodeSelector); } - /// - /// Gets or sets the Culture Name for the Language - /// + /// [DataMember] public string CultureName { @@ -56,24 +52,25 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector); } - /// - /// Returns a object for the current Language - /// + /// [IgnoreDataMember] public CultureInfo CultureInfo => CultureInfo.GetCultureInfo(IsoCode); - public bool IsDefaultVariantLanguage + /// + public bool IsDefault { get => _isDefaultVariantLanguage; set => SetPropertyValueAndDetectChanges(value, ref _isDefaultVariantLanguage, Ps.Value.IsDefaultVariantLanguageSelector); } - public bool Mandatory + /// + public bool IsMandatory { get => _mandatory; set => SetPropertyValueAndDetectChanges(value, ref _mandatory, Ps.Value.MandatorySelector); } + /// public int? FallbackLanguageId { get => _fallbackLanguageId; diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index f154d9ef27..afa70fbd47 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,6 +2,8 @@ namespace Umbraco.Core.Models.PublishedContent { + // fixme document + // fixme add values? public enum PublishedValueFallbackPriority { RecursiveTree, @@ -16,28 +18,105 @@ namespace Umbraco.Core.Models.PublishedContent // todo - understand caching vs fallback (recurse etc) public interface IPublishedValueFallback { - // note that at property level, property.GetValue() does NOT implement fallback, and one has - // to get property.Value() or property.Value() to trigger fallback - - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - + /// + /// Gets a fallback value for a property. + /// + /// The property. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); - // this method is called whenever property.Value(culture, segment, defaultValue) is called, and - // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - + /// + /// Gets a fallback value for a property. + /// + /// The type of the value. + /// The property. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and + /// property.HasValue(culture, segment) is false. + /// It can only fallback at property level (no recurse). + /// At property level, property.GetValue() does *not* implement fallback, and one has to + /// get property.Value() or property.Value{T}() to trigger fallback. + /// T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); - // these methods to be called whenever getting the property value for the specified alias, culture and segment, - // either returned no property at all, or a property that does not HasValue for the specified culture and segment. - + /// + /// Gets a fallback value for a published element property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + /// + /// Gets a fallback value for a published element property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// It can only fallback at element level (no recurse). + /// T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + /// + /// Gets a fallback value for a published content property. + /// + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// fixme explain & document priority + merge w/recurse? + /// object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + /// + /// Gets a fallback value for a published content property. + /// + /// The type of the value. + /// The published element. + /// The property alias. + /// The requested culture. + /// The requested segment. + /// An optional default value. + /// A fallback value, or null. + /// + /// This method is called whenever getting the property value for the specified alias, culture and + /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. + /// fixme explain & document priority + merge w/recurse? + /// T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); } } diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f87930269a..f389ab78c3 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -10,38 +10,46 @@ namespace Umbraco.Core.Persistence.Dtos { public const string TableName = Constants.DatabaseSchema.Tables.Language; + /// + /// Gets or sets the identifier of the language. + /// [Column("id")] [PrimaryKeyColumn(IdentitySeed = 2)] public short Id { get; set; } + /// + /// Gets or sets the ISO code of the language. + /// [Column("languageISOCode")] [Index(IndexTypes.UniqueNonClustered)] [NullSetting(NullSetting = NullSettings.Null)] [Length(10)] public string IsoCode { get; set; } + /// + /// Gets or sets the culture name of the language. + /// [Column("languageCultureName")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] public string CultureName { get; set; } /// - /// Defines if this language is the default variant language when language variants are in use + /// Gets or sets a value indicating whether the language is the default language. /// [Column("isDefaultVariantLang")] [Constraint(Default = "0")] public bool IsDefaultVariantLanguage { get; set; } /// - /// If true, a variant node cannot be published unless this language variant is created + /// Gets or sets a value indicating whether the language is mandatory. /// [Column("mandatory")] [Constraint(Default = "0")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } /// - /// Defines the fallback language that can be used in multi-lingual scenarios to provide - /// content if the requested language does not have it published. + /// Gets or sets the identifier of a fallback language. /// [Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 7ab36d15d6..db2927eea3 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Persistence.Factories { public static ILanguage BuildEntity(LanguageDto dto) { - var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefaultVariantLanguage = dto.IsDefaultVariantLanguage, Mandatory = dto.Mandatory, FallbackLanguageId = dto.FallbackLanguageId }; + var lang = new Language(dto.IsoCode) { CultureName = dto.CultureName, Id = dto.Id, IsDefault = dto.IsDefaultVariantLanguage, IsMandatory = dto.IsMandatory, FallbackLanguageId = dto.FallbackLanguageId }; // reset dirty initial properties (U4-1946) lang.ResetDirtyProperties(false); return lang; @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence.Factories public static LanguageDto BuildDto(ILanguage entity) { - var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefaultVariantLanguage, Mandatory = entity.Mandatory, FallbackLanguageId = entity.FallbackLanguageId }; + var dto = new LanguageDto { CultureName = entity.CultureName, IsoCode = entity.IsoCode, IsDefaultVariantLanguage = entity.IsDefault, IsMandatory = entity.IsMandatory, FallbackLanguageId = entity.FallbackLanguageId }; if (entity.HasIdentity) { dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 787cbc1690..4753b131fe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -53,12 +53,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get languages var dtos = Database.Fetch(sql); - var languages = dtos.Select(ConvertFromDto).ToList(); + var languages = dtos.Select(ConvertFromDto).ToList(); // fixme - .OrderBy(x => x.Id) is gone? // fix inconsistencies: there has to be a default language, and it has to be mandatory - var defaultLanguage = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage) ?? languages.First(); - defaultLanguage.IsDefaultVariantLanguage = true; - defaultLanguage.Mandatory = true; + var defaultLanguage = languages.FirstOrDefault(x => x.IsDefault) ?? languages.First(); + defaultLanguage.IsDefault = true; + defaultLanguage.IsMandatory = true; // initialize the code-id map lock (_codeIdMap) @@ -122,10 +122,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return list; } - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); #endregion @@ -138,7 +135,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase)entity).AddingEntity(); - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { //if this entity is flagged as the default, we need to set all others to false Database.Execute(Sql().Update(u => u.Set(x => x.IsDefaultVariantLanguage, false))); @@ -161,14 +158,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ((EntityBase)entity).UpdatingEntity(); - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) { //if this entity is flagged as the default, we need to set all others to false Database.Execute(Sql().Update(u => u.Set(x => x.IsDefaultVariantLanguage, false))); //We need to clear the whole cache since all languages will be updated IsolatedCache.ClearAllCache(); } - + var dto = LanguageFactory.BuildDto(entity); Database.Update(dto); @@ -183,7 +180,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistDeletedItem(ILanguage entity) { //we need to validate that we can delete this language - if (entity.IsDefaultVariantLanguage) + if (entity.IsDefault) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); var count = Database.ExecuteScalar(Sql().SelectCount().From()); @@ -268,7 +265,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var language in all) { // if one language is default, return - if (language.IsDefaultVariantLanguage) + if (language.IsDefault) return language; // keep track of language with lowest id if (first == null || language.Id < first.Id) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 30e76468a6..b6d3fece19 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1082,7 +1082,7 @@ namespace Umbraco.Core.Services.Implement var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published if (!cannotBePublished) { - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published } diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index a63bf5e08d..68d1f71ea8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -191,8 +191,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsFalse(languageBR.Mandatory); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsFalse(languageBR.IsMandatory); Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -207,14 +207,14 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); // Assert Assert.That(languageBR.HasIdentity, Is.True); Assert.That(languageBR.Id, Is.EqualTo(6)); //With 5 existing entries the Id should be 6 - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); Assert.IsNull(languageBR.FallbackLanguageId); } } @@ -252,22 +252,22 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageBR = (ILanguage)new Language("pt-BR") { CultureName = "pt-BR", IsDefault = true, IsMandatory = true }; repository.Save(languageBR); var languageEN = new Language("en-AU") { CultureName = "en-AU" }; repository.Save(languageEN); - Assert.IsTrue(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageBR.Mandatory); + Assert.IsTrue(languageBR.IsDefault); + Assert.IsTrue(languageBR.IsMandatory); // Act - var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefaultVariantLanguage = true, Mandatory = true }; + var languageNZ = new Language("en-NZ") { CultureName = "en-NZ", IsDefault = true, IsMandatory = true }; repository.Save(languageNZ); languageBR = repository.Get(languageBR.Id); // Assert - Assert.IsFalse(languageBR.IsDefaultVariantLanguage); - Assert.IsTrue(languageNZ.IsDefaultVariantLanguage); + Assert.IsFalse(languageBR.IsDefault); + Assert.IsTrue(languageNZ.IsDefault); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs similarity index 97% rename from src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs rename to src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 22eb4bd799..7f9e7ad954 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Tests.PublishedContent { [TestFixture] [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] - public class PublishedContentLanuageVariantTests : PublishedContentSnapshotTestBase + public class PublishedContentLanguageVariantTests : PublishedContentSnapshotTestBase { protected override void Compose() { @@ -37,7 +37,7 @@ namespace Umbraco.Tests.PublishedContent // French has no fall back. var languages = new List { - new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, + new Language("en-US") { Id = 1, CultureName = "English", IsDefault = true }, new Language("fr") { Id = 2, CultureName = "French" }, new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index d5003674af..cce264fb3d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2489,7 +2489,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2524,7 +2524,7 @@ namespace Umbraco.Tests.Services { var languageService = ServiceContext.LocalizationService; - var langUk = new Language("en-UK") { IsDefaultVariantLanguage = true }; + var langUk = new Language("en-UK") { IsDefault = true }; var langFr = new Language("fr-FR"); languageService.Save(langFr); @@ -2562,7 +2562,7 @@ namespace Umbraco.Tests.Services var languageService = ServiceContext.LocalizationService; //var langFr = new Language("fr-FR") { IsDefaultVariantLanguage = true }; - var langXx = new Language("pt-PT") { IsDefaultVariantLanguage = true }; + var langXx = new Language("pt-PT") { IsDefault = true }; var langFr = new Language("fr-FR"); var langUk = new Language("en-UK"); var langDe = new Language("de-DE"); diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 033fa08d4a..cc97c98f5d 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -362,21 +362,21 @@ namespace Umbraco.Tests.Services { var localizationService = ServiceContext.LocalizationService; var language = new Core.Models.Language("en-AU"); - language.IsDefaultVariantLanguage = true; + language.IsDefault = true; localizationService.Save(language); var result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result.IsDefaultVariantLanguage); + Assert.IsTrue(result.IsDefault); var language2 = new Core.Models.Language("en-NZ"); - language2.IsDefaultVariantLanguage = true; + language2.IsDefault = true; localizationService.Save(language2); var result2 = localizationService.GetLanguageById(language2.Id); //re-get result = localizationService.GetLanguageById(language.Id); - Assert.IsTrue(result2.IsDefaultVariantLanguage); - Assert.IsFalse(result.IsDefaultVariantLanguage); + Assert.IsTrue(result2.IsDefault); + Assert.IsFalse(result.IsDefault); } [Test] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 0b984b1167..bf9e9fefde 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -122,7 +122,7 @@ - + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 650f0d082b..176e1bc461 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -766,7 +766,7 @@ namespace Umbraco.Web.Editors var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant - .Where(x => x.Mandatory); + .Where(x => x.IsMandatory); foreach (var lang in mandatoryLangs) { //cannot continue publishing since a required language that is not currently being published isn't published diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 0de3aca634..7b4e1e36fe 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -57,18 +57,18 @@ namespace Umbraco.Web.Editors //if there's only one language, by default it is the default var allLangs = Services.LocalizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); - if (!lang.IsDefaultVariantLanguage) + if (!lang.IsDefault) { if (allLangs.Count == 1) { - model.IsDefaultVariantLanguage = true; - model.Mandatory = true; + model.IsDefault = true; + model.IsMandatory = true; } - else if (allLangs.All(x => !x.IsDefaultVariantLanguage)) + else if (allLangs.All(x => !x.IsDefault)) { //if no language has the default flag, then the default language is the one with the lowest id - model.IsDefaultVariantLanguage = allLangs[0].Id == lang.Id; - model.Mandatory = allLangs[0].Id == lang.Id; + model.IsDefault = allLangs[0].Id == lang.Id; + model.IsMandatory = allLangs[0].Id == lang.Id; } } @@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors var langs = Services.LocalizationService.GetAllLanguages().ToArray(); var totalLangs = langs.Length; - if (language.IsDefaultVariantLanguage || totalLangs == 1) + if (language.IsDefault || totalLangs == 1) { var message = $"Language '{language.CultureName}' is currently set to 'default' or it is the only installed language and cannot be deleted."; throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); @@ -145,8 +145,8 @@ namespace Umbraco.Web.Editors var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, - IsDefaultVariantLanguage = language.IsDefaultVariantLanguage, - Mandatory = language.Mandatory, + IsDefault = language.IsDefault, + IsMandatory = language.IsMandatory, FallbackLanguageId = language.FallbackLanguageId }; @@ -154,8 +154,8 @@ namespace Umbraco.Web.Editors return Mapper.Map(newLang); } - found.Mandatory = language.Mandatory; - found.IsDefaultVariantLanguage = language.IsDefaultVariantLanguage; + found.IsMandatory = language.IsMandatory; + found.IsDefault = language.IsDefault; found.FallbackLanguageId = language.FallbackLanguageId; string selectedFallbackLanguageCultureName; diff --git a/src/Umbraco.Web/Models/ContentEditing/Language.cs b/src/Umbraco.Web/Models/ContentEditing/Language.cs index 7693ee836e..75dd07bf09 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Language.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing @@ -20,10 +19,10 @@ namespace Umbraco.Web.Models.ContentEditing public string Name { get; set; } [DataMember(Name = "isDefault")] - public bool IsDefaultVariantLanguage { get; set; } + public bool IsDefault { get; set; } [DataMember(Name = "isMandatory")] - public bool Mandatory { get; set; } + public bool IsMandatory { get; set; } [DataMember(Name = "fallbackLanguageId")] public int? FallbackLanguageId { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index cb6e2938be..21c27649bb 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Models.Mapping var variants = langs.Select(x => new ContentVariation { Language = x, - Mandatory = x.Mandatory, + Mandatory = x.IsMandatory, Name = source.GetCultureName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished @@ -61,7 +61,7 @@ namespace Umbraco.Web.Models.Mapping } } if (!foundCurrent) - variants.First(x => x.Language.IsDefaultVariantLanguage).IsCurrent = true; + variants.First(x => x.Language.IsDefault).IsCurrent = true; return variants; } From ecc75bc4c9ae9a12b21dcccded61e32cee3f85e8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 08:24:08 +0200 Subject: [PATCH 020/124] Allowed delete of langage via services even if used as a fall-back for other languages, by setting references to null before deleting --- .../Persistence/Dtos/LanguageDto.cs | 1 + .../Implement/LanguageRepository.cs | 7 ++++-- .../Services/Implement/LocalizationService.cs | 3 +-- .../Repositories/LanguageRepositoryTest.cs | 24 +++++++++++++++++++ .../Services/LocalizationServiceTests.cs | 14 +++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index f87930269a..25ca43f918 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -45,6 +45,7 @@ namespace Umbraco.Core.Persistence.Dtos ///
[Column("fallbackLanguageId")] [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] [NullSetting(NullSetting = NullSettings.Null)] public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index af5d28c18e..271e084969 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -177,7 +177,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistDeletedItem(ILanguage entity) { - //we need to validate that we can delete this language + // We need to validate that we can delete this language if (entity.IsDefaultVariantLanguage) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); @@ -185,9 +185,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (count == 1) throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})"); + // We need to remove any references to the language if it's being used as a fall-back from other ones + Database.Execute(Sql().Update(u => u.Set(x => x.FallbackLanguageId, null)).Where(x => x.FallbackLanguageId == entity.Id)); + base.PersistDeletedItem(entity); - //Clear the cache entries that exist by key/iso + // Clear the cache entries that exist by key/iso IsolatedCache.ClearCacheItem(RepositoryCacheKeys.GetKey(entity.IsoCode)); IsolatedCache.ClearCacheItem(RepositoryCacheKeys.GetKey(entity.CultureName)); } diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index 663ecf586c..e63a3cbdbb 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -393,8 +393,7 @@ namespace Umbraco.Core.Services.Implement return; } - //NOTE: There isn't any constraints in the db, so possible references aren't deleted - + // NOTE: Other than the fall-back language, there aren't any other constraints in the db, so possible references aren't deleted _languageRepository.Delete(language); deleteEventArgs.CanCancel = false; diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index a63bf5e08d..ed82d03c6e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -318,6 +318,30 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Delete_On_LanguageRepository_With_Language_Used_As_Fallback() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + // Add language to delete as a fall-back language to another one + var repository = CreateRepository(provider); + var languageToFallbackFrom = repository.Get(5); + languageToFallbackFrom.FallbackLanguageId = 1; + repository.Save(languageToFallbackFrom); + + // Act + var languageToDelete = repository.Get(1); + repository.Delete(languageToDelete); + + var exists = repository.Exists(1); + + // Assert + Assert.That(exists, Is.False); + } + } + [Test] public void Can_Perform_Exists_On_LanguageRepository() { diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 033fa08d4a..ff5aa8edc9 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -192,6 +192,20 @@ namespace Umbraco.Tests.Services Assert.Null(language); } + [Test] + public void Can_Delete_Language_Used_As_Fallback() + { + var danish = ServiceContext.LocalizationService.GetLanguageByIsoCode("da-DK"); + var norwegian = new Language("nb-NO") { CultureName = "Norwegian", FallbackLanguageId = danish.Id }; + ServiceContext.LocalizationService.Save(norwegian, 0); + var languageId = danish.Id; + + ServiceContext.LocalizationService.Delete(danish); + + var language = ServiceContext.LocalizationService.GetLanguageById(languageId); + Assert.Null(language); + } + [Test] public void Can_Create_DictionaryItem_At_Root() { From 695f21eadbe6aee6415f8983998686db2ffd49f8 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 09:41:07 +0200 Subject: [PATCH 021/124] Removed unnecessary loop in looking up value from a fall-back language. Put in a check to abort fall-back if there's a loop in language fall-backs. --- .../IPublishedValueFallback.cs | 14 +- .../NoopPublishedValueFallback.cs | 16 ++- .../PublishedContentLanuageVariantTests.cs | 14 +- .../PublishedValueFallback.cs | 17 +-- .../PublishedValueLanguageFallback.cs | 129 +++++++++--------- src/Umbraco.Web/PublishedContentExtensions.cs | 15 +- .../PublishedContentPropertyExtension.cs | 9 +- src/Umbraco.Web/PublishedElementExtensions.cs | 11 +- 8 files changed, 127 insertions(+), 98 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index f154d9ef27..d9d9c0c298 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Composing; +using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent { @@ -22,22 +22,22 @@ namespace Umbraco.Core.Models.PublishedContent // this method is called whenever property.Value(culture, segment, defaultValue) is called, and // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages); // this method is called whenever property.Value(culture, segment, defaultValue) is called, and // property.HasValue(culture, segment) is false. it can only fallback at property level (no recurse). - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages); // these methods to be called whenever getting the property value for the specified alias, culture and segment, // either returned no property at all, or a property that does not HasValue for the specified culture and segment. - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages); - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index 75ab9df35a..a7de0709e6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Models.PublishedContent +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a noop implementation for . @@ -9,21 +11,21 @@ public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index 22eb4bd799..17ce032005 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -35,13 +35,17 @@ namespace Umbraco.Tests.PublishedContent // Set up languages. // Spanish falls back to English and Italian to Spanish (and then to English). // French has no fall back. + // Danish, Swedish and Norweigan create an invalid loop. var languages = new List { new Language("en-US") { Id = 1, CultureName = "English", IsDefaultVariantLanguage = true }, new Language("fr") { Id = 2, CultureName = "French" }, new Language("es") { Id = 3, CultureName = "Spanish", FallbackLanguageId = 1 }, new Language("it") { Id = 4, CultureName = "Italian", FallbackLanguageId = 3 }, - new Language("de") { Id = 5, CultureName = "German" } + new Language("de") { Id = 5, CultureName = "German" }, + new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 }, + new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 }, + new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 } }; var localizationService = Mock.Get(serviceContext.LocalizationService); @@ -126,5 +130,13 @@ namespace Umbraco.Tests.PublishedContent var value = content.Value("welcomeText", "it"); Assert.AreEqual("Welcome", value); } + + [Test] + public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "no"); + Assert.IsNull(value); + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 562b8e393b..86823767fd 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models.PublishedContent; +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models.PublishedContent { @@ -11,45 +12,45 @@ namespace Umbraco.Web.Models.PublishedContent // kinda reproducing what was available in v7 /// - public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { // no fallback here if (!recurse) return defaultValue; // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority); + return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority, visitedLanguages); } /// - public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { // no fallback here if (!recurse) return defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index d6e8db83f4..d19ef80732 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models; +using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -21,72 +22,72 @@ namespace Umbraco.Web.Models.PublishedContent } /// - public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { object value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(property, culture, segment, defaultValue); + return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); } /// - public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { T value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(property, culture, segment, defaultValue); + return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); } /// - public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { object value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(content, alias, culture, segment, defaultValue); + return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); } /// - public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { T value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, out value)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) { return value; } - return base.GetValue(content, alias, culture, segment, defaultValue); + return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); } /// - public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { - return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); } /// - public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority) + public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) { if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) { - var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree); + var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree, visitedLanguages); if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) { // We've prioritised recursive tree search and found a value, so can return it. return result; } - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) { return result; } @@ -97,112 +98,116 @@ namespace Umbraco.Web.Models.PublishedContent if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) { T result; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, out result)) + if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) { return result; } } // No language fall back content found, so use base implementation - return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); } - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) { + value = defaultValue; + if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) { + value = defaultValue; + if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, out T value) + private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages, out T value) { + value = defaultValue; if (string.IsNullOrEmpty(culture)) { - value = defaultValue; return false; } var language = _localizationService.GetLanguageByIsoCode(culture); if (language.FallbackLanguageId.HasValue == false) { - value = defaultValue; return false; } - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) + if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) { - var fallbackLanguage = GetLanguageById(fallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - fallbackLanguageId = fallbackLanguage.FallbackLanguageId; + return false; + } + + visitedLanguages.Add(language.FallbackLanguageId.Value); + + var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); + if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) + { + return true; } - value = defaultValue; return false; } + private static bool AlreadyVisitedLanguage(ICollection visitedLanguages, int fallbackLanguageId) + { + return visitedLanguages.Contains(fallbackLanguageId); + } + private ILanguage GetLanguageById(int id) { return _localizationService.GetLanguageById(id); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 22a0bc8aca..4ab2d1dc79 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -162,6 +162,7 @@ namespace Umbraco.Web /// The default value. /// A value indicating whether to recurse. /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -170,14 +171,15 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); } #endregion @@ -195,6 +197,7 @@ namespace Umbraco.Web /// The default value. /// A value indicating whether to recurse. /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// Recursively means: walking up the tree from , get the first value that can be found. @@ -203,18 +206,20 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", + bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs index fcbfc7f431..fdfd772ce7 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedContentPropertyExtension.cs @@ -1,4 +1,5 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -15,19 +16,19 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion #region Value - public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) { // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index 945270cb9e..b0d2826df4 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -99,6 +100,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -106,14 +108,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion @@ -129,6 +131,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -136,14 +139,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); } #endregion From 1be74589f2d3869f504f13a07ad6ce36f9664752 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 21 Jul 2018 15:58:49 +0200 Subject: [PATCH 022/124] Combined fallback parameters into an array that defines methods and priority to be used. --- src/Umbraco.Core/Constants-Content.cs | 22 +++ .../IPublishedValueFallback.cs | 10 +- .../NoopPublishedValueFallback.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedContentLanuageVariantTests.cs | 102 ++++++++++- .../PublishedContent/PublishedContentTests.cs | 4 +- .../SolidPublishedSnapshot.cs | 22 ++- .../PublishedValueFallback.cs | 78 ++++++--- .../PublishedValueLanguageFallback.cs | 163 ++---------------- src/Umbraco.Web/PublishedContentExtensions.cs | 39 ++--- src/Umbraco.Web/umbraco.presentation/item.cs | 4 +- 11 files changed, 226 insertions(+), 223 deletions(-) create mode 100644 src/Umbraco.Core/Constants-Content.cs diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs new file mode 100644 index 0000000000..4b8c383e6f --- /dev/null +++ b/src/Umbraco.Core/Constants-Content.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Defines content retrieval related constants + /// + public static class Content + { + /// + /// Defines core supported content fall-back options when retrieving content property values. + /// Defined as constants rather than enum to allow solution or package defined fall-back methods. + /// + public static class FallbackMethods + { + public const int None = 0; + public const int RecursiveTree = 1; + public const int FallbackLanguage = 2; + } + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index d9d9c0c298..7a7b67a2d1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -2,12 +2,6 @@ namespace Umbraco.Core.Models.PublishedContent { - public enum PublishedValueFallbackPriority - { - RecursiveTree, - FallbackLanguage - } - /// /// Provides a fallback strategy for getting values. /// @@ -36,8 +30,8 @@ namespace Umbraco.Core.Models.PublishedContent T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a7de0709e6..a8d55176a3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -23,9 +23,9 @@ namespace Umbraco.Core.Models.PublishedContent public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 67028568eb..0a24cc5286 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -286,6 +286,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs index 17ce032005..7ee2e11209 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanuageVariantTests.cs @@ -45,7 +45,8 @@ namespace Umbraco.Tests.PublishedContent new Language("de") { Id = 5, CultureName = "German" }, new Language("da") { Id = 6, CultureName = "Danish", FallbackLanguageId = 8 }, new Language("sv") { Id = 7, CultureName = "Swedish", FallbackLanguageId = 6 }, - new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 } + new Language("no") { Id = 8, CultureName = "Norweigan", FallbackLanguageId = 7 }, + new Language("nl") { Id = 9, CultureName = "Dutch", FallbackLanguageId = 1 } }; var localizationService = Mock.Get(serviceContext.LocalizationService); @@ -68,12 +69,28 @@ namespace Umbraco.Tests.PublishedContent { Alias = "welcomeText", }; - prop1.SetSourceValue("en-US", "Welcome"); - prop1.SetValue("en-US", "Welcome"); + prop1.SetSourceValue("en-US", "Welcome", true); + prop1.SetValue("en-US", "Welcome", true); prop1.SetSourceValue("de", "Willkommen"); prop1.SetValue("de", "Willkommen"); + prop1.SetSourceValue("nl", "Welkom"); + prop1.SetValue("nl", "Welkom"); - cache.Add(new SolidPublishedContent(contentType1) + var prop2 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText2", + }; + prop2.SetSourceValue("en-US", "Welcome", true); + prop2.SetValue("en-US", "Welcome", true); + + var prop3 = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "welcomeText", + }; + prop3.SetSourceValue("en-US", "Welcome", true); + prop3.SetValue("en-US", "Welcome", true); + + var item1 = new SolidPublishedContent(contentType1) { Id = 1, SortOrder = 0, @@ -83,12 +100,35 @@ namespace Umbraco.Tests.PublishedContent Level = 1, Url = "/content-1", ParentId = -1, + ChildIds = new[] { 2 }, + Properties = new Collection + { + prop1, prop2 + } + }; + + var item2 = new SolidPublishedContent(contentType1) + { + Id = 2, + SortOrder = 0, + Name = "Content 2", + UrlSegment = "content-2", + Path = "/1/2", + Level = 2, + Url = "/content-1/content-2", + ParentId = 1, ChildIds = new int[] { }, Properties = new Collection { - prop1 + prop3 } - }); + }; + + item1.Children = new List { item2 }; + item2.Parent = item1; + + cache.Add(item1); + cache.Add(item2); } [Test] @@ -116,10 +156,18 @@ namespace Umbraco.Tests.PublishedContent } [Test] - public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + public void Do_Not_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Unless_Requested() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); var value = content.Value("welcomeText", "es"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var value = content.Value("welcomeText", "es", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.AreEqual("Welcome", value); } @@ -127,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "it"); + var value = content.Value("welcomeText", "it", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.AreEqual("Welcome", value); } @@ -135,8 +183,44 @@ namespace Umbraco.Tests.PublishedContent public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "no"); + var value = content.Value("welcomeText", "no", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); Assert.IsNull(value); } + + [Test] + public void Do_Not_Get_Content_Recursively_Unless_Requested() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2"); + Assert.IsNull(value); + } + + [Test] + public void Can_Get_Content_Recursively() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText2", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree }); + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Can_Get_Content_With_Recursive_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree, Core.Constants.Content.FallbackMethods.FallbackLanguage }); + + // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. + Assert.AreEqual("Welkom", value); + } + + [Test] + public void Can_Get_Content_With_Fallback_Language_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage, Core.Constants.Content.FallbackMethods.RecursiveTree }); + + // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + Assert.AreEqual("Welcome", value); + } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index a09cf6d4ad..330711a6ba 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -326,8 +326,8 @@ namespace Umbraco.Tests.PublishedContent public void Get_Property_Value_Recursive() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", recurse: true); - var nullVal = doc.Value("DoNotFindThis", recurse: true); + var rVal = doc.Value("testRecursive", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree } ); + var nullVal = doc.Value("DoNotFindThis", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 33e315ebec..cbf7f3189d 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -248,7 +248,7 @@ namespace Umbraco.Tests.PublishedContent #endregion } - class SolidPublishedProperty : IPublishedProperty + internal class SolidPublishedProperty : IPublishedProperty { public PublishedPropertyType PropertyType { get; set; } public string Alias { get; set; } @@ -309,19 +309,33 @@ namespace Umbraco.Tests.PublishedContent return _solidSourceValues.ContainsKey(culture); } - public void SetSourceValue(string culture, object value) + public void SetSourceValue(string culture, object value, bool defaultValue = false) { _solidSourceValues.Add(culture, value); + if (defaultValue) + { + SolidSourceValue = value; + SolidHasValue = true; + } } - public void SetValue(string culture, object value) + public void SetValue(string culture, object value, bool defaultValue = false) { _solidValues.Add(culture, value); + if (defaultValue) + { + SolidValue = value; + SolidHasValue = true; + } } - public void SetXPathValue(string culture, object value) + public void SetXPathValue(string culture, object value, bool defaultValue = false) { _solidXPathValues.Add(culture, value); + if (defaultValue) + { + SolidXPathValue = value; + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 86823767fd..85b10be480 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models.PublishedContent @@ -8,54 +9,79 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueFallback : IPublishedValueFallback { - // this is our default implementation // kinda reproducing what was available in v7 /// - public virtual object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) { // no fallback here return defaultValue; } /// - public virtual object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) { - // no fallback here - if (!recurse) return defaultValue; - // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, true, fallbackPriority, visitedLanguages); + return GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages); } /// - public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) { - // no fallback here - if (!recurse) return defaultValue; + if (fallbackMethods == null) + { + return defaultValue; + } - // otherwise, implement recursion as it was implemented in PublishedContentBase + foreach (var fallbackMethod in fallbackMethods) + { + if (TryGetValueWithFallbackMethod(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, fallbackMethod, out T value)) + { + return value; + } + } + + return defaultValue; + } + + protected virtual bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) + { + value = defaultValue; + switch (fallbackMethod) + { + case Core.Constants.Content.FallbackMethods.None: + return false; + case Core.Constants.Content.FallbackMethods.RecursiveTree: + return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + default: + throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); + } + } + + protected static bool TryGetValueWithRecursiveTree(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { + // Implement recursion as it was implemented in PublishedContentBase // fixme caching? // @@ -73,21 +99,31 @@ namespace Umbraco.Web.Models.PublishedContent { content = content.Parent; property = content?.GetProperty(alias); - if (property != null) noValueProperty = property; - } while (content != null && (property == null || property.HasValue(culture, segment) == false)); + if (property != null) + { + noValueProperty = property; + } + } + while (content != null && (property == null || property.HasValue(culture, segment) == false)); // if we found a content with the property having a value, return that property value if (property != null && property.HasValue(culture, segment)) - return property.Value(culture, segment); + { + value = property.Value(culture, segment); + return true; + } // if we found a property, even though with no value, return that property value // because the converter may want to handle the missing value. ie if defaultValue is default, // either specified or by default, the converter may want to substitute something else. if (noValueProperty != null) - return noValueProperty.Value(culture, segment, defaultValue: defaultValue); + { + value = noValueProperty.Value(culture, segment, defaultValue: defaultValue); + return true; + } - // else return default - return defaultValue; + value = defaultValue; + return false; } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs index d19ef80732..e94a8559a0 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -21,158 +23,23 @@ namespace Umbraco.Web.Models.PublishedContent _localizationService = services.LocalizationService; } - /// - public override object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) - { - object value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) - { - T value; - if (TryGetValueFromFallbackLanguage(property, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(property, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) - { - object value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) - { - T value; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, visitedLanguages, out value)) - { - return value; - } - - return base.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages); - } - - /// - public override object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) - { - return GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); - } - - /// - public override T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages) - { - if (fallbackPriority == PublishedValueFallbackPriority.RecursiveTree) - { - var result = base.GetValue(content, alias, culture, segment, defaultValue, recurse, PublishedValueFallbackPriority.RecursiveTree, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(result, defaultValue)) - { - // We've prioritised recursive tree search and found a value, so can return it. - return result; - } - - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) - { - return result; - } - - return defaultValue; - } - - if (fallbackPriority == PublishedValueFallbackPriority.FallbackLanguage) - { - T result; - if (TryGetValueFromFallbackLanguage(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages, out result)) - { - return result; - } - } - - // No language fall back content found, so use base implementation - return base.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); - } - - private bool TryGetValueFromFallbackLanguage(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) + protected override bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) { value = defaultValue; - - if (string.IsNullOrEmpty(culture)) + switch (fallbackMethod) { - return false; + case Core.Constants.Content.FallbackMethods.None: + return false; + case Core.Constants.Content.FallbackMethods.RecursiveTree: + return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + case Core.Constants.Content.FallbackMethods.FallbackLanguage: + return TryGetValueWithFallbackLanguage(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, out value); + default: + throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = property.Value(fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; } - private bool TryGetValueFromFallbackLanguage(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages, out T value) - { - value = defaultValue; - - if (string.IsNullOrEmpty(culture)) - { - return false; - } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; - } - - private bool TryGetValueFromFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, bool recurse, PublishedValueFallbackPriority fallbackPriority, ICollection visitedLanguages, out T value) + private bool TryGetValueWithFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, out T value) { value = defaultValue; if (string.IsNullOrEmpty(culture)) @@ -194,7 +61,7 @@ namespace Umbraco.Web.Models.PublishedContent visitedLanguages.Add(language.FallbackLanguageId.Value); var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, recurse, fallbackPriority, visitedLanguages); + value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, fallbackMethods.ToArray(), visitedLanguages); if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) { return true; diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 4ab2d1dc79..c4c745a087 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -151,43 +151,35 @@ namespace Umbraco.Web #endregion #region Value - + /// - /// Recursively the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. /// /// The content. /// The property alias. /// The variation language. /// The variation segment. /// The default value. - /// A value indicating whether to recurse. - /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// Options for fall-back if content not found. /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); } #endregion #region Value - + /// - /// Recursively gets the value of a content's property identified by its alias, converted to a specified type. + /// Gets the value of a content's property identified by its alias, converted to a specified type. /// /// The target property type. /// The content. @@ -195,31 +187,24 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A value indicating whether to recurse. - /// Flag indicating priority order of fallback paths in cases when content does not exist and a fall back method is used. + /// Options for fall-back if content not found. /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// - /// Recursively means: walking up the tree from , get the first value that can be found. - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, recurse, fallbackPriority, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); } // fixme - .Value() refactoring - in progress public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", - bool recurse = false, PublishedValueFallbackPriority fallbackPriority = PublishedValueFallbackPriority.RecursiveTree, ICollection visitedLanguages = null) + int[] fallbackMethods = null, ICollection visitedLanguages = null) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 3937b5675c..18437f5235 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -76,7 +76,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, recurse: recursive); + var pval = publishedContent.Value(_fieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -96,7 +96,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, recurse: recursive); + var pval = publishedContent.Value(altFieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } From a35f67ecef25af5befcbe52c62df6471ac66d460 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 24 Jul 2018 13:32:29 +0200 Subject: [PATCH 023/124] Refactoring --- src/Umbraco.Core/Constants-Content.cs | 24 +++- .../IPublishedValueFallback.cs | 53 ++++++-- .../NoopPublishedValueFallback.cs | 16 ++- .../PublishedContentLanguageVariantTests.cs | 28 +---- .../PublishedContent/PublishedContentTests.cs | 6 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../PublishedValueFallback.cs | 116 +++++++++++------- .../PublishedValueLanguageFallback.cs | 90 -------------- src/Umbraco.Web/PublishedContentExtensions.cs | 20 ++- src/Umbraco.Web/PublishedElementExtensions.cs | 15 ++- ...nsion.cs => PublishedPropertyExtension.cs} | 4 +- .../Runtime/WebRuntimeComponent.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- src/Umbraco.Web/umbraco.presentation/item.cs | 4 +- 14 files changed, 168 insertions(+), 215 deletions(-) delete mode 100644 src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs rename src/Umbraco.Web/{PublishedContentPropertyExtension.cs => PublishedPropertyExtension.cs} (96%) diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs index 4b8c383e6f..b9d0691454 100644 --- a/src/Umbraco.Core/Constants-Content.cs +++ b/src/Umbraco.Core/Constants-Content.cs @@ -11,11 +11,27 @@ /// Defines core supported content fall-back options when retrieving content property values. /// Defined as constants rather than enum to allow solution or package defined fall-back methods. /// - public static class FallbackMethods + public static class ValueFallback { - public const int None = 0; - public const int RecursiveTree = 1; - public const int FallbackLanguage = 2; + /// + /// No fallback at all. + /// + public const int None = -1; + + /// + /// Default fallback. + /// + public const int Default = 0; + + /// + /// Recurse up the tree. + /// + public const int Recurse = 1; + + /// + /// Fallback to other languages. + /// + public const int Language = 2; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 59442e20bb..96a6c144fa 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -1,15 +1,44 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a fallback strategy for getting values. /// - // fixme - IPublishedValueFallback is still WorkInProgress - // todo - properly document methods, etc - // todo - understand caching vs fallback (recurse etc) public interface IPublishedValueFallback { + // fixme discussions & challenges + // + // - what's with visitedLanguage? should be internal to fallback implementation + // so that should be the case now, with latest changes + // + // - should be as simple as + // model.Value("price", fallback: ValueFallback.Language); + // model.Value("name", fallback: ValueFallback.Recurse); + // + // so chaining things through an array of ints is not... convenient + // it feels like ppl could have ValueFallback.LanguageAndRecurse or something? + // + // - the fallback: parameter value must be open, so about anything can be passed to the IPublishedValueFallback + // we have it now, it's an integer + constants, cool + // + // - we need to be able to configure (via code for now) a default fallback policy? + // not! the default value of the fallback: parameter is 'default', not 'none', and if people + // want to implement a different default behavior, they have to override the fallback provider + // + // - currently, no policies on IPublishedProperty nor IPublishedElement, but some may apply (language) + // todo: implement + // + // - general defaultValue discussion: + // when HasValue is false, the converter may return something, eg an empty enumerable, even though + // defaultValue is null, so should we respect defaultValue only when it is not 'default'? + // todo: when defaultValue==default, and HasValue is false, still return GetValue to ensure this + // + // - (and...) + // ModelsBuilder model.Value(x => x.Price, ...) extensions need to be adjusted too + // + // - cache & perfs + // soon as ppl implement custom fallbacks, caching is a problem, so better just not cache + // OTOH we need to implement the readonly thing for languages + /// /// Gets a fallback value for a property. /// @@ -25,7 +54,7 @@ namespace Umbraco.Core.Models.PublishedContent /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); /// /// Gets a fallback value for a property. @@ -43,7 +72,7 @@ namespace Umbraco.Core.Models.PublishedContent /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); /// /// Gets a fallback value for a published element property. @@ -59,7 +88,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); /// /// Gets a fallback value for a published element property. @@ -76,7 +105,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); /// /// Gets a fallback value for a published content property. @@ -92,7 +121,7 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// fixme explain & document priority + merge w/recurse? /// - object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); + object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a published content property. @@ -109,6 +138,6 @@ namespace Umbraco.Core.Models.PublishedContent /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// fixme explain & document priority + merge w/recurse? /// - T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages); + T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index a8d55176a3..9d74c4d8a2 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// /// Provides a noop implementation for . @@ -11,21 +9,21 @@ namespace Umbraco.Core.Models.PublishedContent public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) => defaultValue; + public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue; } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 7108824602..0b0f4dea51 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -167,7 +167,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "es", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "es", fallback: Core.Constants.Content.ValueFallback.Language); Assert.AreEqual("Welcome", value); } @@ -175,7 +175,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "it", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "it", fallback: Core.Constants.Content.ValueFallback.Language); Assert.AreEqual("Welcome", value); } @@ -183,7 +183,7 @@ namespace Umbraco.Tests.PublishedContent public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); - var value = content.Value("welcomeText", "no", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage }); + var value = content.Value("welcomeText", "no", fallback: Core.Constants.Content.ValueFallback.Language); Assert.IsNull(value); } @@ -199,27 +199,7 @@ namespace Umbraco.Tests.PublishedContent public void Can_Get_Content_Recursively() { var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText2", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree }); - Assert.AreEqual("Welcome", value); - } - - [Test] - public void Can_Get_Content_With_Recursive_Priority() - { - var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.RecursiveTree, Core.Constants.Content.FallbackMethods.FallbackLanguage }); - - // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. - Assert.AreEqual("Welkom", value); - } - - [Test] - public void Can_Get_Content_With_Fallback_Language_Priority() - { - var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); - var value = content.Value("welcomeText", "nl", fallbackMethods: new[] { Core.Constants.Content.FallbackMethods.FallbackLanguage, Core.Constants.Content.FallbackMethods.RecursiveTree }); - - // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + var value = content.Value("welcomeText2", fallback: Core.Constants.Content.ValueFallback.Recurse); Assert.AreEqual("Welcome", value); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 950bbf2283..93f4f3f242 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.PublishedContent Container.RegisterSingleton(f => new PublishedModelFactory(f.GetInstance().GetTypes())); Container.RegisterSingleton(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( @@ -336,8 +336,8 @@ namespace Umbraco.Tests.PublishedContent public void Get_Property_Value_Recursive() { var doc = GetNode(1174); - var rVal = doc.Value("testRecursive", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree } ); - var nullVal = doc.Value("DoNotFindThis", fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var rVal = doc.Value("testRecursive", fallback: Constants.Content.ValueFallback.Recurse); + var nullVal = doc.Value("DoNotFindThis", fallback: Constants.Content.ValueFallback.Recurse); Assert.AreEqual("This is the recursive val", rVal); Assert.AreEqual(null, nullVal); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 2f7fe8700b..5eea6bcf72 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.TestHelpers { base.Compose(); - Container.RegisterSingleton(); + Container.RegisterSingleton(); Container.RegisterSingleton(); } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 85b10be480..4136be40d8 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using ValueFallback = Umbraco.Core.Constants.Content.ValueFallback; namespace Umbraco.Web.Models.PublishedContent { @@ -9,90 +12,72 @@ namespace Umbraco.Web.Models.PublishedContent /// public class PublishedValueFallback : IPublishedValueFallback { - // kinda reproducing what was available in v7 + private readonly ILocalizationService _localizationService; + + /// + /// Initializes a new instance of the class. + /// + /// + public PublishedValueFallback(ILocalizationService localizationService) + { + _localizationService = localizationService; + } /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, ICollection visitedLanguages) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) { // no fallback here return defaultValue; } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, ICollection visitedLanguages) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) { // no fallback here return defaultValue; } /// - public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) + public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) { // is that ok? - return GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages); + return GetValue(content, alias, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages) + public virtual T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback) { - if (fallbackMethods == null) + switch (fallback) { - return defaultValue; - } - - foreach (var fallbackMethod in fallbackMethods) - { - if (TryGetValueWithFallbackMethod(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, fallbackMethod, out T value)) - { - return value; - } - } - - return defaultValue; - } - - protected virtual bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) - { - value = defaultValue; - switch (fallbackMethod) - { - case Core.Constants.Content.FallbackMethods.None: - return false; - case Core.Constants.Content.FallbackMethods.RecursiveTree: - return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Recurse: + return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value1) ? value1 : defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; default: - throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); + throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); } } - protected static bool TryGetValueWithRecursiveTree(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + // tries to get a value, recursing the tree + protected static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { - // Implement recursion as it was implemented in PublishedContentBase - - // fixme caching? - // - // all caches were using PublishedContentBase.GetProperty(alias, recurse) to get the property, - // then, - // NuCache.PublishedContent was storing the property in GetAppropriateCache() with key "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]"; - // XmlPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // DictionaryPublishedContent was storing the property in _cacheProvider with key $"XmlPublishedCache.PublishedMediaCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}"; - // - // at the moment, caching has been entirely removed, until we better understand caching + fallback - IPublishedProperty property = null; // if we are here, content's property has no value IPublishedProperty noValueProperty = null; do @@ -125,5 +110,44 @@ namespace Umbraco.Web.Models.PublishedContent value = defaultValue; return false; } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + // fixme + // _localizationService.GetXxx() is expensive, it deep clones objects + // we want _localizationService.GetReadOnlyXxx() returning IReadOnlyLanguage which cannot be saved back = no need to clone + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } + } + } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs deleted file mode 100644 index e94a8559a0..0000000000 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueLanguageFallback.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; - -namespace Umbraco.Web.Models.PublishedContent -{ - /// - /// Provides a default implementation for that allows - /// for use of fall-back languages - /// - /// - /// Inherits from that implments what was available in v7. - /// - public class PublishedValueLanguageFallback : PublishedValueFallback - { - private readonly ILocalizationService _localizationService; - - public PublishedValueLanguageFallback(ServiceContext services) - { - _localizationService = services.LocalizationService; - } - - protected override bool TryGetValueWithFallbackMethod(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, int fallbackMethod, out T value) - { - value = defaultValue; - switch (fallbackMethod) - { - case Core.Constants.Content.FallbackMethods.None: - return false; - case Core.Constants.Content.FallbackMethods.RecursiveTree: - return TryGetValueWithRecursiveTree(content, alias, culture, segment, defaultValue, out value); - case Core.Constants.Content.FallbackMethods.FallbackLanguage: - return TryGetValueWithFallbackLanguage(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages, out value); - default: - throw new NotSupportedException($"Fallback method with indentifying number {fallbackMethod} is not supported within {GetType().Name}."); - } - } - - private bool TryGetValueWithFallbackLanguage(IPublishedContent content, string alias, string culture, string segment, T defaultValue, IEnumerable fallbackMethods, ICollection visitedLanguages, out T value) - { - value = defaultValue; - if (string.IsNullOrEmpty(culture)) - { - return false; - } - - var language = _localizationService.GetLanguageByIsoCode(culture); - if (language.FallbackLanguageId.HasValue == false) - { - return false; - } - - if (AlreadyVisitedLanguage(visitedLanguages, language.FallbackLanguageId.Value)) - { - return false; - } - - visitedLanguages.Add(language.FallbackLanguageId.Value); - - var fallbackLanguage = GetLanguageById(language.FallbackLanguageId.Value); - value = content.Value(alias, fallbackLanguage.IsoCode, segment, defaultValue, fallbackMethods.ToArray(), visitedLanguages); - if (ValueIsNotNullEmptyOrDefault(value, defaultValue)) - { - return true; - } - - return false; - } - - private static bool AlreadyVisitedLanguage(ICollection visitedLanguages, int fallbackLanguageId) - { - return visitedLanguages.Contains(fallbackLanguageId); - } - - private ILanguage GetLanguageById(int id) - { - return _localizationService.GetLanguageById(id); - } - - private static bool ValueIsNotNullEmptyOrDefault(T value, T defaultValue) - { - return value != null && - string.IsNullOrEmpty(value.ToString()) == false && - value.Equals(defaultValue) == false; - } - } -} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 0bea71358a..64738aa923 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -153,18 +153,16 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// Options for fall-back if content not found. - /// A list of cultures already visited in looking for a value via a fall-back method. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static object Value(this IPublishedContent content, string alias, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion @@ -180,24 +178,20 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// Options for fall-back if content not found. - /// A list of cultures already visited in looking for a value via a fall-back method. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, converted to the specified type. - /// - public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static T Value(this IPublishedContent content, string alias, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallbackMethods, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } // fixme - .Value() refactoring - in progress - public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", - int[] fallbackMethods = null, ICollection visitedLanguages = null) + public static IHtmlString Value(this IPublishedContent content, string aliases, Func format, string alt = "", int fallback = 0) { var aliasesA = aliases.Split(','); if (aliasesA.Length == 0) diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index b0d2826df4..51f0b6c0dc 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -100,7 +100,6 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -108,14 +107,19 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); + // fixme defaultValue is a problem here + // assuming the value may return as an IEnumerable and no defaultValue is provided, then defaultValue is null + // and if HasValue is false, what we get is 'null' - but the converter may instead have been able to return an + // empty enumerable, which would be way nicer - so we need a way to tell that 'no defaultValue has been provided'? + + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion @@ -131,7 +135,6 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. - /// A list of cultures already visited in looking for a value via a fall-back method. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -139,14 +142,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); } #endregion diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs similarity index 96% rename from src/Umbraco.Web/PublishedContentPropertyExtension.cs rename to src/Umbraco.Web/PublishedPropertyExtension.cs index fdfd772ce7..bce13d30b4 100644 --- a/src/Umbraco.Web/PublishedContentPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -16,12 +16,12 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, ICollection visitedLanguages = null) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, visitedLanguages ?? new List()); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); } #endregion diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 78ddb935a0..03ba763527 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -199,7 +199,7 @@ namespace Umbraco.Web.Runtime composition.Container.Register(_ => GlobalHost.ConnectionManager.GetHubContext(), new PerContainerLifetime()); // register properties fallback - composition.Container.RegisterSingleton(); + composition.Container.RegisterSingleton(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index af2170123a..1d56ed5c7c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -263,7 +263,6 @@ - @@ -838,7 +837,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 14835b278f..d89733044d 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -78,7 +78,7 @@ namespace umbraco //check for published content and get its value using that if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) { - var pval = publishedContent.Value(_fieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var pval = publishedContent.Value(_fieldName, fallback: Constants.Content.ValueFallback.Recurse); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } @@ -98,7 +98,7 @@ namespace umbraco { if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) { - var pval = publishedContent.Value(altFieldName, fallbackMethods: new[] { Constants.Content.FallbackMethods.RecursiveTree }); + var pval = publishedContent.Value(altFieldName, fallback: Constants.Content.ValueFallback.Recurse); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } From 85bdc8f14d4f092c96a25de140ff1b0d9aa3c913 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 07:56:23 +0200 Subject: [PATCH 024/124] Updated XML documentation on IPublisedValueCallback methods --- .../Models/PublishedContent/IPublishedValueFallback.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 96a6c144fa..0c826e6bd5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -115,6 +115,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and @@ -132,6 +133,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and From b608ea832277d645664621ddec1f87211ecb282b Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 07:58:22 +0200 Subject: [PATCH 025/124] Restored service context as being DIed into PublishedValueFallback. Whilst only the LocalizationService service is needed, not injecting this via the service context was breaking the unit tests. If there's a way around this so we can use the mocked ILocalizationService in tests then this change can be reverted. --- .../Models/PublishedContent/PublishedValueFallback.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 4136be40d8..71bbe9577a 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -17,10 +17,10 @@ namespace Umbraco.Web.Models.PublishedContent /// /// Initializes a new instance of the class. /// - /// - public PublishedValueFallback(ILocalizationService localizationService) + /// + public PublishedValueFallback(ServiceContext serviceContext) { - _localizationService = localizationService; + _localizationService = serviceContext.LocalizationService; } /// From 49c6212cb63547f1530b8da931853ef938d5374e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 08:11:48 +0200 Subject: [PATCH 026/124] Implemented constants, tests and functionality for trying one fallback method and then another --- src/Umbraco.Core/Constants-Content.cs | 10 ++++ .../PublishedContentLanguageVariantTests.cs | 48 +++++++++++++++---- .../PublishedValueFallback.cs | 8 ++++ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs index b9d0691454..3f12ece6dc 100644 --- a/src/Umbraco.Core/Constants-Content.cs +++ b/src/Umbraco.Core/Constants-Content.cs @@ -32,6 +32,16 @@ /// Fallback to other languages. /// public const int Language = 2; + + /// + /// Recurse up the tree. If content not found, fallback to other languages. + /// + public const int RecurseThenLanguage = 3; + + /// + /// Fallback to other languages. If content not found, recurse up the tree. + /// + public const int LanguageThenRecurse = 4; } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 0b0f4dea51..d5e01fd424 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Moq; @@ -66,9 +67,9 @@ namespace Umbraco.Tests.PublishedContent var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props); var prop1 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText", - }; + { + Alias = "welcomeText", + }; prop1.SetSourceValue("en-US", "Welcome", true); prop1.SetValue("en-US", "Welcome", true); prop1.SetSourceValue("de", "Willkommen"); @@ -77,16 +78,16 @@ namespace Umbraco.Tests.PublishedContent prop1.SetValue("nl", "Welkom"); var prop2 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText2", - }; + { + Alias = "welcomeText2", + }; prop2.SetSourceValue("en-US", "Welcome", true); prop2.SetValue("en-US", "Welcome", true); var prop3 = new SolidPublishedPropertyWithLanguageVariants - { - Alias = "welcomeText", - }; + { + Alias = "welcomeText", + }; prop3.SetSourceValue("en-US", "Welcome", true); prop3.SetValue("en-US", "Welcome", true); @@ -202,5 +203,32 @@ namespace Umbraco.Tests.PublishedContent var value = content.Value("welcomeText2", fallback: Core.Constants.Content.ValueFallback.Recurse); Assert.AreEqual("Welcome", value); } + + [Test] + public void Can_Get_Content_With_Recursive_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.RecurseThenLanguage); + + // No Dutch value is directly assigned. Check has fallen back to Dutch value from parent. + Assert.AreEqual("Welkom", value); + } + + [Test] + public void Can_Get_Content_With_Fallback_Language_Priority() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.LanguageThenRecurse); + + // No Dutch value is directly assigned. Check has fallen back to English value from language variant. + Assert.AreEqual("Welcome", value); + } + + [Test] + public void Throws_For_Non_Supported_Fallback() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First(); + Assert.Throws(() => content.Value("welcomeText", "nl", fallback: 999)); + } } } diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 71bbe9577a..02e809025a 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -70,6 +70,14 @@ namespace Umbraco.Web.Models.PublishedContent return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value1) ? value1 : defaultValue; case ValueFallback.Language: return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + case ValueFallback.RecurseThenLanguage: + return TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value3) + ? value3 + : TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value4) ? value4 : defaultValue; + case ValueFallback.LanguageThenRecurse: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value5) + ? value5 + : TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value6) ? value6 : defaultValue; default: throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); } From c61f041d069ea8a1be9e939a47da8316eb6ec885 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 28 Jul 2018 08:39:02 +0200 Subject: [PATCH 027/124] Added fallback by language option to IPublishedElement and IPublishedProperty --- .../IPublishedValueFallback.cs | 12 +- .../NoopPublishedValueFallback.cs | 8 +- .../PublishedValueFallback.cs | 116 +++++++++++++++--- src/Umbraco.Web/PublishedElementExtensions.cs | 10 +- src/Umbraco.Web/PublishedPropertyExtension.cs | 8 +- 5 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 0c826e6bd5..c67df36c4f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -46,6 +46,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever property.Value(culture, segment, defaultValue) is called, and @@ -54,7 +55,7 @@ /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue); + object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a property. @@ -64,6 +65,7 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and @@ -72,7 +74,7 @@ /// At property level, property.GetValue() does *not* implement fallback, and one has to /// get property.Value() or property.Value{T}() to trigger fallback. /// - T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue); + T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback); /// /// Gets a fallback value for a published element property. @@ -82,13 +84,14 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue); + object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback); /// /// Gets a fallback value for a published element property. @@ -99,13 +102,14 @@ /// The requested culture. /// The requested segment. /// An optional default value. + /// Integer value defining method to use for fallback when content not found /// A fallback value, or null. /// /// This method is called whenever getting the property value for the specified alias, culture and /// segment, either returned no property at all, or a property with HasValue(culture, segment) being false. /// It can only fallback at element level (no recurse). /// - T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue); + T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback); /// /// Gets a fallback value for a published content property. diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs index 9d74c4d8a2..d920cefb24 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs @@ -9,16 +9,16 @@ public class NoopPublishedValueFallback : IPublishedValueFallback { /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback) => defaultValue; /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) => defaultValue; + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) => defaultValue; + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue; /// public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue; diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 02e809025a..bfcd339650 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -24,31 +24,45 @@ namespace Umbraco.Web.Models.PublishedContent } /// - public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue) + public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback) { - // no fallback here - return defaultValue; + return GetValue(property, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue) + public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback) { - // no fallback here - return defaultValue; + switch (fallback) + { + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(property, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + default: + throw NotSupportedFallbackMethod(fallback); + } } /// - public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue) + public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback) { - // no fallback here - return defaultValue; + return GetValue(content, alias, culture, segment, defaultValue, fallback); } /// - public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue) + public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback) { - // no fallback here - return defaultValue; + switch (fallback) + { + case ValueFallback.None: + case ValueFallback.Default: + return defaultValue; + case ValueFallback.Language: + return TryGetValueWithLanguageFallback(content, alias, culture, segment, defaultValue, out var value2) ? value2 : defaultValue; + default: + throw NotSupportedFallbackMethod(fallback); + } } /// @@ -79,12 +93,17 @@ namespace Umbraco.Web.Models.PublishedContent ? value5 : TryGetValueWithRecursiveFallback(content, alias, culture, segment, defaultValue, out var value6) ? value6 : defaultValue; default: - throw new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); + throw NotSupportedFallbackMethod(fallback); } } + private NotSupportedException NotSupportedFallbackMethod(int fallback) + { + return new NotSupportedException($"Fallback {GetType().Name} does not support policy code '{fallback}'."); + } + // tries to get a value, recursing the tree - protected static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) + private static bool TryGetValueWithRecursiveFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { IPublishedProperty property = null; // if we are here, content's property has no value IPublishedProperty noValueProperty = null; @@ -119,6 +138,74 @@ namespace Umbraco.Web.Models.PublishedContent return false; } + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (property.HasValue(culture2, segment)) + { + value = property.Value(culture2, segment); + return true; + } + + language = language2; + } + } + + // tries to get a value, falling back onto other languages + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string culture, string segment, T defaultValue, out T value) + { + value = defaultValue; + + if (culture.IsNullOrWhiteSpace()) return false; + + var visited = new HashSet(); + + var language = _localizationService.GetLanguageByIsoCode(culture); + if (language == null) return false; + + while (true) + { + if (language.FallbackLanguageId == null) return false; + + var language2Id = language.FallbackLanguageId.Value; + if (visited.Contains(language2Id)) return false; + visited.Add(language2Id); + + var language2 = _localizationService.GetLanguageById(language2Id); + if (language2 == null) return false; + var culture2 = language2.IsoCode; + + if (content.HasValue(alias, culture2, segment)) + { + value = content.Value(alias, culture2, segment); + return true; + } + + language = language2; + } + } + // tries to get a value, falling back onto other languages private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, T defaultValue, out T value) { @@ -156,6 +243,5 @@ namespace Umbraco.Web.Models.PublishedContent language = language2; } } - } } diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index 51f0b6c0dc..eea92f4c6c 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -100,6 +100,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, if it exists, otherwise a default value. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -107,7 +108,7 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedElement content, string alias, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); @@ -119,7 +120,7 @@ namespace Umbraco.Web // and if HasValue is false, what we get is 'null' - but the converter may instead have been able to return an // empty enumerable, which would be way nicer - so we need a way to tell that 'no defaultValue has been provided'? - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion @@ -135,6 +136,7 @@ namespace Umbraco.Web /// The variation language. /// The variation segment. /// The default value. + /// Optional fallback strategy. /// The value of the content's property identified by the alias, converted to the specified type. /// /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. @@ -142,14 +144,14 @@ namespace Umbraco.Web /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. /// The alias is case-insensitive. /// - public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default) + public static T Value(this IPublishedElement content, string alias, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { var property = content.GetProperty(alias); if (property != null && property.HasValue(culture, segment)) return property.Value(culture, segment); - return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(content, alias, culture, segment, defaultValue, fallback); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index bce13d30b4..6632076022 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -16,19 +16,19 @@ namespace Umbraco.Web #region Value - public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default) + public static object Value(this IPublishedProperty property, string culture = null, string segment = null, object defaultValue = default, int fallback = 0) { if (property.HasValue(culture, segment)) return property.GetValue(culture, segment); - return PublishedValueFallback.GetValue(property, culture, segment, defaultValue); + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, fallback); } #endregion #region Value - public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, ICollection visitedLanguages = null) + public static T Value(this IPublishedProperty property, string culture = null, string segment = null, T defaultValue = default, int fallback = 0) { // for Value when defaultValue is not specified, and HasValue() is false, we still want to convert the result (see below) // but we have no way to tell whether default value is specified or not - we could do it with overloads, but then defaultValue @@ -65,7 +65,7 @@ namespace Umbraco.Web //convert = source.TryConvertTo(); //if (convert.Success) return convert.Result; - return defaultValue; + return PublishedValueFallback.GetValue(property, culture, segment, defaultValue, fallback); } #endregion From 0cd3bb38ed00ce9ebcfb1b9396f199bb7d7107f0 Mon Sep 17 00:00:00 2001 From: RaduOrleanu Date: Fri, 17 Aug 2018 15:07:57 +0200 Subject: [PATCH 028/124] Found additional usages for (var web = new HttpClient()) and replaced with private static readonly HttpClient HttpClient = new HttpClient(); --- src/Umbraco.Web/Editors/HelpController.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs index 50e6547e36..8f981d9c88 100644 --- a/src/Umbraco.Web/Editors/HelpController.cs +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -7,20 +7,18 @@ using System.Threading.Tasks; namespace Umbraco.Web.Editors { public class HelpController : UmbracoAuthorizedJsonController - { + { + private static readonly HttpClient HttpClient = new HttpClient(); public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); - using (var web = new HttpClient()) - { - //fetch dashboard json and parse to JObject - var json = await web.GetStringAsync(url); - var result = JsonConvert.DeserializeObject>(json); - if (result != null) - return result; + //fetch dashboard json and parse to JObject + var json = await HttpClient.GetStringAsync(url); + var result = JsonConvert.DeserializeObject>(json); + if (result != null) + return result; - return new List(); - } + return new List(); } } From 051b595f827d20d4563a4ecbd80da0bbe4e34635 Mon Sep 17 00:00:00 2001 From: RaduOrleanu Date: Fri, 17 Aug 2018 15:18:08 +0200 Subject: [PATCH 029/124] Found additional usages for (var web = new HttpClient()) and replaced with private static readonly HttpClient HttpClient = new HttpClient(); --- src/Umbraco.Core/Services/PackagingService.cs | 7 ++-- .../Editors/DashboardController.cs | 35 ++++++++-------- src/Umbraco.Web/Install/InstallHelper.cs | 6 +-- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 40 +++++++++---------- 4 files changed, 42 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index dcef982992..3e1a09fd6c 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core.Services private Dictionary _importedContentTypes; private IPackageInstallation _packageInstallation; private readonly IUserService _userService; - + private static readonly HttpClient HttpClient = new HttpClient(); public PackagingService( ILogger logger, @@ -89,7 +89,6 @@ namespace Umbraco.Core.Services /// public string FetchPackageFile(Guid packageId, Version umbracoVersion, int userId) { - using (var httpClient = new HttpClient()) using (var uow = _uowProvider.GetUnitOfWork()) { //includeHidden = true because we don't care if it's hidden we want to get the file regardless @@ -97,7 +96,7 @@ namespace Umbraco.Core.Services byte[] bytes; try { - bytes = httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); + bytes = HttpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); } catch (HttpRequestException ex) { @@ -1746,7 +1745,7 @@ namespace Umbraco.Core.Services internal InstallationSummary InstallPackage(string packageFilePath, int userId = 0, bool raiseEvents = false) { - var metaData = GetPackageMetaData(packageFilePath); + var metaData = GetPackageMetaData(packageFilePath); if (raiseEvents) { diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 55286d99b4..19fbb2e59e 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -27,6 +27,8 @@ namespace Umbraco.Web.Editors [WebApi.UmbracoAuthorize] public class DashboardController : UmbracoApiController { + //we have just one instance of HttpClient shared for the entire application + private static readonly HttpClient HttpClient = new HttpClient(); //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") @@ -54,13 +56,10 @@ namespace Umbraco.Web.Editors //content is null, go get it try { - using (var web = new HttpClient()) - { - //fetch dashboard json and parse to JObject - var json = await web.GetStringAsync(url); - content = JObject.Parse(json); - result = content; - } + //fetch dashboard json and parse to JObject + var json = await HttpClient.GetStringAsync(url); + content = JObject.Parse(json); + result = content; ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); } @@ -93,17 +92,17 @@ namespace Umbraco.Web.Editors //content is null, go get it try { - using (var web = new HttpClient()) - { - //fetch remote css - content = await web.GetStringAsync(url); + // using (var web = new HttpClient()) + // { + //fetch remote css + content = await HttpClient.GetStringAsync(url); - //can't use content directly, modified closure problem - result = content; + //can't use content directly, modified closure problem + result = content; - //save server content for 30 mins - ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); - } + //save server content for 30 mins + ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + // } } catch (HttpRequestException ex) { @@ -119,12 +118,12 @@ namespace Umbraco.Web.Editors Content = new StringContent(result, Encoding.UTF8, "text/css") }; } - + [ValidateAngularAntiForgeryToken] public IEnumerable> GetDashboard(string section) { var dashboardHelper = new DashboardHelper(Services.SectionService); - return dashboardHelper.GetDashboard(section, Security.CurrentUser); + return dashboardHelper.GetDashboard(section, Security.CurrentUser); } } } diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 29cfe76ca2..5f3ca61b43 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -23,6 +23,7 @@ namespace Umbraco.Web.Install { internal class InstallHelper { + private static readonly HttpClient HttpClient = new HttpClient(); private readonly UmbracoContext _umbContext; private InstallationType? _installationType; @@ -199,8 +200,7 @@ namespace Umbraco.Web.Install UmbracoVersion.Current); using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri)) - using (var httpClient = new HttpClient()) - using (var response = httpClient.SendAsync(request).Result) + using (var response = HttpClient.SendAsync(request).Result) { packages = response.Content.ReadAsAsync>().Result.ToList(); } @@ -213,4 +213,4 @@ namespace Umbraco.Web.Install return packages; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index ad7fef5fd9..052142bd81 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -17,11 +17,12 @@ namespace Umbraco.Web.Scheduling internal class ScheduledTasks : RecurringTaskBase { + private static readonly HttpClient HttpClient = new HttpClient(); private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); - public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -61,28 +62,25 @@ namespace Umbraco.Web.Scheduling private async Task GetTaskByHttpAync(string url, CancellationToken token) { - using (var wc = new HttpClient()) + if (Uri.TryCreate(_appContext.UmbracoApplicationUrl, UriKind.Absolute, out var baseUri)) { - if (Uri.TryCreate(_appContext.UmbracoApplicationUrl, UriKind.Absolute, out var baseUri)) - { - wc.BaseAddress = baseUri; - } - var request = new HttpRequestMessage(HttpMethod.Get, url); - - //TODO: pass custom the authorization header, currently these aren't really secured! - //request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - - try - { - var result = await wc.SendAsync(request, token).ConfigureAwait(false); // ConfigureAwait(false) is recommended? http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - return result.StatusCode == HttpStatusCode.OK; - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling web task for url: " + url, ex); - } - return false; + HttpClient.BaseAddress = baseUri; } + var request = new HttpRequestMessage(HttpMethod.Get, url); + + //TODO: pass custom the authorization header, currently these aren't really secured! + //request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + + try + { + var result = await HttpClient.SendAsync(request, token).ConfigureAwait(false); // ConfigureAwait(false) is recommended? http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html + return result.StatusCode == HttpStatusCode.OK; + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); + } + return false; } public override async Task PerformRunAsync(CancellationToken token) From 03e642fced5a62e6ba3b0fff06bf68e4e1bc2ca8 Mon Sep 17 00:00:00 2001 From: RaduOrleanu Date: Fri, 17 Aug 2018 15:21:33 +0200 Subject: [PATCH 030/124] Found additional usages for (var web = new HttpClient()) and replaced with private static readonly HttpClient HttpClient = new HttpClient(); --- src/Umbraco.Web/Editors/DashboardController.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 19fbb2e59e..b9fcbf66e7 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -92,8 +92,6 @@ namespace Umbraco.Web.Editors //content is null, go get it try { - // using (var web = new HttpClient()) - // { //fetch remote css content = await HttpClient.GetStringAsync(url); @@ -102,7 +100,6 @@ namespace Umbraco.Web.Editors //save server content for 30 mins ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); - // } } catch (HttpRequestException ex) { From 07e5161988b8b00c71bca2c92bbee98bb8ed61fa Mon Sep 17 00:00:00 2001 From: RaduOrleanu Date: Sat, 18 Aug 2018 20:21:24 +0200 Subject: [PATCH 031/124] U4-11570 - changed WebClient() from created inside of using to check if null and then creating it --- .../Editors/CanvasDesignerController.cs | 15 ++++++++------- .../Install/InstallSteps/NewInstallStep.cs | 3 ++- .../EmbedProviders/AbstractOEmbedProvider.cs | 17 +++++++++-------- .../umbraco.presentation/keepAliveService.cs | 12 ++++++------ .../umbraco/dashboard/FeedProxy.aspx.cs | 16 ++++++++++------ .../businesslogic/Packager/Installer.cs | 3 ++- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web/Editors/CanvasDesignerController.cs b/src/Umbraco.Web/Editors/CanvasDesignerController.cs index 722167d18a..8284bf0b12 100644 --- a/src/Umbraco.Web/Editors/CanvasDesignerController.cs +++ b/src/Umbraco.Web/Editors/CanvasDesignerController.cs @@ -20,9 +20,10 @@ using Umbraco.Core.Services; namespace Umbraco.Web.Editors { - + public class CanvasDesignerController : UmbracoApiController { + private static WebClient _webClient; [HttpGet] public HttpResponseMessage GetGoogleFont() @@ -35,10 +36,10 @@ namespace Umbraco.Web.Editors var googleWebFontAPIURL = string.Format("https://www.googleapis.com/webfonts/v1/webfonts?key={0}", APIKey); var response = "{}"; - using (var client = new System.Net.WebClient()) - { - response = client.DownloadString(new Uri(googleWebFontAPIURL)); - } + if (_webClient == null) + _webClient = new WebClient(); + + response = _webClient.DownloadString(new Uri(googleWebFontAPIURL)); var resp = Request.CreateResponse(); resp.Content = new StringContent(response); @@ -59,7 +60,7 @@ namespace Umbraco.Web.Editors // Prepare string parameter result string[] paramLines = paramBlock.Trim().Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); - IList parameters = new List(); + IList parameters = new List(); foreach (var line in paramLines) { if (!line.Contains("@import")) @@ -121,4 +122,4 @@ namespace Umbraco.Web.Editors } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index dfcb441cce..0eaf3cc663 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; +using System.Net; using System.Web; using System.Web.Security; using Umbraco.Core; @@ -80,7 +81,7 @@ namespace Umbraco.Web.Install.InstallSteps { try { - var client = new System.Net.WebClient(); + var client = new WebClient(); var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email} }; client.UploadValues("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", values); } diff --git a/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs b/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs index 2f8ec85075..737b04fc83 100644 --- a/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs +++ b/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs @@ -12,18 +12,20 @@ namespace Umbraco.Web.Media.EmbedProviders { //TODO: Make all Http calls async - public abstract class AbstractOEmbedProvider: IEmbedProvider + public abstract class AbstractOEmbedProvider : IEmbedProvider { + private static WebClient _webClient; + public virtual bool SupportsDimensions { get { return true; } } [ProviderSetting] - public string APIEndpoint{ get;set; } + public string APIEndpoint { get; set; } [ProviderSetting] - public Dictionary RequestParams{ get;set; } + public Dictionary RequestParams { get; set; } public abstract string GetMarkup(string url, int maxWidth, int maxHeight); @@ -51,10 +53,9 @@ namespace Umbraco.Web.Media.EmbedProviders public virtual string DownloadResponse(string url) { - using (var webClient = new WebClient()) - { - return webClient.DownloadString(url); - } + if (_webClient == null) + _webClient = new WebClient(); + return _webClient.DownloadString(url); } public virtual T GetJsonResponse(string url) where T : class @@ -79,4 +80,4 @@ namespace Umbraco.Web.Media.EmbedProviders } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index abc3425f3f..192c35a233 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -10,8 +10,9 @@ namespace umbraco.presentation [Obsolete("This is no longer used and will be removed in future versions")] public class keepAliveService { + private static WebClient _webClient; //NOTE: sender will be the umbraco ApplicationContext - public static void PingUmbraco(object sender) + public static void PingUmbraco(object sender) { if (sender == null || !(sender is ApplicationContext)) return; @@ -21,10 +22,9 @@ namespace umbraco.presentation var url = appContext.UmbracoApplicationUrl + "/ping.aspx"; try { - using (var wc = new WebClient()) - { - wc.DownloadString(url); - } + if (_webClient == null) + _webClient = new WebClient(); + _webClient.DownloadString(url); } catch(Exception ee) { @@ -32,4 +32,4 @@ namespace umbraco.presentation } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs index b304cda0b6..488e4f06c2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Logging; +using System.Net.Http; +using Umbraco.Core.Logging; using Umbraco.Web; namespace dashboardUtilities @@ -14,6 +15,8 @@ namespace dashboardUtilities public partial class FeedProxy : UmbracoEnsuredPage { + private static WebClient _webClient; + protected void Page_Load(object sender, EventArgs e) { try @@ -31,9 +34,10 @@ namespace dashboardUtilities && feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null && requestUri.Port == 80) { - using (var client = new WebClient()) - { - var response = client.DownloadString(requestUri); + if (_webClient == null) + _webClient = new WebClient(); + + var response = _webClient.DownloadString(requestUri); if (string.IsNullOrEmpty(response) == false) { @@ -41,7 +45,7 @@ namespace dashboardUtilities Response.ContentType = Request.CleanForXss("type") ?? MediaTypeNames.Text.Xml; Response.Write(response); } - } + } else { @@ -57,4 +61,4 @@ namespace dashboardUtilities } } } -} \ No newline at end of file +} diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index a502ef5a32..eb46e82e8b 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -14,6 +14,7 @@ using umbraco.cms.businesslogic.web; using umbraco.BusinessLogic; using System.Diagnostics; using System.IO.Compression; +using System.Net; using umbraco.cms.businesslogic.template; using umbraco.interfaces; using Umbraco.Core.Events; @@ -685,7 +686,7 @@ namespace umbraco.cms.businesslogic.packager if (Directory.Exists(IOHelper.MapPath(SystemDirectories.Packages)) == false) Directory.CreateDirectory(IOHelper.MapPath(SystemDirectories.Packages)); - var wc = new System.Net.WebClient(); + var wc = new WebClient(); wc.DownloadFile( "http://" + PackageServer + "/fetch?package=" + Package.ToString(), From efbcce526154b32c97c21b5b7eb2a28fa9ba90b0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Aug 2018 10:41:50 +1000 Subject: [PATCH 032/124] Fixes SQL generation for paged results for both UserRepository and AuditRepository --- .../Repositories/AuditRepository.cs | 37 +++----- .../Repositories/UserRepository.cs | 93 +++++++++++-------- .../Repositories/VersionableRepositoryBase.cs | 5 +- .../Repositories/AuditRepositoryTest.cs | 90 +++++++++++++++--- .../Repositories/UserRepositoryTest.cs | 69 +++++++++++++- 5 files changed, 215 insertions(+), 79 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs index f7c8b0d3cd..e602358cf6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -47,43 +48,32 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (query == null) query = new Query(); + var queryHasWhereClause = query.GetWhereClauses().Any(); var translatorIds = new SqlTranslator(sql, query); var translatedQuery = translatorIds.Translate(); - var customFilterWheres = customFilter != null ? customFilter.GetWhereClauses().ToArray() : null; + var customFilterWheres = customFilter?.GetWhereClauses().ToArray(); var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; if (hasCustomFilter) { var filterSql = new Sql(); - var first = true; - foreach (var filterClaus in customFilterWheres) + foreach (var filterClause in customFilterWheres) { - if (first == false) - { - filterSql.Append(" AND "); - } - filterSql.Append(string.Format("({0})", filterClaus.Item1), filterClaus.Item2); - first = false; + filterSql.Append($"AND ({filterClause.Item1})", filterClause.Item2); } - translatedQuery = GetFilteredSqlForPagedResults(translatedQuery, filterSql); + translatedQuery = GetFilteredSqlForPagedResults(translatedQuery, filterSql, queryHasWhereClause); } if (auditTypeFilter.Length > 0) { var filterSql = new Sql(); - var first = true; - foreach (var filterClaus in auditTypeFilter) + foreach (var filterClause in auditTypeFilter) { - if (first == false || hasCustomFilter) - { - filterSql.Append(" AND "); - } - filterSql.Append("(logHeader = @logHeader)", new {logHeader = filterClaus.ToString() }); - first = false; + filterSql.Append("AND (logHeader = @logHeader)", new { logHeader = filterClause.ToString() }); } - translatedQuery = GetFilteredSqlForPagedResults(translatedQuery, filterSql); + translatedQuery = GetFilteredSqlForPagedResults(translatedQuery, filterSql, queryHasWhereClause || hasCustomFilter); } if (orderDirection == Direction.Descending) @@ -99,7 +89,7 @@ namespace Umbraco.Core.Persistence.Repositories dto => new AuditItem(dto.Id, dto.Comment, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId)).ToArray(); //Mapping the DateStamp - for (int i = 0; i < pages.Length; i++) + for (var i = 0; i < pages.Length; i++) { pages[i].CreateDate = pagedResult.Items[i].Datestamp; } @@ -169,14 +159,17 @@ namespace Umbraco.Core.Persistence.Repositories } #endregion - private Sql GetFilteredSqlForPagedResults(Sql sql, Sql filterSql) + private Sql GetFilteredSqlForPagedResults(Sql sql, Sql filterSql, bool hasWhereClause) { Sql filteredSql; // Apply filter if (filterSql != null) { - var sqlFilter = " WHERE " + filterSql.SQL.TrimStart("AND "); + //ensure we don't append a WHERE if there is already one + var sqlFilter = hasWhereClause + ? filterSql.SQL + : " WHERE " + filterSql.SQL.TrimStart("AND "); //NOTE: this is certainly strange - NPoco handles this much better but we need to re-create the sql // instance a couple of times to get the parameter order correct, for some reason the first diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 5701cd1008..82ad1fdcbf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Text.RegularExpressions; using System.Web.Security; using Newtonsoft.Json; using Umbraco.Core.Configuration; @@ -707,10 +708,10 @@ ORDER BY colName"; - private IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, - string[] includeUserGroups = null, - string[] excludeUserGroups = null, - UserState[] userState = null, + private IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, + string[] includeUserGroups = null, + string[] excludeUserGroups = null, + UserState[] userState = null, IQuery customFilter = null) { if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", "orderBy"); @@ -718,23 +719,23 @@ ORDER BY colName"; Sql filterSql = null; var customFilterWheres = customFilter != null ? customFilter.GetWhereClauses().ToArray() : null; - var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; + var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; if (hasCustomFilter - || (includeUserGroups != null && includeUserGroups.Length > 0) || (excludeUserGroups != null && excludeUserGroups.Length > 0) - || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) - filterSql = new Sql(); + || (includeUserGroups != null && includeUserGroups.Length > 0) || (excludeUserGroups != null && excludeUserGroups.Length > 0) + || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) + filterSql = new Sql(); if (hasCustomFilter) { foreach (var filterClause in customFilterWheres) { - filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); + filterSql.Append($"AND ({filterClause.Item1})", filterClause.Item2); } - } + } if (includeUserGroups != null && includeUserGroups.Length > 0) { - var subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id + const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId @@ -744,21 +745,21 @@ ORDER BY colName"; if (excludeUserGroups != null && excludeUserGroups.Length > 0) { - var subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id + const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; filterSql.Append(subQuery, new { userGroups = excludeUserGroups }); - } + } if (userState != null && userState.Length > 0) - { + { //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering if (userState.Contains(UserState.All) == false) { var sb = new StringBuilder("("); - var appended = false; + var appended = false; if (userState.Contains(UserState.Active)) { @@ -788,67 +789,81 @@ ORDER BY colName"; filterSql.Append("AND " + sb); } - } - - // Get base query for returning IDs - var sqlBaseIds = GetBaseQuery("id"); - + } + + // Get base query for returning IDs + var sqlBaseIds = GetBaseQuery("id"); + if (query == null) query = new Query(); + var queryHasWhereClause = query.GetWhereClauses().Any(); var translatorIds = new SqlTranslator(sqlBaseIds, query); - var sqlQueryIds = translatorIds.Translate(); - - //get sorted and filtered sql + var sqlQueryIds = translatorIds.Translate(); + var sqlBaseFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*"); + var translatorFull = new SqlTranslator(sqlBaseFull, query); + + //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlQueryIds, filterSql), + GetFilteredSqlForPagedResults(sqlQueryIds, filterSql, queryHasWhereClause), orderDirection, orderBy); // Get page of results and total count var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); - - //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in - // the pageResult. + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // the pageResult. if (pagedResult.Items.Any()) { //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); + + var sqlQueryFull = translatorFull.Translate(); - var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*"); - - var fullQueryWithPagedInnerJoin = sqlQueryFull - .Append("INNER JOIN (") - //join the paged query with the paged query arguments + //We need to make this FULL query an inner join on the paged ID query + var splitQuery = sqlQueryFull.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) + .Append("INNER JOIN (") + //join the paged query with the paged query arguments .Append(sqlStringPage, args) .Append(") temp ") .Append("ON umbracoUser.id = temp.id"); AddGroupLeftJoin(fullQueryWithPagedInnerJoin); + if (splitQuery.Length > 1) + { + //add the original where clause back with the original arguments + fullQueryWithPagedInnerJoin.Where(splitQuery[1], sqlQueryIds.Arguments); + } + //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, filterSql), + GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, filterSql, queryHasWhereClause), orderDirection, orderBy); var users = ConvertFromDtos(Database.Fetch(new UserGroupRelator().Map, fullQuery)) .ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching. return users; - } + } return Enumerable.Empty(); } - private Sql GetFilteredSqlForPagedResults(Sql sql, Sql filterSql) + private Sql GetFilteredSqlForPagedResults(Sql sql, Sql filterSql, bool hasWhereClause) { Sql filteredSql; // Apply filter if (filterSql != null) { - var sqlFilter = " WHERE " + filterSql.SQL.TrimStart("AND "); + //ensure we don't append a WHERE if there is already one + var sqlFilter = hasWhereClause + ? filterSql.SQL + : " WHERE " + filterSql.SQL.TrimStart("AND "); //NOTE: this is certainly strange - NPoco handles this much better but we need to re-create the sql // instance a couple of times to get the parameter order correct, for some reason the first @@ -915,4 +930,4 @@ ORDER BY colName"; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 883b05b2fd..aa7566a4c3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -290,6 +290,9 @@ namespace Umbraco.Core.Persistence.Repositories // Apply filter if (defaultFilter != null) { + //NOTE: It is assumed here that the `sql` already contains a WHERE clause, see UserRepository.GetFilteredSqlForPagedResults + // for an example of when it's not assumed there's already a WHERE clause + var filterResult = defaultFilter(); //NOTE: this is certainly strange - NPoco handles this much better but we need to re-create the sql @@ -993,4 +996,4 @@ ORDER BY contentNodeId, versionId, propertytypeid } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs index 04677c208b..e6f8f44019 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs @@ -66,6 +66,46 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Get_Paged_Items_By_User_Id_With_Query_And_Filter() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repo = new AuditRepository(unitOfWork, CacheHelper, Logger, SqlSyntax)) + { + for (int i = 0; i < 100; i++) + { + repo.AddOrUpdate(new AuditItem(i, string.Format("Content {0} created", i), AuditType.New, 0)); + repo.AddOrUpdate(new AuditItem(i, string.Format("Content {0} published", i), AuditType.Publish, 0)); + } + unitOfWork.Commit(); + } + + using (var repo = new AuditRepository(unitOfWork, CacheHelper, Logger, SqlSyntax)) + { + var query = Query.Builder.Where(x => x.UserId == 0); + + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + + var page = repo.GetPagedResultsByQuery(query, 0, 10, out var total, Direction.Descending, + new[] { AuditType.Publish }, + Query.Builder.Where(x => x.UserId > -1)); + + Assert.AreEqual(10, page.Count()); + Assert.AreEqual(100, total); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } + } + } + + [Test] public void Get_Paged_Items_With_AuditType_Filter() { @@ -83,14 +123,24 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repo = new AuditRepository(unitOfWork, CacheHelper, Logger, SqlSyntax)) { - long total; - var page = repo.GetPagedResultsByQuery(Query.Builder, 0, 9, out total, Direction.Descending, - new[] {AuditType.Publish}, null) - .ToArray(); + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); - Assert.AreEqual(9, page.Length); - Assert.IsTrue(page.All(x => x.AuditType == AuditType.Publish)); - Assert.AreEqual(100, total); + var page = repo.GetPagedResultsByQuery(Query.Builder, 0, 9, out var total, Direction.Descending, + new[] { AuditType.Publish }, null) + .ToArray(); + + Assert.AreEqual(9, page.Length); + Assert.IsTrue(page.All(x => x.AuditType == AuditType.Publish)); + Assert.AreEqual(100, total); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } } } @@ -111,15 +161,25 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repo = new AuditRepository(unitOfWork, CacheHelper, Logger, SqlSyntax)) { - long total; - var page = repo.GetPagedResultsByQuery(Query.Builder, 0, 8, out total, Direction.Descending, - null, Query.Builder.Where(item => item.Comment == "Content created")) - .ToArray(); + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); - Assert.AreEqual(8, page.Length); - Assert.IsTrue(page.All(x => x.Comment == "Content created")); - Assert.AreEqual(100, total); + var page = repo.GetPagedResultsByQuery(Query.Builder, 0, 8, out var total, Direction.Descending, + null, Query.Builder.Where(item => item.Comment == "Content created")) + .ToArray(); + + Assert.AreEqual(8, page.Length); + Assert.IsTrue(page.All(x => x.Comment == "Content created")); + Assert.AreEqual(100, total); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 334c4f98c2..3fa490e742 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -392,7 +393,71 @@ namespace Umbraco.Tests.Persistence.Repositories var result = repository.Count(query); // Assert - Assert.That(result, Is.GreaterThanOrEqualTo(2)); + Assert.AreEqual(2, result); + } + } + + [Test] + public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var users = CreateAndCommitMultipleUsers(repository, unitOfWork); + var query = Query.Builder.Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + + // Act + var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: Query.Builder.Where(x => x.Id > -1)); + + // Assert + Assert.AreEqual(2, totalRecs); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } + } + } + + [Test] + public void Can_Get_Paged_Results_With_Filter_And_Groups() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var users = CreateAndCommitMultipleUsers(repository, unitOfWork); + + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + + // Act + var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: Query.Builder.Where(x => x.Id == 0)); + + // Assert + Assert.AreEqual(1, totalRecs); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } } } @@ -439,4 +504,4 @@ namespace Umbraco.Tests.Persistence.Repositories return new IUser[] { user1, user2, user3 }; } } -} \ No newline at end of file +} From 412069414b0d74bc38125b8022427d1a42a22c12 Mon Sep 17 00:00:00 2001 From: RaduOrleanu Date: Wed, 29 Aug 2018 12:39:06 +0200 Subject: [PATCH 033/124] U4-11570 - performed instantiation locally in classes where WebClient or HttpClient is used only in 1 method and added null check --- src/Umbraco.Core/Services/PackagingService.cs | 8 ++++++-- src/Umbraco.Web/Install/InstallHelper.cs | 9 ++++++--- src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs | 8 ++++++-- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 10 +++++++--- src/umbraco.cms/businesslogic/Packager/Installer.cs | 8 ++++++-- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 3e1a09fd6c..f8fd604097 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core.Services private Dictionary _importedContentTypes; private IPackageInstallation _packageInstallation; private readonly IUserService _userService; - private static readonly HttpClient HttpClient = new HttpClient(); + private static HttpClient _httpClient; public PackagingService( ILogger logger, @@ -96,7 +96,11 @@ namespace Umbraco.Core.Services byte[] bytes; try { - bytes = HttpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } + bytes = _httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); } catch (HttpRequestException ex) { diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 5f3ca61b43..145395e235 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Install { internal class InstallHelper { - private static readonly HttpClient HttpClient = new HttpClient(); + private static HttpClient _httpClient; private readonly UmbracoContext _umbContext; private InstallationType? _installationType; @@ -192,15 +192,18 @@ namespace Umbraco.Web.Install internal IEnumerable GetStarterKits() { + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } var packages = new List(); - try { var requestUri = string.Format("https://our.umbraco.com/webapi/StarterKit/Get/?umbracoVersion={0}", UmbracoVersion.Current); using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri)) - using (var response = HttpClient.SendAsync(request).Result) + using (var response = _httpClient.SendAsync(request).Result) { packages = response.Content.ReadAsAsync>().Result.ToList(); } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 0eaf3cc663..4dfa793675 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web.Install.InstallSteps { private readonly HttpContextBase _http; private readonly ApplicationContext _applicationContext; + private static WebClient _webClient; public NewInstallStep(HttpContextBase http, ApplicationContext applicationContext) { @@ -81,9 +82,12 @@ namespace Umbraco.Web.Install.InstallSteps { try { - var client = new WebClient(); + if (_webClient == null) + { + _webClient = new WebClient(); + } var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email} }; - client.UploadValues("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", values); + _webClient.UploadValues("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", values); } catch { /* fail in silence */ } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 052142bd81..a8a25fe1fd 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Scheduling internal class ScheduledTasks : RecurringTaskBase { - private static readonly HttpClient HttpClient = new HttpClient(); + private static HttpClient _httpClient; private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); @@ -62,9 +62,13 @@ namespace Umbraco.Web.Scheduling private async Task GetTaskByHttpAync(string url, CancellationToken token) { + if (_httpClient == null) + { + _httpClient = new HttpClient(); + } if (Uri.TryCreate(_appContext.UmbracoApplicationUrl, UriKind.Absolute, out var baseUri)) { - HttpClient.BaseAddress = baseUri; + _httpClient.BaseAddress = baseUri; } var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -73,7 +77,7 @@ namespace Umbraco.Web.Scheduling try { - var result = await HttpClient.SendAsync(request, token).ConfigureAwait(false); // ConfigureAwait(false) is recommended? http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html + var result = await _httpClient.SendAsync(request, token).ConfigureAwait(false); // ConfigureAwait(false) is recommended? http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html return result.StatusCode == HttpStatusCode.OK; } catch (Exception ex) diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index eb46e82e8b..f3733d5e5a 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -47,6 +47,7 @@ namespace umbraco.cms.businesslogic.packager private readonly List _binaryFileErrors = new List(); private int _currentUserId = -1; + private static WebClient _webClient; public string Name { get; private set; } @@ -686,9 +687,12 @@ namespace umbraco.cms.businesslogic.packager if (Directory.Exists(IOHelper.MapPath(SystemDirectories.Packages)) == false) Directory.CreateDirectory(IOHelper.MapPath(SystemDirectories.Packages)); - var wc = new WebClient(); + if (_webClient == null) + { + _webClient = new WebClient(); + } - wc.DownloadFile( + _webClient.DownloadFile( "http://" + PackageServer + "/fetch?package=" + Package.ToString(), IOHelper.MapPath(SystemDirectories.Packages + "/" + Package + ".umb")); From c95cc14703cf519283096416087263a8c09a5ffc Mon Sep 17 00:00:00 2001 From: Marc Goodson Date: Sat, 1 Sep 2018 11:57:05 +0100 Subject: [PATCH 034/124] Make the file extension for files visible when picking a file using the media picker --- .../src/less/components/umb-media-grid.less | 13 +++++++++++++ .../src/less/property-editors.less | 19 ++++++++++++++++++- .../src/views/components/umb-media-grid.html | 6 ++++-- .../mediapicker/mediapicker.html | 5 ++++- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 79e859b69a..3a9d612c4d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -33,6 +33,19 @@ background-color: @gray-10; } +.umb-media-grid__item-file-icon > span { + color: @white; + background: @gray-4; + padding: 1px 3px; + font-size: 10px; + line-height: 130%; + display: block; + margin-top: -30px; + margin-left: -10px; + /*Need to reset the opacity?*/ + opacity: 0.99; +} + .umb-media-grid__item.-selected { box-shadow: 0 2px 8px 0 rgba(0,0,0,.35); } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 1855a3a6b0..fabe7c2291 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -363,13 +363,30 @@ ul.color-picker li { text-align: center; } -.umb-sortable-thumbnails .umb-icon-holder .icon{ +.umb-sortable-thumbnails .umb-icon-holder .icon { font-size: 40px; line-height: 50px; color: @gray-3; display: block; } +.umb-sortable-thumbnails .umb-icon-holder .file-icon > span { + color: @white; + background: @gray-4; + padding: 1px 3px; + font-size: 10px; + line-height: 130%; + display: block; + margin-top: -30px; + width: 2em; +} + +.umb-sortable-thumbnails .umb-icon-holder .file-icon + small { + display: block; + margin-top: 1em; +} + + .umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper { width: 124px; height: 124px; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 9df33e7e0e..bad1ccc1ce 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -21,8 +21,10 @@ {{item.name}} - - + + + .{{item.extension}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index fd28929616..2efed8b3e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -17,7 +17,10 @@ - + + + .{{image.extension}} + {{image.name}} From ef3abea005989db831eee20d6aedb67ea7b6fe61 Mon Sep 17 00:00:00 2001 From: Kim Holzmann Date: Sat, 1 Sep 2018 12:57:32 +0200 Subject: [PATCH 035/124] U4-8824 Selection in media grid doesn't match when filter has value. --- .../listview/layouts/grid/grid.html | 2 +- .../listview/listview.controller.js | 27 ++++++------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index cf1c3f6411..29dd1b870f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -82,7 +82,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 8f9279fb02..cb42646bd5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -213,10 +213,6 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }, 500); - if (reload === true) { - $scope.reloadView($scope.contentId, true); - } - if (err.data && angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); @@ -250,12 +246,12 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie with simple values */ $scope.getContent = function() { - $scope.reloadView($scope.contentId, true); + $scope.reloadView($scope.contentId); } - $scope.reloadView = function (id, reloadFolders) { - + $scope.reloadView = function (id) { $scope.viewLoaded = false; + $scope.folders = []; listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); @@ -268,20 +264,13 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie if ($scope.listViewResultSet.items) { _.each($scope.listViewResultSet.items, function (e, index) { setPropertyValues(e); + if (e.contentTypeAlias === 'Folder') { + $scope.folders.push(e); + } }); } - if (reloadFolders && $scope.entityType === 'media') { - //The folders aren't loaded - we only need to do this once since we're never changing node ids - mediaResource.getChildFolders($scope.contentId) - .then(function (folders) { - $scope.folders = folders; - $scope.viewLoaded = true; - }); - - } else { - $scope.viewLoaded = true; - } + $scope.viewLoaded = true; //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last @@ -620,7 +609,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.options.allowBulkMove || $scope.options.allowBulkDelete; - $scope.reloadView($scope.contentId, true); + $scope.reloadView($scope.contentId); } function getLocalizedKey(alias) { From c6a31b30f155da2355e5eb49b6c8d249ad1f5cc5 Mon Sep 17 00:00:00 2001 From: Marc Goodson Date: Sat, 1 Sep 2018 19:27:55 +0100 Subject: [PATCH 036/124] position relative to stack the item above to avoid opacity issue --- .../src/less/components/umb-media-grid.less | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 3a9d612c4d..21684c624b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -42,8 +42,7 @@ display: block; margin-top: -30px; margin-left: -10px; - /*Need to reset the opacity?*/ - opacity: 0.99; + position: relative; } .umb-media-grid__item.-selected { From 1437af62b36c5c302bd40863e6015ebd6744e0c5 Mon Sep 17 00:00:00 2001 From: villian Date: Mon, 3 Sep 2018 17:39:17 +0300 Subject: [PATCH 037/124] fix U4-8143 --- .../propertyeditors/contentpicker/contentpicker.controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 76d6fa77aa..c1fe21b1b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -35,6 +35,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper return $scope.model.config.idType === "udi" ? i.udi : i.id; }); $scope.model.value = trim(currIds.join(), ","); + angularHelper.getCurrentForm($scope).$setDirty(); //Validate! if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) { @@ -185,7 +186,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper if (angular.isArray(model.selection)) { _.each(model.selection, function (item, i) { $scope.add(item); - }); + }); + angularHelper.getCurrentForm($scope).$setDirty(); } $scope.contentPickerOverlay.show = false; From cbdfb6f25f5dd60b7a78858aab1d11b3ca29c96e Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 3 Sep 2018 17:32:39 +0200 Subject: [PATCH 038/124] Hide "Created Date" field when content hasn't yet been created --- .../src/views/components/content/umb-content-node-info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 8584b30177..62d6004a95 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -171,7 +171,7 @@ - + {{node.createDateFormatted}} by {{ node.owner.name }} From c8664a886abb2f7ef5126fab701a70d3320758ba Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Tue, 4 Sep 2018 14:27:58 +0200 Subject: [PATCH 039/124] ID property is now hidden as well when creating new content --- .../src/views/components/content/umb-content-node-info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 62d6004a95..7e80aa2fca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -201,7 +201,7 @@ - +
{{ node.id }}
{{ node.key }}
From 5986899b5edf30023624e6d4c91c939cbc1619ee Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 6 Sep 2018 11:06:12 +0200 Subject: [PATCH 040/124] Fixes #U4-10383 Feature request - Ignore casing when getting member email and username from IPublishedContent --- src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs b/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs index 35b3d6ee62..f914c817ce 100644 --- a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs @@ -129,11 +129,11 @@ namespace Umbraco.Web.PublishedCache public override IPublishedProperty GetProperty(string alias) { - switch (alias) + switch (alias.ToLowerInvariant()) { - case "Email": + case "email": return new PropertyResult("Email", Email, PropertyResultType.CustomProperty); - case "UserName": + case "username": return new PropertyResult("UserName", UserName, PropertyResultType.CustomProperty); } From 68d083249baa95efd5f7090ae8f3837246d7ee7c Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 6 Sep 2018 16:09:31 +0200 Subject: [PATCH 041/124] Fix merge --- src/Umbraco.Web/Editors/ContentController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 09126b80ee..d9ef3b5ed5 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -860,7 +860,7 @@ namespace Umbraco.Web.Editors var canPublish = true; //validate any mandatory variants that are not in the list - var mandatoryLangs = Mapper.Map, IEnumerable>(_allLangs.Value.Values).Where(x => x.Mandatory); + var mandatoryLangs = Mapper.Map, IEnumerable>(_allLangs.Value.Values).Where(x => x.IsMandatory); foreach (var lang in mandatoryLangs) { From 8ff671a7c67a59bc8621e0c19cd822e58f6d8340 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 7 Sep 2018 13:10:55 +1000 Subject: [PATCH 042/124] Changes compilation batch mode to true uses Roslyn compiler for views --- build/NuSpecs/UmbracoCms.nuspec | 1 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 15 + src/Umbraco.Web.UI/packages.config | 2 + src/Umbraco.Web.UI/web.Template.Debug.config | 2 - src/Umbraco.Web.UI/web.Template.config | 815 ++++++++++--------- 5 files changed, 429 insertions(+), 406 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index d3ab992a23..e8bf606dc3 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -20,6 +20,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c07cd295f9..e3ea93b98e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,5 +1,7 @@  + + 9.0.30729 @@ -49,6 +51,9 @@ true true + + + bin\ @@ -158,6 +163,9 @@ ..\packages\Microsoft.CodeAnalysis.CSharp.1.0.0\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + + ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.2\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll @@ -1085,4 +1093,11 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 8a4b77ca3a..677d5da297 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -22,7 +22,9 @@ + + diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 014fe14329..741d4d192d 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -318,8 +318,6 @@ - - diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index b4f046b277..10794e6aa2 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -1,456 +1,463 @@ - -
-
-
-
-
+ +
+
+
+
+
- -
-
-
-
-
- + +
+
+
+
+
+ - -
-
-
- - + +
+
+
+ + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + - - + + - - + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + - - - - - + - - - - - - - - - + + - - - - - - - - - - - - - + + + + + + + + + - - - - - + + + + + + + + + + + + + - - - - + + + + + - - - - + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - From da88cc2d04fef1a617f364eb9635bf791b0583fd Mon Sep 17 00:00:00 2001 From: "Ali.Taheri" Date: Fri, 7 Sep 2018 11:04:35 +0100 Subject: [PATCH 043/124] temp-2939 - Removed hardcoded number of rows from textarea with the default value of 10. --- .../src/views/propertyeditors/textarea/textarea.html | 4 ++-- src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index ffb692ced0..51938bd676 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -1,9 +1,9 @@
- + Required
{{model.count}} characters left
-
\ No newline at end of file + diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index 659b1b3224..f4273303b4 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -20,6 +20,9 @@ namespace Umbraco.Web.PropertyEditors { [PreValueField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] public bool MaxChars { get; set; } + + [PreValueField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] + public bool Rows { get; set; } } } } From 010a2e012e32f0814609047c663b3f866d38a691 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Fri, 7 Sep 2018 13:54:20 +0100 Subject: [PATCH 044/124] Clean up multiple textstring(repeatable textstring) view - (#2934) * Clean up multiple textstring(repeatable textstring) view - Introduced some styling rules to make the view look cleaner * Removes unnecessary additional included files from csproj --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../src/less/components/umb-multiple-textbox.less | 7 +++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index a391e16635..2eaabe7842 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -126,6 +126,7 @@ @import "components/umb-querybuilder.less"; @import "components/umb-pagination.less"; @import "components/umb-mini-list-view.less"; +@import "components/umb-multiple-textbox.less"; @import "components/umb-badge.less"; @import "components/umb-nested-content.less"; @import "components/umb-checkmark.less"; 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 new file mode 100644 index 0000000000..f4001d2d91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less @@ -0,0 +1,7 @@ +.umb-multiple-textbox .textbox-wrapper{ + padding: 5px 0; +} + +.umb-multiple-textbox .textbox-wrapper:last-child { + padding: 5px 0 10px; +} diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c07cd295f9..b5e022fa8e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1085,4 +1085,4 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" - \ No newline at end of file + From e05a55c5329af18071c49c785471e8a5da3c0bdb Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 7 Sep 2018 15:20:29 +0200 Subject: [PATCH 045/124] Fixes incorrectly named method and removes extraneous spaces --- src/Umbraco.Core/StringExtensions.cs | 12 ++++++------ src/Umbraco.Web/Editors/MediaController.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 835b333813..c6b4da96eb 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1574,29 +1574,29 @@ namespace Umbraco.Core guid[left] = guid[right]; guid[right] = temp; } - - + /// /// Converts a file name to a friendly name for a content item /// /// /// - public static string friendlyNameFromFilename(this string fileName) + public static string ToFriendlyName(this string fileName) { // strip the file extension - fileName = StripFileExtension(fileName); + fileName = fileName.StripFileExtension(); // underscores and dashes to spaces - fileName = ReplaceMany(fileName, new char[] { '_', '-' }, ' '); + fileName = fileName.ReplaceMany(new[] { '_', '-' }, ' '); // any other conversions ? // Pascalcase (to be done last) fileName = System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(fileName); + // Replace multiple consecutive spaces with a single space + fileName = string.Join(" ", fileName.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); return fileName; } - } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index eb276664e6..59693e94a2 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -717,7 +717,7 @@ namespace Umbraco.Web.Editors mediaType = result.FormData["contentTypeAlias"]; } - var mediaItemName = fileName.friendlyNameFromFilename(); + var mediaItemName = fileName.ToFriendlyName(); var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); From b2133aace1ee18b5cfe5df669ebf8ed47a2c38dd Mon Sep 17 00:00:00 2001 From: Chris Houston Date: Sat, 8 Sep 2018 23:28:44 -0400 Subject: [PATCH 046/124] Fixing issue #2910 --- src/Umbraco.Web.UI.Client/src/less/components/umb-table.less | 2 +- src/Umbraco.Web.UI.Client/src/views/components/umb-table.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 7679fd3c24..2c1fc4a701 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -165,7 +165,7 @@ input.umb-table__input { } -.-content .-unpublished { +.-content :not(.with-unpublished-version).-unpublished { .umb-table__name > * { opacity: .4; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 8c5c7fcc4a..b5f4c39a8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -32,7 +32,8 @@ ng-class="{ '-selected':item.selected, '-published':item.published, - '-unpublished':!item.published + '-unpublished':!item.published, + 'with-unpublished-version':!item.published && item.hasPublishedVersion }" ng-click="selectItem(item, $index, $event)"> From 91ebc649d47574d05a71880c7cd59a816e30fd01 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 9 Sep 2018 16:35:48 +0200 Subject: [PATCH 047/124] Re-apply missing changes from #2446 --- .../less/components/umb-multiple-textbox.less | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) 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 f4001d2d91..21f59a3e2d 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,7 +1,34 @@ -.umb-multiple-textbox .textbox-wrapper{ - padding: 5px 0; +.umb-multiple-textbox .textbox-wrapper { + align-items: center; + margin-bottom: 15px; } -.umb-multiple-textbox .textbox-wrapper:last-child { - padding: 5px 0 10px; +.umb-multiple-textbox .textbox-wrapper .umb-editor { + margin-bottom: 0; +} + +.umb-multiple-textbox .textbox-wrapper i { + margin-right: 5px; +} + +.umb-multiple-textbox .textbox-wrapper i.handle { + margin-left: 5px; +} + +.umb-multiple-textbox .textbox-wrapper a.remove { + margin-left: 5px; + text-decoration: none; +} + +.umb-multiple-textbox .add-link { + &:extend(.umb-node-preview-add); +} + +.umb-editor-wrapper .umb-multiple-textbox .add-link { + &:extend(.umb-editor-wrapper .umb-node-preview); +} + +.umb-modal .umb-multiple-textbox .textbox-wrapper .umb-editor { + flex: 1 1 auto; + width: auto; } From 2f74720c38e734f359cdd3e9a7556860fb96801f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 10 Sep 2018 11:12:46 +1000 Subject: [PATCH 048/124] updates wording --- .../Services/ServerRegistrationService.cs | 4 ++-- src/Umbraco.Core/Sync/ServerRole.cs | 4 ++-- src/Umbraco.Tests/ApplicationUrlHelperTests.cs | 6 +++--- src/Umbraco.Web/Mvc/MasterControllerFactory.cs | 12 ++++++------ .../Routing/RedirectTrackingEventHandler.cs | 8 ++++---- src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs | 4 ++-- src/Umbraco.Web/Scheduling/KeepAlive.cs | 2 +- src/Umbraco.Web/Scheduling/LogScrubber.cs | 4 ++-- src/Umbraco.Web/Scheduling/ScheduledPublishing.cs | 4 ++-- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 2 +- src/Umbraco.Web/Scheduling/Scheduler.cs | 4 ++-- 11 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index 6f311019af..cdd06d6c3e 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -77,7 +77,7 @@ namespace Umbraco.Core.Services regs = xr.Repository.GetAll().ToArray(); // default role is single server, but if registrations contain more - // than one active server, then role is master or slave + // than one active server, then role is master or replica _currentServerRole = regs.Count(x => x.IsActive) > 1 ? (server.IsMaster ? ServerRole.Master : ServerRole.Slave) : ServerRole.Single; @@ -175,4 +175,4 @@ namespace Umbraco.Core.Services return _currentServerRole; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Sync/ServerRole.cs b/src/Umbraco.Core/Sync/ServerRole.cs index cbc121c4bc..11f8811771 100644 --- a/src/Umbraco.Core/Sync/ServerRole.cs +++ b/src/Umbraco.Core/Sync/ServerRole.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Sync Single = 1, /// - /// In a multi-servers environment, the server is a slave server. + /// In a multi-servers environment, the server is a replica server. /// Slave = 2, @@ -25,4 +25,4 @@ namespace Umbraco.Core.Sync /// Master = 3 } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/ApplicationUrlHelperTests.cs b/src/Umbraco.Tests/ApplicationUrlHelperTests.cs index 578a569104..0eb4f53ac2 100644 --- a/src/Umbraco.Tests/ApplicationUrlHelperTests.cs +++ b/src/Umbraco.Tests/ApplicationUrlHelperTests.cs @@ -129,7 +129,7 @@ namespace Umbraco.Tests public void SetApplicationUrlFromDcSettingsSsl2() { // set from distributed call settings - // other servers are slave servers + // other servers are replica servers var settings = Mock.Of(section => section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == new IServer[] @@ -224,7 +224,7 @@ namespace Umbraco.Tests [Test] public void ServerRoleUnknown2() { - // distributed call enabled, cannot find server, assume it's an undeclared slave + // distributed call enabled, cannot find server, assume it's an undeclared replica var settings = Mock.Of(section => section.DistributedCall == Mock.Of(callSection => callSection.Enabled == true && callSection.Servers == new IServer[] @@ -301,4 +301,4 @@ namespace Umbraco.Tests Assert.AreEqual("httpx://whatever.com/umbraco", appCtx._umbracoApplicationUrl); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs index c60e9d883e..1bfadbee4b 100644 --- a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs @@ -17,11 +17,11 @@ namespace Umbraco.Web.Mvc /// internal class MasterControllerFactory : DefaultControllerFactory { - private readonly FilteredControllerFactoriesResolver _slaveFactories; + private readonly FilteredControllerFactoriesResolver _replicaFactories; public MasterControllerFactory(FilteredControllerFactoriesResolver factoryResolver) { - _slaveFactories = factoryResolver; + _replicaFactories = factoryResolver; } /// @@ -36,7 +36,7 @@ namespace Umbraco.Web.Mvc /// public override IController CreateController(RequestContext requestContext, string controllerName) { - var factory = _slaveFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); + var factory = _replicaFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); return factory != null ? factory.CreateController(requestContext, controllerName) : base.CreateController(requestContext, controllerName); @@ -53,7 +53,7 @@ namespace Umbraco.Web.Mvc /// The name of the controller. internal Type GetControllerTypeInternal(RequestContext requestContext, string controllerName) { - var factory = _slaveFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); + var factory = _replicaFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); if (factory != null) { //check to see if the factory is of type UmbracoControllerFactory which exposes the GetControllerType method so we don't have to create @@ -88,7 +88,7 @@ namespace Umbraco.Web.Mvc if (controller is Controller) { var requestContext = ((Controller)controller).ControllerContext.RequestContext; - var factory = _slaveFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); + var factory = _replicaFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); if (factory != null) { factory.ReleaseController(controller); @@ -98,4 +98,4 @@ namespace Umbraco.Web.Mvc if (!released) base.ReleaseController(controller); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs index 77577e26e2..75cce71f73 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs @@ -241,8 +241,8 @@ namespace Umbraco.Web.Routing /// private void PageCacheRefresher_CacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs cacheRefresherEventArgs) { - // only on master / single, not on slaves! - if (IsSlaveServer) return; + // only on master / single, not on replicas! + if (IsReplicaServer) return; // simply getting OldRoutes will register it in the request cache, // so whatever we do with it, try/finally it to ensure it's cleared @@ -299,8 +299,8 @@ namespace Umbraco.Web.Routing return route == null; } - // gets a value indicating whether server is 'slave' - private static bool IsSlaveServer + // gets a value indicating whether server is 'replica' + private static bool IsReplicaServer { get { diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index c10c1c5315..d6e72f936b 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - LogHelper.Debug("Does not run on slave servers."); + LogHelper.Debug("Does not run on replica servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: LogHelper.Debug("Does not run on servers with unknown role."); @@ -80,4 +80,4 @@ namespace Umbraco.Web.Scheduling get { return true; } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 0e6c2c7fb4..eb41fa4c82 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - Logger.Debug("Does not run on slave servers."); + Logger.Debug("Does not run on replica servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: Logger.Debug("Does not run on servers with unknown role."); diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 9000fc72cf..c525f537d0 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -63,7 +63,7 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - LogHelper.Debug("Does not run on slave servers."); + LogHelper.Debug("Does not run on replica servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: LogHelper.Debug("Does not run on servers with unknown role."); @@ -94,4 +94,4 @@ namespace Umbraco.Web.Scheduling get { return false; } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 52cca003b7..bf82f6cb67 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -41,7 +41,7 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - Logger.Debug("Does not run on slave servers."); + Logger.Debug("Does not run on replica servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: Logger.Debug("Does not run on servers with unknown role."); @@ -106,4 +106,4 @@ namespace Umbraco.Web.Scheduling get { return false; } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index ad7fef5fd9..41daf796d6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -92,7 +92,7 @@ namespace Umbraco.Web.Scheduling switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: - LogHelper.Debug("Does not run on slave servers."); + LogHelper.Debug("Does not run on replica servers."); return true; // DO repeat, server role can change case ServerRole.Unknown: LogHelper.Debug("Does not run on servers with unknown role."); diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 5a8d409a8c..4a74aaa55c 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -104,13 +104,13 @@ namespace Umbraco.Web.Scheduling _keepAliveRunner.TryAdd(tasks[0]); // scheduled publishing/unpublishing - // install on all, will only run on non-slaves servers + // install on all, will only run on non-replica servers _publishingRunner.TryAdd(tasks[1]); _tasksRunner.TryAdd(tasks[2]); // log scrubbing - // install on all, will only run on non-slaves servers + // install on all, will only run on non-replica servers _scrubberRunner.TryAdd(tasks[3]); if (healthCheckConfig.NotificationSettings.Enabled) From fbbc89b477effbc6487d775e170c6ebc7578b736 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Tue, 11 Sep 2018 16:29:21 +0100 Subject: [PATCH 049/124] Removed the anchor tag around notification --- .../src/views/components/notifications/umb-notifications.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html index 9962e2dd84..6c74014c7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -11,10 +11,8 @@ From 9e3cf00cc0e9e02ea7a43b36a30e25f9a6c07a1b Mon Sep 17 00:00:00 2001 From: Paul Seal Date: Wed, 12 Sep 2018 13:32:10 +0100 Subject: [PATCH 050/124] update the release notes url --- build/NuSpecs/tools/Readme.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt index aa648caaaa..100e29ab7f 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -21,6 +21,6 @@ The following items will now be automatically included when creating a deploy pa system: umbraco, umbraco_client, config\splashes and global.asax. Please read the release notes on our.umbraco.com: -http://our.umbraco.com/contribute/releases +https://our.umbraco.com/download/releases -- Umbraco \ No newline at end of file +- Umbraco From a0ae39a147005d37650d8e9299b223c0c0d68a49 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Thu, 13 Sep 2018 10:27:48 +0100 Subject: [PATCH 051/124] Reworked the notifications banner to use ng-switch to show the anchor tag when there is a notification url. --- .../components/notifications/umb-notifications.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html index 6c74014c7e..7cd0d1a510 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -9,10 +9,15 @@
- -
+
+ {{notification.headline}} + +
+ {{notification.headline}} + +
From 25c87d76b8cbda43a71ade65a7cb49515efb32d9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 13 Sep 2018 14:59:45 +0200 Subject: [PATCH 052/124] Fix merge --- .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 11 +-- .../V_8_0_0/UpdateDefaultMandatoryLanguage.cs | 6 +- .../Persistence/Dtos/LanguageDto.cs | 2 +- .../Persistence/Factories/LanguageFactory.cs | 4 +- .../Implement/LanguageRepository.cs | 15 ++++- .../Services/Implement/LocalizationService.cs | 24 +++++++ src/Umbraco.Web/Editors/LanguageController.cs | 67 ++++++++----------- 8 files changed, 78 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index bfb8705480..5ec259ade4 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -232,7 +232,7 @@ namespace Umbraco.Core.Migrations.Install private void CreateLanguageData() { - _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefaultVariantLanguage = true }); + _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }); } private void CreateContentChildTypeData() diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index fb87a1ff4f..036fc99fa4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -118,17 +118,18 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 - //Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's - .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to final + .CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next // resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ... - Chain("{guid1}"); // add stephan's after others, with a new target state + + Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}") // and provide a path out of stephan's - .Chain("{guid1}"); - #error is this OK? + .Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next + // resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ... + //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs index dd5fe7c369..b965bc71d2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var defaultId = int.MaxValue; foreach (var dto in dtos) { - if (dto.IsDefaultVariantLanguage) + if (dto.IsDefault) { defaultId = dto.Id; break; @@ -38,8 +38,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // update, so that language with that id is now default and mandatory var updateDefault = Sql() .Update(u => u - .Set(x => x.IsDefaultVariantLanguage, true) - .Set(x => x.Mandatory, true)) + .Set(x => x.IsDefault, true) + .Set(x => x.IsMandatory, true)) .Where(x => x.Id == defaultId); Database.Execute(updateDefault); diff --git a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs index 5f3247bc49..488390f985 100644 --- a/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/LanguageDto.cs @@ -39,7 +39,7 @@ namespace Umbraco.Core.Persistence.Dtos ///
[Column("isDefaultVariantLang")] [Constraint(Default = "0")] - public bool IsDefaultVariantLanguage { get; set; } + public bool IsDefault { get; set; } /// /// Gets or sets a value indicating whether the language is mandatory. diff --git a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs index 9be2409a5e..ad58c5b570 100644 --- a/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/LanguageFactory.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories { CultureName = dto.CultureName, Id = dto.Id, - IsDefault = dto.IsDefaultVariantLanguage, + IsDefault = dto.IsDefault, IsMandatory = dto.IsMandatory, FallbackLanguageId = dto.FallbackLanguageId }; @@ -28,7 +28,7 @@ namespace Umbraco.Core.Persistence.Factories { CultureName = entity.CultureName, IsoCode = entity.IsoCode, - IsDefaultVariantLanguage = entity.IsDefault, + IsDefault = entity.IsDefault, IsMandatory = entity.IsMandatory, FallbackLanguageId = entity.FallbackLanguageId }; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 97535fe07c..2b3674700b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -136,9 +136,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // set all other entities to non-default // safe (no race cond) because the service locks languages var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefaultVariantLanguage, false)); + .Update(u => u.Set(x => x.IsDefault, false)); Database.Execute(setAllDefaultToFalse); } + + // fallback cycles are detected at service level + // insert var dto = LanguageFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -178,6 +181,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } + // fallback cycles are detected at service level + // update var dto = LanguageFactory.BuildDto(entity); Database.Update(dto); @@ -199,8 +204,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); // We need to remove any references to the language if it's being used as a fall-back from other ones - #error rewirte - Database.Execute(Sql().Update(u => u.Set(x => x.FallbackLanguageId, null)).Where(x => x.FallbackLanguageId == entity.Id)); + var clearFallbackLanguage = Sql() + .Update(u => u + .Set(x => x.FallbackLanguageId, null)) + .Where(x => x.FallbackLanguageId == entity.Id); + + Database.Execute(clearFallbackLanguage); // delete base.PersistDeletedItem(entity); diff --git a/src/Umbraco.Core/Services/Implement/LocalizationService.cs b/src/Umbraco.Core/Services/Implement/LocalizationService.cs index ec328bdb9d..49a764b533 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizationService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizationService.cs @@ -363,6 +363,16 @@ namespace Umbraco.Core.Services.Implement // write-lock languages to guard against race conds when dealing with default language scope.WriteLock(Constants.Locks.Languages); + // look for cycles - within write-lock + if (language.FallbackLanguageId.HasValue) + { + var languages = _languageRepository.GetMany().ToDictionary(x => x.Id, x => x); + if (!languages.ContainsKey(language.FallbackLanguageId.Value)) + throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback id={language.FallbackLanguageId.Value} which is not a valid language id."); + if (CreatesCycle(language, languages)) + throw new InvalidOperationException($"Cannot save language {language.IsoCode} with fallback {languages[language.FallbackLanguageId.Value].IsoCode} as it would create a fallback cycle."); + } + var saveEventArgs = new SaveEventArgs(language); if (scope.Events.DispatchCancelable(SavingLanguage, this, saveEventArgs)) { @@ -380,6 +390,20 @@ namespace Umbraco.Core.Services.Implement } } + private bool CreatesCycle(ILanguage language, IDictionary languages) + { + // a new language is not referenced yet, so cannot be part of a cycle + if (!language.HasIdentity) return false; + + var id = language.FallbackLanguageId; + while (true) // assuming languages does not already contains a cycle, this must end + { + if (!id.HasValue) return false; // no fallback means no cycle + if (id.Value == language.Id) return true; // back to language = cycle! + id = languages[id.Value].FallbackLanguageId; // else keep chaining + } + } + /// /// Deletes a by removing it (but not its usages) from the db /// diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 5a6848dea1..a3e613670b 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -77,11 +77,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); } - if (langs.Any(x => x.FallbackLanguageId.HasValue && x.FallbackLanguageId.Value == language.Id)) - { - var message = $"Language '{language.CultureName}' is defined as a fall-back language for one or more other languages, and so cannot be deleted."; - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(message)); - } + // service is happy deleting a language that's fallback for another language, + // will just remove it - so no need to check here Services.LocalizationService.Delete(language); @@ -121,7 +118,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - //create it + // create it (creating a new language cannot create a fallback cycle) var newLang = new Core.Models.Language(culture.Name) { CultureName = culture.DisplayName, @@ -147,46 +144,40 @@ namespace Umbraco.Web.Editors existing.IsDefault = language.IsDefault; existing.FallbackLanguageId = language.FallbackLanguageId; - string selectedFallbackLanguageCultureName; - if (DoesUpdatedFallbackLanguageCreateACircularPath(existing, out selectedFallbackLanguageCultureName)) + // modifying an existing language can create a fallback, verify + // note that the service will check again, dealing with race conds + if (existing.FallbackLanguageId.HasValue) { - ModelState.AddModelError("FallbackLanguage", "The selected fall back language '" + selectedFallbackLanguageCultureName + "' would create a circular path."); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); + if (!languages.ContainsKey(existing.FallbackLanguageId.Value)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + if (CreatesCycle(existing, languages)) + { + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existing.FallbackLanguageId.Value].IsoCode} would create a circular path."); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } } Services.LocalizationService.Save(existing); return Mapper.Map(existing); - } - - private bool DoesUpdatedFallbackLanguageCreateACircularPath(ILanguage language, out string selectedFallbackLanguageCultureName) - { - if (language.FallbackLanguageId.HasValue == false) - { - selectedFallbackLanguageCultureName = string.Empty; - return false; - } - - var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var fallbackLanguageId = language.FallbackLanguageId; - while (fallbackLanguageId.HasValue) - { - if (fallbackLanguageId.Value == language.Id) - { - // We've found the current language in the path of fall back languages, so we have a circular path. - selectedFallbackLanguageCultureName = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).CultureName; - return true; - } - - fallbackLanguageId = GetLanguageFromCollectionById(languages, fallbackLanguageId.Value).FallbackLanguageId; - } - - selectedFallbackLanguageCultureName = string.Empty; - return false; } - private static ILanguage GetLanguageFromCollectionById(IEnumerable languages, int id) + // see LocalizationService + private bool CreatesCycle(ILanguage language, IDictionary languages) { - return languages.Single(x => x.Id == id); + // a new language is not referenced yet, so cannot be part of a cycle + if (!language.HasIdentity) return false; + + var id = language.FallbackLanguageId; + while (true) // assuming languages does not already contains a cycle, this must end + { + if (!id.HasValue) return false; // no fallback means no cycle + if (id.Value == language.Id) return true; // back to language = cycle! + id = languages[id.Value].FallbackLanguageId; // else keep chaining + } } } } From 1acc61ede835f1331fe87cc8927e9f73c927cb7c Mon Sep 17 00:00:00 2001 From: "Ali.Taheri" Date: Thu, 13 Sep 2018 16:24:50 +0100 Subject: [PATCH 053/124] temp-2939 - corrected the data type from boolean to int for rich text editor. --- src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index f4273303b4..5c17449df6 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -19,10 +19,10 @@ namespace Umbraco.Web.PropertyEditors internal class TextAreaPreValueEditor : PreValueEditor { [PreValueField("maxChars", "Maximum allowed characters", "number", Description = "If empty - no character limit")] - public bool MaxChars { get; set; } + public int MaxChars { get; set; } [PreValueField("rows", "Number of rows", "number", Description = "If empty - 10 rows would be set as the default value")] - public bool Rows { get; set; } + public int Rows { get; set; } } } } From 28ae25c1661276efc6dea70970332b7a6a3ec2fd Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 14 Sep 2018 00:57:19 +0200 Subject: [PATCH 054/124] Move logic to load moment.js locales to public function in userservice --- .../src/common/services/user.service.js | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 80565c23e3..a598cb905c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -143,7 +143,6 @@ angular.module('umbraco.services') /** Called to update the current user's timeout */ function setUserTimeoutInternal(newTimeout) { - var asNumber = parseFloat(newTimeout); if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) { currentUser.remainingAuthSeconds = newTimeout; @@ -151,6 +150,31 @@ angular.module('umbraco.services') } } + function getMomentLocales(locales, supportedLocales) { + + var localeUrls = []; + var locales = locales.split(','); + for (var i = 0; i < locales.length; i++) { + var locale = locales[i].toString().toLowerCase(); + console.log("locale", locale); + + if (locale !== 'en-us') { + + if (supportedLocales.indexOf(locale + '.js') > -1) { + localeUrls.push('lib/moment/' + locale + '.js'); + } + if (locale.indexOf('-') > -1) { + var majorLocale = locale.split('-')[0] + '.js'; + if (supportedLocales.indexOf(majorLocale) > -1) { + localeUrls.push('lib/moment/' + majorLocale); + } + } + } + } + + return localeUrls; + } + /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */ function userAuthExpired(isLogout) { //store the last user id and clear the user @@ -178,7 +202,7 @@ angular.module('umbraco.services') } }); - return { + var services = { /** Internal method to display the login dialog */ _showLoginDialog: function () { @@ -279,47 +303,40 @@ angular.module('umbraco.services') /** Loads the Moment.js Locale for the current user. */ loadMomentLocaleForCurrentUser: function () { - function loadLocales(currentUser, supportedLocales) { - var locale = currentUser.locale.toLowerCase(); - if (locale !== 'en-us') { - var localeUrls = []; - if (supportedLocales.indexOf(locale + '.js') > -1) { - localeUrls.push('lib/moment/' + locale + '.js'); - } - if (locale.indexOf('-') > -1) { - var majorLocale = locale.split('-')[0] + '.js'; - if (supportedLocales.indexOf(majorLocale) > -1) { - localeUrls.push('lib/moment/' + majorLocale); - } - } - return assetsService.load(localeUrls, $rootScope); - } - else { - //return a noop promise - var deferred = $q.defer(); - var promise = deferred.promise; - deferred.resolve(true); - return promise; - } - } - var promises = { currentUser: this.getCurrentUser(), supportedLocales: javascriptLibraryService.getSupportedLocalesForMoment() } return $q.all(promises).then(function (values) { - return loadLocales(values.currentUser, values.supportedLocales); + return services.loadLocales(values.currentUser.locale, values.supportedLocales); }); - - }, + /** Loads specific Moment.js Locales. */ + loadLocales: function (locales, supportedLocales) { + + var localeUrls = getMomentLocales(locales, supportedLocales); + console.log("localeUrls", localeUrls); + + if (localeUrls.length >= 1) { + return assetsService.load(localeUrls, $rootScope); + } + else { + //return a noop promise + var deferred = $q.defer(); + var promise = deferred.promise; + deferred.resolve(true); + return promise; + } + }, + /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ setUserTimeout: function (newTimeout) { setUserTimeoutInternal(newTimeout); } }; + return services; }); From e9f8fb8d1fd3f88ee921047edcfdf064bfc53dcb Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 14 Sep 2018 01:10:17 +0200 Subject: [PATCH 055/124] Remove console.log --- src/Umbraco.Web.UI.Client/src/common/services/user.service.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index a598cb905c..922e33ae08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -156,8 +156,6 @@ angular.module('umbraco.services') var locales = locales.split(','); for (var i = 0; i < locales.length; i++) { var locale = locales[i].toString().toLowerCase(); - console.log("locale", locale); - if (locale !== 'en-us') { if (supportedLocales.indexOf(locale + '.js') > -1) { @@ -318,7 +316,6 @@ angular.module('umbraco.services') loadLocales: function (locales, supportedLocales) { var localeUrls = getMomentLocales(locales, supportedLocales); - console.log("localeUrls", localeUrls); if (localeUrls.length >= 1) { return assetsService.load(localeUrls, $rootScope); From 7f4280c67f4fcce342b6ea9e9e41074c5cd6ffb4 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Fri, 14 Sep 2018 10:06:32 +0100 Subject: [PATCH 056/124] added checks for whitespace --- .../src/views/components/notifications/umb-notifications.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html index 7cd0d1a510..3b78562c1a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -10,7 +10,7 @@
- + {{notification.headline}} From 6ee235b640105826ab58101cf1ffd10c2f8d8cf2 Mon Sep 17 00:00:00 2001 From: Damiaan Date: Mon, 17 Sep 2018 20:42:22 +0200 Subject: [PATCH 057/124] old issue tracker reference Since the github issue tracker is used, we should at least mention that. --- .github/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 74f9762040..e8a9889916 100644 --- a/.github/README.md +++ b/.github/README.md @@ -46,4 +46,5 @@ Umbraco is contribution focused and community driven. If you want to contribute Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/.github/CONTRIBUTING.md). -To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). +You can comment and report issues on the [github issue tracker](https://github.com/umbraco/Umbraco-CMS/issues). +Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/) the old issue tracker is in read only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org). From 8bf8e0a7415a42e00edaab08c4b13994d22eb22d Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 20 Sep 2018 08:48:46 +0200 Subject: [PATCH 058/124] Updates CDF --- build/NuSpecs/UmbracoCms.Core.nuspec | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 14 +++++++------- src/Umbraco.Core/packages.config | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 7 ++++--- src/Umbraco.Web.UI/packages.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- src/Umbraco.Web/packages.config | 2 +- src/umbraco.cms/packages.config | 2 +- src/umbraco.cms/umbraco.cms.csproj | 4 ++-- src/umbraco.controls/packages.config | 2 +- src/umbraco.controls/umbraco.controls.csproj | 4 ++-- src/umbraco.editorControls/packages.config | 2 +- .../umbraco.editorControls.csproj | 4 ++-- 13 files changed, 26 insertions(+), 25 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 1b0e082aa8..0ae9591c97 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4b933bf728..aa3b2db784 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + Debug @@ -45,8 +45,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll @@ -567,8 +567,8 @@ - - + + @@ -638,7 +638,7 @@ - + @@ -1688,4 +1688,4 @@ --> - + \ No newline at end of file diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index eaeddb1627..4d7c2380b4 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index b5e022fa8e..1efd3052aa 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -49,6 +49,7 @@ true true + bin\ @@ -116,8 +117,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll ..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll @@ -1085,4 +1086,4 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 8a4b77ca3a..a27a63329e 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e970c09fca..7e961bf7af 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -107,8 +107,8 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll ..\packages\dotless.1.5.2\lib\dotless.Core.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 0acf6ef32e..e03c088146 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/umbraco.cms/packages.config b/src/umbraco.cms/packages.config index 7514fb8672..404427fb5c 100644 --- a/src/umbraco.cms/packages.config +++ b/src/umbraco.cms/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index c38dbb2d2c..31e1faecac 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -106,8 +106,8 @@ false - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll diff --git a/src/umbraco.controls/packages.config b/src/umbraco.controls/packages.config index de6a15b59a..cbccba27be 100644 --- a/src/umbraco.controls/packages.config +++ b/src/umbraco.controls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.controls/umbraco.controls.csproj b/src/umbraco.controls/umbraco.controls.csproj index d8b17c9240..e1b190b701 100644 --- a/src/umbraco.controls/umbraco.controls.csproj +++ b/src/umbraco.controls/umbraco.controls.csproj @@ -68,8 +68,8 @@ false - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll diff --git a/src/umbraco.editorControls/packages.config b/src/umbraco.editorControls/packages.config index de6a15b59a..cbccba27be 100644 --- a/src/umbraco.editorControls/packages.config +++ b/src/umbraco.editorControls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.editorControls/umbraco.editorControls.csproj b/src/umbraco.editorControls/umbraco.editorControls.csproj index 5ff7ca7184..1bbd3b8a24 100644 --- a/src/umbraco.editorControls/umbraco.editorControls.csproj +++ b/src/umbraco.editorControls/umbraco.editorControls.csproj @@ -114,8 +114,8 @@ {651E1350-91B6-44B7-BD60-7207006D7003} Umbraco.Web - - ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll System From 1050e9d0a10e6c7619ed12ba4fdb712789277465 Mon Sep 17 00:00:00 2001 From: Paul Daly Date: Thu, 20 Sep 2018 13:15:22 +0100 Subject: [PATCH 059/124] Preview update: smartphone sizes The current most used screen size on a smarthphone is 360x640 according to Globalstats Statcounter. With this in mind I have adjusted the sizes of the preview frames for both portrait and landscape. Minor change but something that has been on my mind for a while. --- src/Umbraco.Web.UI.Client/src/less/canvas-designer.less | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 7410c09580..78c1616bc6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -790,13 +790,13 @@ h4.panel-title { } .smartphone-portrait { - width: 320px; - height: 504px; + width: 360px; + height: 640px; } .smartphone-landscape { - width: 480px; - height: 256px; + width: 640px; + height: 360px; } .border { From 3f79067e2678fda2cbd07eb8d08a663f3c3ec7a2 Mon Sep 17 00:00:00 2001 From: Jim Osborn Date: Mon, 24 Sep 2018 12:38:04 +0100 Subject: [PATCH 060/124] Fixes content getting deleted when saving an RTE before it's fully loaded. --- .../src/views/propertyeditors/rte/rte.controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index c5a739bec2..5e37bcce5d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -377,7 +377,9 @@ angular.module("umbraco") var unsubscribe = $scope.$on("formSubmitting", function () { //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer // we do parse it out on the server side but would be nice to do that on the client side before as well. - $scope.model.value = tinyMceEditor ? tinyMceEditor.getContent() : null; + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + $scope.model.value = tinyMceEditor.getContent(); + } }); //when the element is disposed we need to unsubscribe! From 70a851dbf9147736f5d11c4c08964985ee95cff4 Mon Sep 17 00:00:00 2001 From: Pavel Budik Date: Mon, 24 Sep 2018 14:23:00 +0200 Subject: [PATCH 061/124] #3024: Removed unnecessary queries in MultiNodeTreePickerPropertyConverter --- .../MultiNodeTreePickerPropertyConverter.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs index 249b7e0cac..4008c1c61b 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs @@ -164,13 +164,23 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var multiNodeTreePicker = new List(); var objectType = UmbracoObjectTypes.Unknown; + IPublishedContent multiNodeTreePickerItem = null; foreach (var udi in udis) { - var multiNodeTreePickerItem = - GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent) - ?? GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia) - ?? GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); + switch (udi.EntityType) + { + case Constants.UdiEntityType.Document: + multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent); + break; + case Constants.UdiEntityType.Media: + multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia); + break; + case Constants.UdiEntityType.Member: + multiNodeTreePickerItem = GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); + break; + } + if (multiNodeTreePickerItem != null) { multiNodeTreePicker.Add(multiNodeTreePickerItem); From 3d0d8d74305f8613d24285faa291446ece39b43e Mon Sep 17 00:00:00 2001 From: kedde Date: Mon, 24 Sep 2018 20:00:08 +0200 Subject: [PATCH 062/124] fix mysql casing --- .../TargetVersionSevenTwelveZero/SetDefaultTagsStorageType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/SetDefaultTagsStorageType.cs index 8e13fa1013..6172c60b2c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/SetDefaultTagsStorageType.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/SetDefaultTagsStorageType.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwelveZ // We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed var datatypes = Context.Database.Fetch("SELECT * FROM cmsDataType"); var tagsDataTypes = datatypes.Where(x => string.Equals(x.PropertyEditorAlias, Constants.PropertyEditors.TagsAlias, StringComparison.InvariantCultureIgnoreCase)); - var dataTypePreValues = Context.Database.Fetch("SELECT * FROM cmsDataTypePrevalues"); + var dataTypePreValues = Context.Database.Fetch("SELECT * FROM cmsDataTypePreValues"); foreach (var datatype in tagsDataTypes) { @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwelveZ // if the "storageType" has not been set we do so by adding a new row in the table for the nodid and set it if (result == null) { - Insert.IntoTable("CmsDataTypePrevalues").Row(new + Insert.IntoTable("cmsDataTypePreValues").Row(new { datatypeNodeId = datatype.DataTypeId, value = "Csv", From f9973f602ca58cbcc46f6aa73f6a196ac885c29a Mon Sep 17 00:00:00 2001 From: Umbraco HQ Date: Tue, 25 Sep 2018 14:03:21 +0200 Subject: [PATCH 063/124] Fixes the belle build (requires newer node version) --- build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 index 9b465fd78d..2a1e9e6831 100644 --- a/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 +++ b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 @@ -94,14 +94,14 @@ function Get-UmbracoBuildEnv } # ensure we have node - $node = "$path\node-v6.9.1-win-x86" - $source = "http://nodejs.org/dist/v6.9.1/node-v6.9.1-win-x86.7z" + $node = "$path\node-v8.12.0-win-x86" + $source = "http://nodejs.org/dist/v8.12.0/node-v8.12.0-win-x86.7z " if (-not (test-path $node)) { Write-Host "Download Node..." - Invoke-WebRequest $source -OutFile "$path\node-v6.9.1-win-x86.7z" - &$sevenZip x "$path\node-v6.9.1-win-x86.7z" -o"$path" -aos > $nul - Remove-File "$path\node-v6.9.1-win-x86.7z" + Invoke-WebRequest $source -OutFile "$path\node-v8.12.0-win-x86.7z" + &$sevenZip x "$path\node-v8.12.0-win-x86.7z" -o"$path" -aos > $nul + Remove-File "$path\node-v8.12.0-win-x86.7z" } # note: why? node already brings everything we need! From 35ec07f97089ba2aa8f3729282609664efc75674 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 25 Sep 2018 16:15:37 +0200 Subject: [PATCH 064/124] Some tiny code style updates --- src/Umbraco.Web/Editors/HelpController.cs | 8 ++++++-- src/Umbraco.Web/Install/InstallHelper.cs | 3 +-- src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs | 3 +-- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 6 ++---- src/umbraco.cms/businesslogic/Packager/Installer.cs | 2 -- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs index 8f981d9c88..ccbbcaeee8 100644 --- a/src/Umbraco.Web/Editors/HelpController.cs +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -8,12 +8,16 @@ namespace Umbraco.Web.Editors { public class HelpController : UmbracoAuthorizedJsonController { - private static readonly HttpClient HttpClient = new HttpClient(); + private static HttpClient _httpClient; public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); + + if (_httpClient == null) + _httpClient = new HttpClient(); + //fetch dashboard json and parse to JObject - var json = await HttpClient.GetStringAsync(url); + var json = await _httpClient.GetStringAsync(url); var result = JsonConvert.DeserializeObject>(json); if (result != null) return result; diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 145395e235..5b6e8a0c60 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -193,9 +193,8 @@ namespace Umbraco.Web.Install internal IEnumerable GetStarterKits() { if (_httpClient == null) - { _httpClient = new HttpClient(); - } + var packages = new List(); try { diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 4dfa793675..22f5f0ff7e 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -83,9 +83,8 @@ namespace Umbraco.Web.Install.InstallSteps try { if (_webClient == null) - { _webClient = new WebClient(); - } + var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email} }; _webClient.UploadValues("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", values); } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index a50b4dffbd..4278b82c34 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -63,13 +63,11 @@ namespace Umbraco.Web.Scheduling private async Task GetTaskByHttpAync(string url, CancellationToken token) { if (_httpClient == null) - { _httpClient = new HttpClient(); - } + if (Uri.TryCreate(_appContext.UmbracoApplicationUrl, UriKind.Absolute, out var baseUri)) - { _httpClient.BaseAddress = baseUri; - } + var request = new HttpRequestMessage(HttpMethod.Get, url); //TODO: pass custom the authorization header, currently these aren't really secured! diff --git a/src/umbraco.cms/businesslogic/Packager/Installer.cs b/src/umbraco.cms/businesslogic/Packager/Installer.cs index f3733d5e5a..13d412ce98 100644 --- a/src/umbraco.cms/businesslogic/Packager/Installer.cs +++ b/src/umbraco.cms/businesslogic/Packager/Installer.cs @@ -688,9 +688,7 @@ namespace umbraco.cms.businesslogic.packager Directory.CreateDirectory(IOHelper.MapPath(SystemDirectories.Packages)); if (_webClient == null) - { _webClient = new WebClient(); - } _webClient.DownloadFile( "http://" + PackageServer + "/fetch?package=" + Package.ToString(), From 540e4f17d6f2718e0bb8827e792fa2e566bcfa25 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 26 Sep 2018 09:48:09 +0200 Subject: [PATCH 065/124] Fixes #3042 Code comes from v8, commit: 9bfe9e6bbf0d0e81970c07210da8e15c6754b291 --- .../Security/AuthenticationExtensions.cs | 22 ++++++++++++++++++- .../BackOfficeCookieAuthenticationProvider.cs | 21 +++--------------- src/Umbraco.Web/UmbracoModule.cs | 7 ++++++ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 9df84cc2b1..bd0891cd1a 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -458,5 +460,23 @@ namespace Umbraco.Core.Security return ticket; } + + /// + /// Ensures that the thread culture is set based on the back office user's culture + /// + /// + internal static void EnsureCulture(this IIdentity identity) + { + if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated) + { + Thread.CurrentThread.CurrentUICulture = + Thread.CurrentThread.CurrentCulture = + UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s)); + } + } + /// + /// Used so that we aren't creating a new CultureInfo object for every single request + /// + private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary(); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs index 3ff1266c6b..1a3b9f54ee 100644 --- a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs @@ -102,7 +102,8 @@ namespace Umbraco.Core.Security /// public override async Task ValidateIdentity(CookieValidateIdentityContext context) { - EnsureCulture(context); + //ensure the thread culture is set + context?.Identity?.EnsureCulture(); await EnsureValidSessionId(context); @@ -120,21 +121,5 @@ namespace Umbraco.Core.Security if (_appCtx.IsConfigured && _appCtx.IsUpgrading == false) await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } - - private void EnsureCulture(CookieValidateIdentityContext context) - { - var umbIdentity = context.Identity as UmbracoBackOfficeIdentity; - if (umbIdentity != null && umbIdentity.IsAuthenticated) - { - Thread.CurrentThread.CurrentCulture = - Thread.CurrentThread.CurrentUICulture = - UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s)); - } - } - - /// - /// Used so that we aren't creating a new CultureInfo object for every single request - /// - private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary(); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 99043143a7..29d0de968d 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -509,6 +509,13 @@ namespace Umbraco.Web } }; + app.PostAuthenticateRequest += (sender, e) => + { + var httpContext = ((HttpApplication)sender).Context; + //ensure the thread culture is set + httpContext.User?.Identity?.EnsureCulture(); + }; + app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; From a1224da5e16f948962600c975a90ff98f386da67 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 26 Sep 2018 13:27:16 +0200 Subject: [PATCH 066/124] Fixes #3036 Removes WebClient completely except for in an old legacy class that is not used for anything any more (umbraco.cms.businesslogic.packager.Installer.Fetch). --- .../Editors/CanvasDesignerController.cs | 31 +++------- src/Umbraco.Web/Install/InstallHelper.cs | 5 +- .../Install/InstallSteps/NewInstallStep.cs | 40 +++++++------ .../EmbedProviders/AbstractOEmbedProvider.cs | 13 ++-- .../umbraco.presentation/keepAliveService.cs | 16 ++--- .../umbraco/dashboard/FeedProxy.aspx.cs | 60 +++++++++---------- 6 files changed, 79 insertions(+), 86 deletions(-) diff --git a/src/Umbraco.Web/Editors/CanvasDesignerController.cs b/src/Umbraco.Web/Editors/CanvasDesignerController.cs index 8284bf0b12..e0645b3c23 100644 --- a/src/Umbraco.Web/Editors/CanvasDesignerController.cs +++ b/src/Umbraco.Web/Editors/CanvasDesignerController.cs @@ -1,29 +1,17 @@ using System.Collections.Generic; -using System.Net; using System.Net.Http; -using System.Text; using System.Web.Http; -using AutoMapper; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using umbraco; using Umbraco.Web.WebApi; using System; using System.Net.Http.Headers; using System.Web; -using System.IO; -using Umbraco.Core.Models; -using System.Text.RegularExpressions; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Services; namespace Umbraco.Web.Editors { public class CanvasDesignerController : UmbracoApiController { - private static WebClient _webClient; + private static HttpClient _httpClient; [HttpGet] public HttpResponseMessage GetGoogleFont() @@ -35,17 +23,14 @@ namespace Umbraco.Web.Editors // Google Web Font JSON URL var googleWebFontAPIURL = string.Format("https://www.googleapis.com/webfonts/v1/webfonts?key={0}", APIKey); - var response = "{}"; - if (_webClient == null) - _webClient = new WebClient(); - - response = _webClient.DownloadString(new Uri(googleWebFontAPIURL)); - - var resp = Request.CreateResponse(); - resp.Content = new StringContent(response); - resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - return resp; + if (_httpClient == null) + _httpClient = new HttpClient(); + using (var request = new HttpRequestMessage(HttpMethod.Get, googleWebFontAPIURL)) + { + var response = _httpClient.SendAsync(request).Result; + return response; + } } [HttpGet] diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 5b6e8a0c60..50324bcfd2 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -198,12 +198,11 @@ namespace Umbraco.Web.Install var packages = new List(); try { - var requestUri = string.Format("https://our.umbraco.com/webapi/StarterKit/Get/?umbracoVersion={0}", - UmbracoVersion.Current); + var requestUri = $"https://our.umbraco.com/webapi/StarterKit/Get/?umbracoVersion={UmbracoVersion.Current}"; using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri)) - using (var response = _httpClient.SendAsync(request).Result) { + var response = _httpClient.SendAsync(request).Result; packages = response.Content.ReadAsAsync>().Result.ToList(); } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 22f5f0ff7e..2d6fd89c46 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -1,13 +1,14 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; using System.Net; +using System.Net.Http; +using System.Text; using System.Web; using System.Web.Security; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Persistence; using Umbraco.Web.Install.Models; namespace Umbraco.Web.Install.InstallSteps @@ -20,13 +21,12 @@ namespace Umbraco.Web.Install.InstallSteps /// error, etc... and the end-user refreshes the installer then we cannot show the user screen because they've already entered that information so instead we'll /// display a simple continue installation view. /// - [InstallSetupStep(InstallationType.NewInstall, - "User", 20, "")] + [InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] internal class NewInstallStep : InstallSetupStep { private readonly HttpContextBase _http; private readonly ApplicationContext _applicationContext; - private static WebClient _webClient; + private static HttpClient _httpClient; public NewInstallStep(HttpContextBase http, ApplicationContext applicationContext) { @@ -76,17 +76,18 @@ namespace Umbraco.Web.Install.InstallSteps admin.Username = user.Email.Trim(); _applicationContext.Services.UserService.Save(admin); - - + if (user.SubscribeToNewsLetter) { + if (_httpClient == null) + _httpClient = new HttpClient(); + + var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } }; + var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); + try { - if (_webClient == null) - _webClient = new WebClient(); - - var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email} }; - _webClient.UploadValues("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", values); + var response = _httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; } catch { /* fail in silence */ } } @@ -113,13 +114,16 @@ namespace Umbraco.Web.Install.InstallSteps public override string View { - get { return RequiresExecution(null) - //the user UI - ? "user" - //the continue install UI - : "continueinstall"; } + get + { + return RequiresExecution(null) + //the user UI + ? "user" + //the continue install UI + : "continueinstall"; + } } - + public override bool RequiresExecution(UserModel model) { //now we have to check if this is really a new install, the db might be configured and might contain data diff --git a/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs b/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs index 737b04fc83..fa1ef9fb93 100644 --- a/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs +++ b/src/Umbraco.Web/Media/EmbedProviders/AbstractOEmbedProvider.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Media.EmbedProviders public abstract class AbstractOEmbedProvider : IEmbedProvider { - private static WebClient _webClient; + private static HttpClient _httpClient; public virtual bool SupportsDimensions { @@ -53,9 +53,14 @@ namespace Umbraco.Web.Media.EmbedProviders public virtual string DownloadResponse(string url) { - if (_webClient == null) - _webClient = new WebClient(); - return _webClient.DownloadString(url); + if (_httpClient == null) + _httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) + { + var response = _httpClient.SendAsync(request).Result; + return response.Content.ReadAsStringAsync().Result; + } } public virtual T GetJsonResponse(string url) where T : class diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index 192c35a233..21511bb1dc 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; -using System.Net; -using System.Web; +using System.Net.Http; using Umbraco.Core; using Umbraco.Core.Logging; @@ -10,7 +8,7 @@ namespace umbraco.presentation [Obsolete("This is no longer used and will be removed in future versions")] public class keepAliveService { - private static WebClient _webClient; + private static HttpClient _httpClient; //NOTE: sender will be the umbraco ApplicationContext public static void PingUmbraco(object sender) { @@ -22,9 +20,13 @@ namespace umbraco.presentation var url = appContext.UmbracoApplicationUrl + "/ping.aspx"; try { - if (_webClient == null) - _webClient = new WebClient(); - _webClient.DownloadString(url); + if (_httpClient == null) + _httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, url)) + { + var response = _httpClient.SendAsync(request).Result; + } } catch(Exception ee) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs index 488e4f06c2..49539b0264 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dashboard/FeedProxy.aspx.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web; @@ -6,54 +7,51 @@ namespace dashboardUtilities { using System; using System.Linq; - using System.Net; using System.Net.Mime; - using umbraco; using umbraco.BasePages; - using umbraco.BusinessLogic; using Umbraco.Core.IO; public partial class FeedProxy : UmbracoEnsuredPage { - private static WebClient _webClient; + private static HttpClient _httpClient; protected void Page_Load(object sender, EventArgs e) { try { - if (Request.QueryString.AllKeys.Contains("url") && Request.QueryString["url"] != null) + if (Request.QueryString.AllKeys.Contains("url") == false || Request.QueryString["url"] == null) + return; + + var url = Request.QueryString["url"]; + if (string.IsNullOrWhiteSpace(url) || url.StartsWith("/")) + return; + + if (Uri.TryCreate(url, UriKind.Absolute, out var requestUri) == false) + return; + + var feedProxyXml = XmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); + if (feedProxyXml?.SelectSingleNode($"//allow[@host = '{requestUri.Host}']") != null && requestUri.Port == 80) { - var url = Request.QueryString["url"]; - if (!string.IsNullOrWhiteSpace(url) && !url.StartsWith("/")) + if (_httpClient == null) + _httpClient = new HttpClient(); + + using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri)) { - Uri requestUri; - if (Uri.TryCreate(url, UriKind.Absolute, out requestUri)) - { - var feedProxyXml = xmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); - if (feedProxyXml != null - && feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null - && requestUri.Port == 80) - { - if (_webClient == null) - _webClient = new WebClient(); - - var response = _webClient.DownloadString(requestUri); + var response = _httpClient.SendAsync(request).Result; + var result = response.Content.ReadAsStringAsync().Result; - if (string.IsNullOrEmpty(response) == false) - { - Response.Clear(); - Response.ContentType = Request.CleanForXss("type") ?? MediaTypeNames.Text.Xml; - Response.Write(response); - } + if (string.IsNullOrEmpty(result)) + return; - } - else - { - LogHelper.Debug(string.Format("Access to unallowed feedproxy attempted: {0}", requestUri)); - } - } + Response.Clear(); + Response.ContentType = Request.CleanForXss("type") ?? MediaTypeNames.Text.Xml; + Response.Write(result); } } + else + { + LogHelper.Debug($"Access to unallowed feedproxy attempted: {requestUri}"); + } } catch (Exception ex) { From 5832387484dd447f7033e91514c8f2a47b0739ad Mon Sep 17 00:00:00 2001 From: Dennis Adolfi Date: Tue, 25 Jul 2017 11:43:07 +0200 Subject: [PATCH 067/124] Added the DefaultLabelDataTypeIdas a non-deletable datatype. --- src/Umbraco.Core/Constants-System.cs | 1 + .../Trees/DataTypeTreeController.cs | 39 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index d4deda826e..1969cd8506 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -61,6 +61,7 @@ public const int DefaultContentListViewDataTypeId = -95; public const int DefaultMediaListViewDataTypeId = -96; public const int DefaultMembersListViewDataTypeId = -97; + public const int DefaultLabelDataTypeId = -92; public const string UmbracoConnectionName = "umbracoDbDSN"; public const string UmbracoMigrationName = "Umbraco"; diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 70da27c1bf..e18debdf2c 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -51,8 +51,8 @@ namespace Umbraco.Web.Trees //if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; - //Normal nodes - var sysIds = GetSystemIds(); + //System ListView nodes + var systemListViewDataTypeIds = GetNonDeletableSystemListViewDataTypeIds(); nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType) @@ -61,7 +61,7 @@ namespace Umbraco.Web.Trees { var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, "icon-autofill", false); node.Path = dt.Path; - if (sysIds.Contains(dt.Id)) + if (systemListViewDataTypeIds.Contains(dt.Id)) { node.Icon = "icon-thumbnail-list"; } @@ -70,17 +70,32 @@ namespace Umbraco.Web.Trees return nodes; } - - private IEnumerable GetSystemIds() - { - var systemIds = new[] + + /// + /// Get all integer identifiers for the non-deletable system datatypes. + /// + private static IEnumerable GetNonDeletableSystemDataTypeIds() + { + var systemIds = new[] { - Constants.System.DefaultContentListViewDataTypeId, - Constants.System.DefaultMediaListViewDataTypeId, + Constants.System.DefaultLabelDataTypeId + }; + + return systemIds.Concat(GetNonDeletableSystemListViewDataTypeIds()); + } + + /// + /// Get all integer identifiers for the non-deletable system listviews. + /// + private static IEnumerable GetNonDeletableSystemListViewDataTypeIds() + { + return new[] + { + Constants.System.DefaultContentListViewDataTypeId, + Constants.System.DefaultMediaListViewDataTypeId, Constants.System.DefaultMembersListViewDataTypeId }; - return systemIds; - } + } protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { @@ -120,7 +135,7 @@ namespace Umbraco.Web.Trees } else { - var sysIds = GetSystemIds(); + var sysIds = GetNonDeletableSystemDataTypeIds(); if (sysIds.Contains(int.Parse(id)) == false) { From 3940176f2338d069adb36423de1c55539f94de9a Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 26 Sep 2018 18:01:50 +0200 Subject: [PATCH 068/124] Just a bit of code cleanup --- .../Trees/DataTypeTreeController.cs | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index e18debdf2c..21da6fc0c0 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -1,19 +1,14 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http.Formatting; -using System.Web.Http; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; -using umbraco; using umbraco.BusinessLogic.Actions; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; @@ -107,8 +102,8 @@ namespace Umbraco.Web.Trees menu.DefaultMenuAlias = ActionNew.Instance.Alias; // root actions - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionNew.Instance.Alias}")); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionRefresh.Instance.Alias}"), hasSeparator: true); return menu; } @@ -118,31 +113,28 @@ namespace Umbraco.Web.Trees //set the default to create menu.DefaultMenuAlias = ActionNew.Instance.Alias; - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionNew.Instance.Alias}")); - menu.Items.Add(new MenuItem("rename", Services.TextService.Localize(String.Format("actions/{0}", "rename"))) + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize("actions/rename")) { Icon = "icon icon-edit" }); if (container.HasChildren() == false) - { //can delete data type - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); - } - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionDelete.Instance.Alias}")); + + menu.Items.Add(Services.TextService.Localize($"actions/{ActionRefresh.Instance.Alias}"), hasSeparator: true); } else { - var sysIds = GetNonDeletableSystemDataTypeIds(); + var nonDeletableSystemDataTypeIds = GetNonDeletableSystemDataTypeIds(); - if (sysIds.Contains(int.Parse(id)) == false) - { - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); - } + if (nonDeletableSystemDataTypeIds.Contains(int.Parse(id)) == false) + menu.Items.Add(Services.TextService.Localize($"actions/{ActionDelete.Instance.Alias}")); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionMove.Instance.Alias}"), hasSeparator: true); } return menu; @@ -154,4 +146,4 @@ namespace Umbraco.Web.Trees return Mapper.Map>(results); } } -} \ No newline at end of file +} From 471bc7f116d19b21134159c8476ea725f0e02d84 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Wed, 26 Sep 2018 20:48:54 +0200 Subject: [PATCH 069/124] U4-11458 - Fix multiple issues for angular logResource (#2719) --- .../Repositories/AuditRepository.cs | 7 +- .../src/common/resources/log.resource.js | 160 ++++++++++++++---- src/Umbraco.Web/Editors/LogController.cs | 33 ++-- 3 files changed, 144 insertions(+), 56 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs index e602358cf6..4fe235346f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs @@ -48,6 +48,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (query == null) query = new Query(); + var queryHasWhereClause = query.GetWhereClauses().Any(); var translatorIds = new SqlTranslator(sql, query); var translatedQuery = translatorIds.Translate(); @@ -58,7 +59,7 @@ namespace Umbraco.Core.Persistence.Repositories { var filterSql = new Sql(); foreach (var filterClause in customFilterWheres) - { + { filterSql.Append($"AND ({filterClause.Item1})", filterClause.Item2); } @@ -69,7 +70,7 @@ namespace Umbraco.Core.Persistence.Repositories { var filterSql = new Sql(); foreach (var filterClause in auditTypeFilter) - { + { filterSql.Append("AND (logHeader = @logHeader)", new { logHeader = filterClause.ToString() }); } @@ -166,7 +167,7 @@ namespace Umbraco.Core.Persistence.Repositories // Apply filter if (filterSql != null) { - //ensure we don't append a WHERE if there is already one + //ensure we don't append a WHERE if there is already one var sqlFilter = hasWhereClause ? filterSql.SQL : " WHERE " + filterSql.SQL.TrimStart("AND "); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index cb676511a5..c4c54c7a50 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -7,10 +7,60 @@ **/ function logResource($q, $http, umbRequestHelper) { + function isValidDate(input) { + if (input) { + if (Object.prototype.toString.call(input) === "[object Date]" && !isNaN(input.getTime())) { + return true; + } + } + + return false; + }; + + function dateToValidIsoString(input) { + if (isValidDate(input)) { + return input.toISOString(); + } + + return ''; + }; + //the factory object returned return { - getPagedEntityLog: function (options) { + /** + * @ngdoc method + * @name umbraco.resources.logResource#getPagedEntityLog + * @methodOf umbraco.resources.logResource + * + * @description + * Gets a paginated log history for a entity + * + * ##usage + *
+        * var options = {
+        *      id : 1234
+        *      pageSize : 10,
+        *      pageNumber : 1,
+        *      orderDirection : "Descending",
+        *      sinceDate : new Date(2018,0,1)
+        * };
+        * logResource.getPagedEntityLog(options)
+        *    .then(function(log) {
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Object} options options object + * @param {Int} options.id the id of the entity + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageNumber if paging data, current page index, default = 1 + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` + * @param {Date} options.sinceDate if provided this will only get log entries going back to this date + * @returns {Promise} resourcePromise object containing the log. + * + */ + getPagedEntityLog: function(options) { var defaults = { pageSize: 10, @@ -24,17 +74,21 @@ function logResource($q, $http, umbRequestHelper) { angular.extend(defaults, options); //now copy back to the options we will use options = defaults; + + if (options.hasOwnProperty('sinceDate')) { + options.sinceDate = dateToValidIsoString(options.sinceDate); + } + //change asc/desct if (options.orderDirection === "asc") { options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { + } else if (options.orderDirection === "desc") { options.orderDirection = "Descending"; } - + if (options.id === undefined || options.id === null) { throw "options.id is required"; - } + } return umbRequestHelper.resourcePromise( $http.get( @@ -45,7 +99,37 @@ function logResource($q, $http, umbRequestHelper) { 'Failed to retrieve log data for id'); }, - getPagedUserLog: function (options) { + /** + * @ngdoc method + * @name umbraco.resources.logResource#getPagedUserLog + * @methodOf umbraco.resources.logResource + * + * @description + * Gets a paginated log history for the current user + * + * ##usage + *
+         * var options = {
+         *      pageSize : 10,
+         *      pageNumber : 1,
+         *      orderDirection : "Descending",
+         *      sinceDate : new Date(2018,0,1)
+         * };
+         * logResource.getPagedUserLog(options)
+         *    .then(function(log) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {Object} options options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageNumber if paging data, current page index, default = 1 + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` + * @param {Date} options.sinceDate if provided this will only get log entries going back to this date + * @returns {Promise} resourcePromise object containing the log. + * + */ + getPagedUserLog: function(options) { var defaults = { pageSize: 10, @@ -59,11 +143,15 @@ function logResource($q, $http, umbRequestHelper) { angular.extend(defaults, options); //now copy back to the options we will use options = defaults; + + if (options.hasOwnProperty('sinceDate')) { + options.sinceDate = dateToValidIsoString(options.sinceDate); + } + //change asc/desct if (options.orderDirection === "asc") { options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { + } else if (options.orderDirection === "desc") { options.orderDirection = "Descending"; } @@ -71,10 +159,10 @@ function logResource($q, $http, umbRequestHelper) { $http.get( umbRequestHelper.getApiUrl( "logApiBaseUrl", - "GetPagedEntityLog", + "GetPagedCurrentUserLog", options)), 'Failed to retrieve log data for id'); - }, + }, /** * @ngdoc method @@ -82,6 +170,7 @@ function logResource($q, $http, umbRequestHelper) { * @methodOf umbraco.resources.logResource * * @description + * [OBSOLETE] use getPagedEntityLog instead
* Gets the log history for a give entity id * * ##usage @@ -96,23 +185,24 @@ function logResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the log. * */ - getEntityLog: function (id) { + getEntityLog: function(id) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "logApiBaseUrl", - "GetEntityLog", - [{ id: id }])), - 'Failed to retrieve user data for id ' + id); + $http.get( + umbRequestHelper.getApiUrl( + "logApiBaseUrl", + "GetEntityLog", + [{ id: id }])), + 'Failed to retrieve user data for id ' + id); }, - + /** * @ngdoc method * @name umbraco.resources.logResource#getUserLog * @methodOf umbraco.resources.logResource * * @description - * Gets the current users' log history for a given type of log entry + * [OBSOLETE] use getPagedUserLog instead
+ * Gets the current user's log history for a given type of log entry * * ##usage *
@@ -127,14 +217,14 @@ function logResource($q, $http, umbRequestHelper) {
          * @returns {Promise} resourcePromise object containing the log.
          *
          */
-        getUserLog: function (type, since) {            
+        getUserLog: function(type, since) {
             return umbRequestHelper.resourcePromise(
-               $http.get(
-                   umbRequestHelper.getApiUrl(
-                       "logApiBaseUrl",
-                       "GetCurrentUserLog",
-                       [{ logtype: type}, {sinceDate: since }])),
-               'Failed to retrieve log data for current user of type ' + type + ' since ' + since);
+                $http.get(
+                    umbRequestHelper.getApiUrl(
+                        "logApiBaseUrl",
+                        "GetCurrentUserLog",
+                        [{ logtype: type }, { sinceDate:  dateToValidIsoString(since) }])),
+                'Failed to retrieve log data for current user of type ' + type + ' since ' + since);
         },
 
         /**
@@ -158,16 +248,16 @@ function logResource($q, $http, umbRequestHelper) {
          * @returns {Promise} resourcePromise object containing the log.
          *
          */
-        getLog: function (type, since) {            
+        getLog: function(type, since) {
             return umbRequestHelper.resourcePromise(
-               $http.get(
-                   umbRequestHelper.getApiUrl(
-                       "logApiBaseUrl",
-                       "GetLog",
-                       [{ logtype: type}, {sinceDate: since }])),
-               'Failed to retrieve log data of type ' + type + ' since ' + since);
-        }
-    };
+                $http.get(
+                    umbRequestHelper.getApiUrl(
+                        "logApiBaseUrl",
+                        "GetLog",
+                        [{ logtype: type }, { sinceDate: dateToValidIsoString(since) }])),
+                'Failed to retrieve log data of type ' + type + ' since ' + since);
+        }      
+};
 }
 
 angular.module('umbraco.resources').factory('logResource', logResource);
diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs
index 5122a458f6..04e34e91e0 100644
--- a/src/Umbraco.Web/Editors/LogController.cs
+++ b/src/Umbraco.Web/Editors/LogController.cs
@@ -1,18 +1,13 @@
-using System;
+using AutoMapper;
+using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using System.Web.Http;
-using AutoMapper;
-using Umbraco.Core;
-using Umbraco.Web.Models.ContentEditing;
 using umbraco.BusinessLogic;
+using Umbraco.Core;
 using Umbraco.Core.Models;
 using Umbraco.Core.Persistence.DatabaseModelDefinitions;
 using Umbraco.Core.Persistence.Querying;
+using Umbraco.Web.Models.ContentEditing;
 using Umbraco.Web.Mvc;
 
 namespace Umbraco.Web.Editors
@@ -52,31 +47,33 @@ namespace Umbraco.Web.Editors
             var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null;
             var result = Services.AuditService.GetPagedItemsByUser(Security.GetUserId(), pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter:dateQuery);
             var mapped = Mapper.Map>(result);
-            return new PagedResult(totalRecords, pageNumber + 1, pageSize)
+            return new PagedResult(totalRecords, pageNumber, pageSize)
             {
                 Items = MapAvatarsAndNames(mapped)
             };
-        }
-
-        [Obsolete("Use GetPagedLog instead")]
+        }
+
+        [Obsolete("Use GetPagedEntityLog instead")]
         public IEnumerable GetEntityLog(int id)
         {
             long totalRecords;
             var result = Services.AuditService.GetPagedItemsByEntity(id, 1, int.MaxValue, out totalRecords);
             return Mapper.Map>(result);
         }
-
-        //TODO: Move to CurrentUserController?
+       
         [Obsolete("Use GetPagedCurrentUserLog instead")]
         public IEnumerable GetCurrentUserLog(AuditType logType, DateTime? sinceDate)
         {
             long totalRecords;
+
+            if (sinceDate == null)
+                sinceDate = DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0, 0));
+
             var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null;
-            var result = Services.AuditService.GetPagedItemsByUser(Security.GetUserId(), 1, int.MaxValue, out totalRecords, auditTypeFilter: new[] {logType},customFilter: dateQuery);
+            var result = Services.AuditService.GetPagedItemsByUser(Security.GetUserId(), 0, int.MaxValue, out totalRecords, auditTypeFilter: new[] {logType},customFilter: dateQuery);
             return Mapper.Map>(result);
         }
-
-        [Obsolete("Use GetPagedLog instead")]
+        
         public IEnumerable GetLog(AuditType logType, DateTime? sinceDate)
         {
             if (sinceDate == null)

From b0374695b15c4532b3dba1e1f62b2075924ba187 Mon Sep 17 00:00:00 2001
From: Dave Woestenborghs 
Date: Wed, 26 Sep 2018 21:51:20 +0200
Subject: [PATCH 070/124] Fixed udi's rendered in rte content (#2531)

---
 .../Configuration/UmbracoConfig.cs            |  2 +-
 .../UmbracoSettings/ContentElement.cs         | 22 ++++++++---
 .../UmbracoSettings/IContentSection.cs        |  4 +-
 src/Umbraco.Tests/Umbraco.Tests.csproj        |  3 ++
 .../Web/TemplateUtilitiesTests.cs             | 37 +++++++++++++++++--
 src/Umbraco.Tests/packages.config             |  1 +
 .../Templates/TemplateUtilities.cs            | 33 ++++++++++++++++-
 7 files changed, 89 insertions(+), 13 deletions(-)

diff --git a/src/Umbraco.Core/Configuration/UmbracoConfig.cs b/src/Umbraco.Core/Configuration/UmbracoConfig.cs
index 82d90073e8..5b6d27b9c5 100644
--- a/src/Umbraco.Core/Configuration/UmbracoConfig.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoConfig.cs
@@ -202,4 +202,4 @@ namespace Umbraco.Core.Configuration
 
         //TODO: Add other configurations here !
     }
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
index 17523ab3a1..9fe35f7cb5 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
@@ -139,8 +139,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
         internal CommaDelimitedConfigurationElement DisallowedUploadFiles
         {
             get { return GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); }
-        }
-
+        }
+
         [ConfigurationProperty("allowedUploadFiles")]
         internal CommaDelimitedConfigurationElement AllowedUploadFiles
         {
@@ -195,6 +195,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
             get { return GetOptionalTextElement("loginBackgroundImage", string.Empty); }
         }
 
+        [ConfigurationProperty("StripUdiAttributes")]
+        internal InnerTextConfigurationElement StripUdiAttributes
+        {
+            get { return GetOptionalTextElement("StripUdiAttributes", true); }
+        }
+
         string IContentSection.NotificationEmailAddress
         {
             get { return Notifications.NotificationEmailAddress; }
@@ -313,8 +319,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
         IEnumerable IContentSection.DisallowedUploadFiles
         {
             get { return DisallowedUploadFiles; }
-        }
-
+        }
+
         IEnumerable IContentSection.AllowedUploadFiles
         {
             get { return AllowedUploadFiles; }
@@ -357,7 +363,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
         string IContentSection.LoginBackgroundImage
         {
             get { return LoginBackgroundImage; }
-        }
-        
+        }
+
+        bool IContentSection.StripUdiAttributes
+        {
+            get { return StripUdiAttributes; }
+        }
     }
 }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
index d6ee260fa9..6c0d8327f1 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
@@ -75,6 +75,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
         bool EnablePropertyValueConverters { get; }
 
         string LoginBackgroundImage { get; }
+
+        bool StripUdiAttributes { get; }
         
     }
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 86c36a8d76..abed9b8245 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -65,6 +65,9 @@
     
       ..\packages\Examine.0.1.89\lib\net45\Examine.dll
     
+    
+      ..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll
+    
     
       ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll
     
diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs
index f2a04c5f0f..96461519d8 100644
--- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs
+++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs
@@ -1,6 +1,6 @@
 using System;
-using System.Linq;
 using System.Web;
+using HtmlAgilityPack;
 using Moq;
 using NUnit.Framework;
 using Umbraco.Core;
@@ -10,7 +10,6 @@ using Umbraco.Core.Models;
 using Umbraco.Core.Persistence.SqlSyntax;
 using Umbraco.Core.Profiling;
 using Umbraco.Core.Scoping;
-using Umbraco.Core.Services;
 using Umbraco.Tests.TestHelpers;
 using Umbraco.Web;
 using Umbraco.Web.Routing;
@@ -34,6 +33,14 @@ namespace Umbraco.Tests.Web
         [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")]
         //this one has an invalid char so won't match
         [TestCase("hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")]
+        // with a-tag with data-udi attribute, that needs to be stripped
+        [TestCase("hello  world ", "hello  world ")]
+        // with a-tag with data-udi attribute spelled wrong, so don't need stripping
+        [TestCase("hello  world ", "hello  world ")]
+        // with a img-tag with data-udi id, that needs to be strippde
+        [TestCase("hello  world ", "hello  world ")]
+        // with a img-tag with data-udi id spelled wrong, so don't need stripping
+        [TestCase("hello  world ", "hello  world ")]
         public void ParseLocalLinks(string input, string result)
         {
             var serviceCtxMock = MockHelper.GetMockedServiceContext();
@@ -63,7 +70,7 @@ namespace Umbraco.Tests.Web
                 //setup a quick mock of the WebRouting section
                 Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "AutoLegacy")),
                 //pass in the custom url provider
-                new[]{ testUrlProvider.Object },
+                new[] { testUrlProvider.Object },
                 true))
             {
                 var output = TemplateUtilities.ParseInternalLinks(input, umbCtx.UrlProvider);
@@ -71,5 +78,27 @@ namespace Umbraco.Tests.Web
                 Assert.AreEqual(result, output);
             }
         }
+        
+        [Test]
+        public void StripDataUdiAttributesUsingSrtringOnLinks()
+        {
+            var input = "hello  world ";
+            var expected = "hello  world ";
+           
+            var result = TemplateUtilities.StripUdiDataAttributes(input);
+
+            Assert.AreEqual(expected, result);
+        }
+
+        [Test]
+        public void StripDataUdiAttributesUsingStringOnImages()
+        {
+            var input = "hello  world ";
+            var expected = "hello  world ";
+
+            var result = TemplateUtilities.StripUdiDataAttributes(input);
+
+            Assert.AreEqual(expected, result);
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config
index 45afd3279c..e458795917 100644
--- a/src/Umbraco.Tests/packages.config
+++ b/src/Umbraco.Tests/packages.config
@@ -3,6 +3,7 @@
   
   
   
+  
   
   
   
diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs
index a7e6738374..39ac69989a 100644
--- a/src/Umbraco.Web/Templates/TemplateUtilities.cs
+++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs
@@ -1,4 +1,6 @@
-using System;
+using HtmlAgilityPack;
+using System;
+using System.Runtime.CompilerServices;
 using System.Text.RegularExpressions;
 using Umbraco.Core;
 using Umbraco.Core.Configuration;
@@ -46,6 +48,11 @@ namespace Umbraco.Web.Templates
         {
             if (urlProvider == null) throw new ArgumentNullException("urlProvider");
 
+            if(string.IsNullOrEmpty(text))
+            {
+                return text;
+            }
+
             // Parse internal links
             var tags = LocalLinkPattern.Matches(text);
             foreach (Match tag in tags)
@@ -74,6 +81,11 @@ namespace Umbraco.Web.Templates
                 }
             }
 
+            if (UmbracoConfig.For.UmbracoSettings().Content.StripUdiAttributes)
+            {
+                text = StripUdiDataAttributes(text);
+            }
+            
             return text;
         }
 
@@ -102,6 +114,9 @@ namespace Umbraco.Web.Templates
         private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?",
             RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
 
+        private static readonly Regex UdiDataAttributePattern = new Regex("data-udi=\"[^\\\"]*\"",
+            RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
         /// 
         /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path.
         /// 
@@ -145,5 +160,21 @@ namespace Umbraco.Web.Templates
         {
             return text.CleanForXss(ignoreFromClean);
         }
+        
+        /// 
+        /// Strips data-udi attributes from rich text
+        /// 
+        /// A html string
+        /// A string stripped from the data-uid attributes
+        public static string StripUdiDataAttributes(string input)
+        {
+            if (string.IsNullOrEmpty(input))
+            {
+                return string.Empty;
+            }
+
+
+            return UdiDataAttributePattern.Replace(input, string.Empty);
+        }
     }
 }

From 0e6376f4e543ed816a3b6b79d65e5defa97eac4e Mon Sep 17 00:00:00 2001
From: Chris Houston 
Date: Fri, 21 Sep 2018 08:36:01 -0400
Subject: [PATCH 071/124] Updated the URL for the issue tracker.

Also updated the version number to reflect the current version.
---
 src/Umbraco.Web.UI.Client/package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index 017eb5dce2..b55767c797 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -2,14 +2,14 @@
   "author": "Umbraco HQ",
   "name": "umbraco",
   "homepage": "https://github.com/umbraco/umbraco-cms/",
-  "version": "7.1.2",
+  "version": "7.12.0",
   "license": "MIT",
   "repository": {
     "type": "git",
     "url": "https://github.com/umbraco/Umbraco-CMS.git"
   },
   "bugs": {
-    "url": "https://issues.umbraco.org"
+    "url": "https://github.com/umbraco/Umbraco-CMS/issues"
   },
   "engines": {
     "node": ">= 0.8.4"

From 258baad127b5454b3248ef273abafbef39758767 Mon Sep 17 00:00:00 2001
From: Sebastiaan Janssen 
Date: Wed, 26 Sep 2018 22:01:09 +0200
Subject: [PATCH 072/124] Removed uneccessary properties

---
 src/Umbraco.Web.UI.Client/package.json | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index b55767c797..850d344607 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -1,19 +1,4 @@
 {
-  "author": "Umbraco HQ",
-  "name": "umbraco",
-  "homepage": "https://github.com/umbraco/umbraco-cms/",
-  "version": "7.12.0",
-  "license": "MIT",
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/umbraco/Umbraco-CMS.git"
-  },
-  "bugs": {
-    "url": "https://github.com/umbraco/Umbraco-CMS/issues"
-  },
-  "engines": {
-    "node": ">= 0.8.4"
-  },
   "scripts": {
     "install": "bower-installer",
     "test": "karma start test/config/karma.conf.js --singlerun",

From c021cc50ce8532acdd22720753d979b4d5f8e05c Mon Sep 17 00:00:00 2001
From: Marc Goodson 
Date: Wed, 26 Sep 2018 22:16:54 +0100
Subject: [PATCH 073/124] U4-10669 7.8. Show 301 url redirects on info tab too
 (#2452)

---
 .../content/umbcontentnodeinfo.directive.js   | 48 ++++++++++++++-----
 .../common/resources/redirecturls.resource.js | 31 +++++++++++-
 .../content/umb-content-node-info.html        | 18 +++++++
 src/Umbraco.Web.UI/umbraco/config/lang/en.xml |  6 ++-
 .../umbraco/config/lang/en_us.xml             |  6 ++-
 .../RedirectUrlManagementController.cs        | 30 ++++++++++--
 6 files changed, 117 insertions(+), 22 deletions(-)

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 7f9d05aebc..8f613469a3 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
@@ -1,7 +1,7 @@
 (function () {
     'use strict';
 
-    function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper) {
+    function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper, redirectUrlsResource) {
 
         function link(scope, element, attrs, ctrl) {
 
@@ -46,6 +46,9 @@
                 // Make sure to set the node status
                 setNodePublishStatus(scope.node);
 
+                //default setting for redirect url management
+                scope.urlTrackerDisabled = false;
+
                 // Declare a fallback URL for the  directive
                 if (scope.documentType !== null) {
                     scope.previewOpenUrl = '#/settings/documenttypes/edit/' + scope.documentType.id;
@@ -99,7 +102,7 @@
 
                         // get current backoffice user and format dates
                         userService.getCurrentUser().then(function (currentUser) {
-                            angular.forEach(data.items, function(item) {
+                            angular.forEach(data.items, function (item) {
                                 item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL');
                             });
                         });
@@ -116,6 +119,25 @@
                     });
 
             }
+            function loadRedirectUrls() {
+                scope.loadingRedirectUrls = true;
+                //check if Redirect Url Management is enabled
+                redirectUrlsResource.getEnableState().then(function (response) {
+                    scope.urlTrackerDisabled = response.enabled !== true;
+                    if (scope.urlTrackerDisabled === false) {
+
+                        redirectUrlsResource.getRedirectsForContentItem(scope.node.udi)
+                            .then(function (data) {
+                                scope.redirectUrls = data.searchResults;
+                                scope.hasRedirects = (typeof data.searchResults !== 'undefined' && data.searchResults.length > 0);
+                                scope.loadingRedirectUrls = false;
+                            });
+                    }
+                    else {
+                        scope.loadingRedirectUrls = false;
+                    }
+                });
+            }
 
             function setAuditTrailLogTypeColor(auditTrail) {
                 angular.forEach(auditTrail, function (item) {
@@ -136,27 +158,27 @@
 
             function setNodePublishStatus(node) {
                 // deleted node
-                if(node.trashed === true) {
+                if (node.trashed === true) {
                     scope.publishStatus.label = localizationService.localize("general_deleted");
                     scope.publishStatus.color = "danger";
                 }
 
                 // unpublished node
-                if(node.published === false && node.trashed === false) {
+                if (node.published === false && node.trashed === false) {
                     scope.publishStatus.label = localizationService.localize("content_unpublished");
                     scope.publishStatus.color = "gray";
                 }
 
                 // published node
-                if(node.hasPublishedVersion === true && node.publishDate && node.published === true) {
+                if (node.hasPublishedVersion === true && node.publishDate && node.published === true) {
                     scope.publishStatus.label = localizationService.localize("content_published");
                     scope.publishStatus.color = "success";
                 }
 
                 // published node with pending changes
-                if(node.hasPublishedVersion === true && node.publishDate && node.published === false) {
+                if (node.hasPublishedVersion === true && node.publishDate && node.published === false) {
                     scope.publishStatus.label = localizationService.localize("content_publishedPendingChanges");
-                    scope.publishStatus.color = "success"
+                    scope.publishStatus.color = "success";
                 }
 
             }
@@ -252,12 +274,13 @@
                 });
             }
 
-            // load audit trail when on the info tab
+            // load audit trail and redirects when on the info tab
             evts.push(eventsService.on("app.tabChange", function (event, args) {
-                $timeout(function(){
+                $timeout(function () {
                     if (args.id === -1) {
                         isInfoTab = true;
                         loadAuditTrail();
+                        loadRedirectUrls();
                     } else {
                         isInfoTab = false;
                     }
@@ -265,13 +288,14 @@
             }));
 
             // watch for content updates - reload content when node is saved, published etc.
-            scope.$watch('node.updateDate', function(newValue, oldValue){
+            scope.$watch('node.updateDate', function (newValue, oldValue) {
 
-                if(!newValue) { return; }
-                if(newValue === oldValue) { return; }
+                if (!newValue) { return; }
+                if (newValue === oldValue) { return; }
 
                 if(isInfoTab) {
                     loadAuditTrail();
+                    loadRedirectUrls();
                     formatDatesToLocal();
                     setNodePublishStatus(scope.node);
                 }
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js
index ea40c066f0..fb145418ed 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js
@@ -40,6 +40,32 @@
                         { searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })),
                 'Failed to retrieve data for searching redirect urls');
         }
+        /**
+   * @ngdoc function
+   * @name umbraco.resources.redirectUrlResource#getRedirectsForContentItem
+   * @methodOf umbraco.resources.redirectUrlResource
+   * @function
+   *
+   * @description
+   * Used to retrieve RedirectUrls for a specific item of content for Information tab
+   * ##usage
+   * 
+   * redirectUrlsResource.getRedirectsForContentItem("udi:123456")
+   *    .then(function(response) {
+   *
+   *    });
+   * 
+ * @param {String} contentUdi identifier for the content item to retrieve redirects for + */ + function getRedirectsForContentItem(contentUdi) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "RedirectUrlsForContentItem", + { contentUdi: contentUdi })), + 'Failed to retrieve redirects for content: ' + contentUdi); + } function getEnableState() { @@ -50,7 +76,7 @@ "GetEnableState")), 'Failed to retrieve data to check if the 301 redirect is enabled'); } - + /** * @ngdoc function * @name umbraco.resources.redirectUrlResource#deleteRedirectUrl @@ -107,7 +133,8 @@ searchRedirectUrls: searchRedirectUrls, deleteRedirectUrl: deleteRedirectUrl, toggleUrlTracker: toggleUrlTracker, - getEnableState: getEnableState + getEnableState: getEnableState, + getRedirectsForContentItem: getRedirectsForContentItem }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 7e80aa2fca..0706e9596c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -19,6 +19,23 @@ + + + +
+
+ +
+

The following URLs redirect to this content item:

+ +
+
+
+
@@ -61,6 +78,7 @@
+ {{ item.logType }} diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index f7a5ac5ef4..7ec238408b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1688,8 +1688,8 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: + Where is this composition used? + This composition is currently used in the composition of the following content types: @@ -2186,6 +2186,8 @@ To manage your website, simply open the Umbraco back office and start adding con Enable URL tracker Original URL Redirected To + Redirect Url Management + The following URLs redirect to this content item: No redirects have been made When a published page gets renamed or moved a redirect will automatically be made to the new page. Remove diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 655c619095..a7fad7132b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1681,8 +1681,8 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: + Where is this composition used? + This composition is currently used in the composition of the following content types: @@ -2178,6 +2178,8 @@ To manage your website, simply open the Umbraco back office and start adding con Enable URL tracker Original URL Redirected To + Redirect Url Management + The following URLs redirect to this content item: No redirects have been made When a published page gets renamed or moved a redirect will automatically be made to the new page. Remove diff --git a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs index 9fb7a63c9f..f9a2807003 100644 --- a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs @@ -12,6 +12,7 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using File = System.IO.File; +using Umbraco.Core; namespace Umbraco.Web.Editors { @@ -39,7 +40,7 @@ namespace Umbraco.Web.Editors var redirectUrlService = Services.RedirectUrlService; long resultCount; - var redirects = string.IsNullOrWhiteSpace(searchTerm) + var redirects = string.IsNullOrWhiteSpace(searchTerm) ? redirectUrlService.GetAllRedirectUrls(page, pageSize, out resultCount) : redirectUrlService.SearchRedirectUrls(searchTerm, page, pageSize, out resultCount); @@ -53,11 +54,32 @@ namespace Umbraco.Web.Editors searchResult.TotalCount = resultCount; searchResult.CurrentPage = page; searchResult.PageCount = ((int)resultCount + pageSize - 1) / pageSize; - + return searchResult; } - + /// + /// This lists the RedirectUrls for a particular content item + /// Do we need to consider paging here? + /// + /// Udi of content item to retrieve RedirectUrls for + /// + [HttpGet] + public RedirectUrlSearchResult RedirectUrlsForContentItem(string contentUdi) + { + var redirectsResult = new RedirectUrlSearchResult(); + if (GuidUdi.TryParse(contentUdi, out var guidIdi)) + { + var redirectUrlService = Services.RedirectUrlService; + var redirects = redirectUrlService.GetContentRedirectUrls(guidIdi.Guid); + redirectsResult.SearchResults = Mapper.Map>(redirects).ToArray(); + //not doing paging 'yet' + redirectsResult.TotalCount = redirects.Count(); + redirectsResult.CurrentPage = 1; + redirectsResult.PageCount = 1; + } + return redirectsResult; + } [HttpPost] public IHttpActionResult DeleteRedirectUrl(Guid id) { @@ -100,4 +122,4 @@ namespace Umbraco.Web.Editors return Ok(string.Format("URL tracker is now {0}d", action)); } } -} \ No newline at end of file +} From f529a47e0a498876dba551d222e56b816e533c31 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 27 Sep 2018 12:55:21 +0200 Subject: [PATCH 074/124] show unpublish button if a node has a published variant --- .../src/common/services/contenteditinghelper.service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 decbd74150..90919d4fe6 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 @@ -252,7 +252,10 @@ 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) { - if (args.content.publishDate && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) { + + var hasPublishedVariant = args.content.variants.filter(function(variant) { return variant.publishDate; }).length > 0; + + if (hasPublishedVariant && _.contains(args.content.allowedActions, "U") && _.contains(args.content.allowedActions, "Z")) { buttons.subButtons.push(createButtonDefinition("Z")); } } From ea3085a08cec230867d21a81b364df178ba171b1 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 27 Sep 2018 12:56:16 +0200 Subject: [PATCH 075/124] add ellipsis to unpublish button label if a node has variants --- .../src/common/services/contenteditinghelper.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 90919d4fe6..676d0508b2 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 @@ -198,7 +198,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica handler: args.methods.unPublish, hotKey: "ctrl+u", hotKeyWhenHidden: true, - alias: "unpublish" + alias: "unpublish", + addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false" }; default: return null; From e41b435d85318b17986ba7ca698a3429bbc74cb5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 27 Sep 2018 15:03:06 +0200 Subject: [PATCH 076/124] small style adjustment --- .../src/views/content/overlays/publish.html | 4 ++-- .../src/views/content/overlays/save.html | 4 ++-- .../src/views/content/overlays/sendtopublish.html | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index 72565f6f71..92ed6c354c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -21,8 +21,8 @@ style="margin-right: 8px;" val-server-field="{{variant.htmlId}}" />
-
@param {array} colors (attribute): The array of colors. -@param {string} colors (attribute): The array of colors. @param {string} selectedColor (attribute): The selected color. @param {string} size (attribute): The size (s, m). +@param {string} useLabel (attribute): Specify if labels should be used. +@param {string} useColorClass (attribute): Specify if color values are css classes. @param {function} onSelect (expression): Callback function when the item is selected. **/ @@ -28,6 +28,11 @@ Use this directive to generate color swatches to pick from. function link(scope, el, attr, ctrl) { + // Set default to true if not defined + if (angular.isUndefined(scope.useColorClass)) { + scope.useColorClass = false; + } + scope.setColor = function (color) { //scope.selectedColor({color: color }); scope.selectedColor = color; @@ -47,7 +52,9 @@ Use this directive to generate color swatches to pick from. colors: '=?', size: '@', selectedColor: '=', - onSelect: '&' + onSelect: '&', + useLabel: '=', + useColorClass: '=?' }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less index 5363a8db9b..43f6697eb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less @@ -10,6 +10,7 @@ .umb-prevalues-multivalues__left { display: flex; flex: 1 1 auto; + overflow: hidden; } .umb-prevalues-multivalues__right { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less index e27bd0d654..df80aef2ce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less @@ -1,18 +1,22 @@ .umb-color-swatches { + display: flex; + flex-flow: row wrap; .umb-color-box { border: none; color: white; cursor: pointer; - padding: 5px; + padding: 1px; text-align: center; text-decoration: none; - display: inline-block; margin: 5px; border-radius: 3px; width: 30px; height: 30px; transition: box-shadow .3s; + display: flex; + align-items: center; + justify-content: center; &:hover, &:focus { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); @@ -34,4 +38,55 @@ } } } + + &.with-labels { + + .umb-color-box { + width: 120px; + height: 100%; + display: flex; + flex-flow: row wrap; + + .umb-color-box-inner { + display: flex; + flex-flow: column wrap; + flex: 0 0 100%; + max-width: 100%; + min-height: 80px; + padding-top: 10px; + + .umb-color-box__label { + background: #fff; + font-size: 14px; + display: flex; + flex-flow: column wrap; + flex: 0 0 100%; + padding: 1px 5px; + max-width: 100%; + margin-top: auto; + margin-bottom: -3px; + margin-left: -1px; + margin-right: -1px; + text-indent: 0; + text-align: left; + border: 1px solid @gray-8; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + overflow: hidden; + + .umb-color-box__name { + color: @black; + font-weight: bold; + margin-top: 3px; + } + + .umb-color-box__description { + font-size: 12px; + line-height: 1.5em; + color: @gray-3; + } + } + } + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index fabe7c2291..88e5842b91 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -133,33 +133,6 @@ div.umb-codeeditor .umb-btn-toolbar { // // Color picker // -------------------------------------------------- -ul.color-picker li { - padding: 2px; - margin: 3px; - border: 2px solid transparent; - width: 60px; - - &.active { - .check_circle { - opacity: 1; - } - } - - .thumbnail{ - min-width: auto; - width: inherit; - padding: 0; - } - - a { - height: 50px; - display:flex; - align-items: center; - justify-content: center; - cursor:pointer; - margin: 0 0 5px; - } -} /* pre-value editor */ .control-group.color-picker-preval { @@ -180,7 +153,7 @@ ul.color-picker li { div.color-picker-prediv { display: inline-flex; align-items: center; - max-width: 80%; + max-width: 85%; pre { display: inline-flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html index 0098463cb4..de99c2a143 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html @@ -1,5 +1,3 @@ - -
@@ -17,7 +15,11 @@
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html index eae299e579..a89e51ab32 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html @@ -1,8 +1,14 @@ -
+
- diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index 8e4a8d2a63..ccea7519ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -1,20 +1,15 @@ 
-
- You haven't defined any colors -
+
+ You haven't defined any colors +
- + + - +
From 3c73f6a764ab49accd275c35d19f76a47f0dedca Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 30 Sep 2018 10:59:20 +0200 Subject: [PATCH 087/124] Rename name property so title gets set for the doctype icon picker - relates to PR #2864 --- .../iconpicker/iconpicker.controller.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js index 3c36505fcb..d9f1c7b6e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js @@ -12,25 +12,25 @@ function IconPickerOverlay($scope, iconHelper, localizationService) { $scope.model.hideSubmitButton = false; $scope.colors = [ - { name: "Black", value: "color-black" }, - { name: "Blue Grey", value: "color-blue-grey" }, - { name: "Grey", value: "color-grey" }, - { name: "Brown", value: "color-brown" }, - { name: "Blue", value: "color-blue" }, - { name: "Light Blue", value: "color-light-blue" }, - { name: "Indigo", value: "color-indigo" }, - { name: "Purple", value: "color-purple" }, - { name: "Deep Purple", value: "color-deep-purple" }, - { name: "Cyan", value: "color-cyan" }, - { name: "Green", value: "color-green" }, - { name: "Light Green", value: "color-light-green" }, - { name: "Lime", value: "color-lime" }, - { name: "Yellow", value: "color-yellow" }, - { name: "Amber", value: "color-amber" }, - { name: "Orange", value: "color-orange" }, - { name: "Deep Orange", value: "color-deep-orange" }, - { name: "Red", value: "color-red" }, - { name: "Pink", value: "color-pink" } + { label: "Black", value: "color-black" }, + { label: "Blue Grey", value: "color-blue-grey" }, + { label: "Grey", value: "color-grey" }, + { label: "Brown", value: "color-brown" }, + { label: "Blue", value: "color-blue" }, + { label: "Light Blue", value: "color-light-blue" }, + { label: "Indigo", value: "color-indigo" }, + { label: "Purple", value: "color-purple" }, + { label: "Deep Purple", value: "color-deep-purple" }, + { label: "Cyan", value: "color-cyan" }, + { label: "Green", value: "color-green" }, + { label: "Light Green", value: "color-light-green" }, + { label: "Lime", value: "color-lime" }, + { label: "Yellow", value: "color-yellow" }, + { label: "Amber", value: "color-amber" }, + { label: "Orange", value: "color-orange" }, + { label: "Deep Orange", value: "color-deep-orange" }, + { label: "Red", value: "color-red" }, + { label: "Pink", value: "color-pink" } ]; if (!$scope.color) { From 5f1fb144e08093b685cf35a4266da59c71cc3b7f Mon Sep 17 00:00:00 2001 From: imranhaidercogworks Date: Sun, 30 Sep 2018 12:09:44 +0100 Subject: [PATCH 088/124] Uploading SVG's Causes Error (Depending on imageFileTypes Setting) (#3072) --- .../UmbracoSettings/IContentSection.cs | 2 +- src/Umbraco.Core/Constants-Conventions.cs | 7 ++- src/Umbraco.Core/IO/MediaFileSystem.cs | 57 ++++++++++--------- src/Umbraco.Core/Media/Exif/ImageFile.cs | 36 ++++++------ .../Media/Exif/ImageFileFormat.cs | 4 ++ src/Umbraco.Core/Media/Exif/SVGFile.cs | 37 ++++++++++++ .../Media/TypeDetector/JpegDetector.cs | 14 +++++ .../TypeDetector/RasterizedTypeDetector.cs | 16 ++++++ .../Media/TypeDetector/SVGDetector.cs | 24 ++++++++ .../Media/TypeDetector/TIFFDetector.cs | 24 ++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 5 ++ .../imaging/umbimagegravity.directive.js | 36 +++++++----- .../imagecropper/imagecropper.html | 4 +- .../config/umbracoSettings.config | 4 +- src/Umbraco.Web/Editors/MediaController.cs | 2 +- .../FileUploadPropertyValueEditor.cs | 4 +- .../ImageCropperPropertyValueEditor.cs | 2 +- src/umbraco.businesslogic/IO/IOHelper.cs | 32 +++++------ 18 files changed, 225 insertions(+), 85 deletions(-) create mode 100644 src/Umbraco.Core/Media/Exif/SVGFile.cs create mode 100644 src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs create mode 100644 src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs create mode 100644 src/Umbraco.Core/Media/TypeDetector/SVGDetector.cs create mode 100644 src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 6c0d8327f1..bfcbabaccd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings IEnumerable ImageTagAllowedAttributes { get; } IEnumerable ImageAutoFillProperties { get; } - + string ScriptFolderPath { get; } IEnumerable ScriptFileTypes { get; } diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index a19e59f4ef..a56b40005e 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -110,6 +110,11 @@ namespace Umbraco.Core /// Property alias for the Media's file extension. ///
public const string Extension = "umbracoExtension"; + + /// + /// The default height/width of an image file if the size can't be determined from the metadata + /// + public const int DefaultSize = 200; } /// @@ -354,4 +359,4 @@ namespace Umbraco.Core } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index fc6490e8cd..37fcdeba70 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -17,15 +17,15 @@ using Umbraco.Core.Models; namespace Umbraco.Core.IO { - /// - /// A custom file system provider for media - /// - [FileSystemProvider("media")] - public class MediaFileSystem : FileSystemWrapper - { - private readonly IContentSection _contentConfig; + /// + /// A custom file system provider for media + /// + [FileSystemProvider("media")] + public class MediaFileSystem : FileSystemWrapper + { + private readonly IContentSection _contentConfig; private readonly UploadAutoFillProperties _uploadAutoFillProperties; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly object _folderCounterLock = new object(); private long _folderCounter; @@ -39,8 +39,8 @@ namespace Umbraco.Core.IO }; public MediaFileSystem(IFileSystem wrapped) - : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content, ApplicationContext.Current.ProfilingLogger.Logger) - { } + : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content, ApplicationContext.Current.ProfilingLogger.Logger) + { } public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig, ILogger logger) : base(wrapped) @@ -60,13 +60,13 @@ namespace Umbraco.Core.IO [Obsolete("This low-level method should NOT exist.")] public string GetRelativePath(int propertyId, string fileName) - { + { var sep = _contentConfig.UploadAllowDirectories - ? Path.DirectorySeparatorChar - : '-'; + ? Path.DirectorySeparatorChar + : '-'; - return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; - } + return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; + } [Obsolete("This low-level method should NOT exist.", false)] public string GetRelativePath(string subfolder, string fileName) @@ -264,7 +264,7 @@ namespace Umbraco.Core.IO var filename = Path.GetFileName(sourcepath); var filepath = GetMediaPath(filename, content.Key, propertyType.Key); this.CopyFile(sourcepath, filepath); - + return filepath; } @@ -321,7 +321,7 @@ namespace Umbraco.Core.IO /// /// private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) - { + { // will use filepath for extension, and filestream for length _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); } @@ -368,19 +368,20 @@ namespace Umbraco.Core.IO return new Size(width, height); } } + + //we have no choice but to try to read in via GDI + using (var image = Image.FromStream(stream)) + { + + var fileWidth = image.Width; + var fileHeight = image.Height; + return new Size(fileWidth, fileHeight); + } } catch (Exception) { //We will just swallow, just means we can't read exif data, we don't want to log an error either - } - - //we have no choice but to try to read in via GDI - using (var image = Image.FromStream(stream)) - { - - var fileWidth = image.Width; - var fileHeight = image.Height; - return new Size(fileWidth, fileHeight); + return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); } } @@ -430,8 +431,8 @@ namespace Umbraco.Core.IO } } - public void DeleteMediaFiles(IEnumerable files) - { + public void DeleteMediaFiles(IEnumerable files) + { files = files.Distinct(); Parallel.ForEach(files, file => diff --git a/src/Umbraco.Core/Media/Exif/ImageFile.cs b/src/Umbraco.Core/Media/Exif/ImageFile.cs index 66350338e9..05a2a955c4 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFile.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFile.cs @@ -2,6 +2,7 @@ using System.Drawing; using System.IO; using System.Text; +using Umbraco.Core.Media.TypeDetector; namespace Umbraco.Core.Media.Exif { @@ -118,22 +119,25 @@ namespace Umbraco.Core.Media.Exif /// The created from the file. public static ImageFile FromStream(Stream stream, Encoding encoding) { - stream.Seek (0, SeekOrigin.Begin); - byte[] header = new byte[8]; - stream.Seek (0, SeekOrigin.Begin); - if (stream.Read (header, 0, header.Length) != header.Length) - throw new NotValidImageFileException (); - - // JPEG - if (header[0] == 0xFF && header[1] == 0xD8) - return new JPEGFile (stream, encoding); - - // TIFF - string tiffHeader = Encoding.ASCII.GetString (header, 0, 4); - if (tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00") - return new TIFFFile (stream, encoding); - - throw new NotValidImageFileException (); + // JPEG + if(JPEGDetector.IsOfType(stream)) + { + return new JPEGFile(stream, encoding); + } + + // TIFF + if (TIFFDetector.IsOfType(stream)) + { + return new TIFFFile(stream, encoding); + } + + // SVG + if (SVGDetector.IsOfType(stream)) + { + return new SVGFile(stream); + } + + throw new NotValidImageFileException (); } #endregion } diff --git a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs index 8e1433c947..964b33e6ea 100644 --- a/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs +++ b/src/Umbraco.Core/Media/Exif/ImageFileFormat.cs @@ -17,5 +17,9 @@ /// The file is a TIFF File. /// TIFF, + /// + /// The file is a SVG File. + /// + SVG, } } diff --git a/src/Umbraco.Core/Media/Exif/SVGFile.cs b/src/Umbraco.Core/Media/Exif/SVGFile.cs new file mode 100644 index 0000000000..bc4d94749c --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/SVGFile.cs @@ -0,0 +1,37 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Umbraco.Core.Media.Exif +{ + internal class SVGFile : ImageFile + { + public SVGFile(Stream fileStream) + { + fileStream.Position = 0; + + var document = XDocument.Load(fileStream); //if it throws an exception the ugly try catch in MediaFileSystem will catch it + + var width = document.Root?.Attributes().Where(x => x.Name == "width").Select(x => x.Value).FirstOrDefault(); + var height = document.Root?.Attributes().Where(x => x.Name == "height").Select(x => x.Value).FirstOrDefault(); + + Properties.Add(new ExifSInt(ExifTag.PixelYDimension, + height == null ? Constants.Conventions.Media.DefaultSize : int.Parse(height))); + Properties.Add(new ExifSInt(ExifTag.PixelXDimension, + width == null ? Constants.Conventions.Media.DefaultSize : int.Parse(width))); + + Format = ImageFileFormat.SVG; + } + + public override void Save(Stream stream) + { + } + + public override Image ToImage() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs new file mode 100644 index 0000000000..5b68f6ad88 --- /dev/null +++ b/src/Umbraco.Core/Media/TypeDetector/JpegDetector.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace Umbraco.Core.Media.TypeDetector +{ + public class JPEGDetector : RasterizedTypeDetector + { + public static bool IsOfType(Stream fileStream) + { + var header = GetFileHeader(fileStream); + + return header[0] == 0xff && header[1] == 0xD8; + } + } +} diff --git a/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs new file mode 100644 index 0000000000..9777d55c20 --- /dev/null +++ b/src/Umbraco.Core/Media/TypeDetector/RasterizedTypeDetector.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Umbraco.Core.Media.TypeDetector +{ + public abstract class RasterizedTypeDetector + { + public static byte[] GetFileHeader(Stream fileStream) + { + fileStream.Seek(0, SeekOrigin.Begin); + byte[] header = new byte[8]; + fileStream.Seek(0, SeekOrigin.Begin); + + return header; + } + } +} diff --git a/src/Umbraco.Core/Media/TypeDetector/SVGDetector.cs b/src/Umbraco.Core/Media/TypeDetector/SVGDetector.cs new file mode 100644 index 0000000000..ab9659ca1e --- /dev/null +++ b/src/Umbraco.Core/Media/TypeDetector/SVGDetector.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Xml.Linq; + +namespace Umbraco.Core.Media.TypeDetector +{ + public class SVGDetector + { + public static bool IsOfType(Stream fileStream) + { + var document = new XDocument(); + + try + { + document = XDocument.Load(fileStream); + } + catch (System.Exception ex) + { + return false; + } + + return document.Root?.Name.LocalName == "svg"; + } + } +} diff --git a/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs new file mode 100644 index 0000000000..2a6e42d0e0 --- /dev/null +++ b/src/Umbraco.Core/Media/TypeDetector/TIFFDetector.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Text; + +namespace Umbraco.Core.Media.TypeDetector +{ + public class TIFFDetector + { + public static bool IsOfType(Stream fileStream) + { + string tiffHeader = GetFileHeader(fileStream); + + return tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00"; + } + + public static string GetFileHeader(Stream fileStream) + { + var header = RasterizedTypeDetector.GetFileHeader(fileStream); + + string tiffHeader = Encoding.ASCII.GetString(header, 0, 4); + + return tiffHeader; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index aa3b2db784..c7393e524f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -365,6 +365,11 @@ + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index e47032fed3..a7f053fcae 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -31,10 +31,10 @@ angular.module("umbraco.directives") //elements var $viewport = element.find(".viewport"); var $image = element.find("img"); - var $overlay = element.find(".overlay"); + var $overlay = element.find(".overlay"); - scope.style = function () { - if(scope.dimensions.width <= 0){ + scope.style = function () { + if (scope.dimensions.width <= 0) { setDimensions(); } @@ -45,23 +45,29 @@ angular.module("umbraco.directives") }; scope.setFocalPoint = function(event) { + scope.$emit("imageFocalPointStart"); - scope.$emit("imageFocalPointStart"); + var offsetX = event.offsetX - 10; + var offsetY = event.offsetY - 10; - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; - - calculateGravity(offsetX, offsetY); - - lazyEndEvent(); + calculateGravity(offsetX, offsetY); + lazyEndEvent(); }; - var setDimensions = function(){ - scope.dimensions.width = $image.width(); - scope.dimensions.height = $image.height(); - - if(scope.center){ + var setDimensions = function () { + if (scope.src.endsWith(".svg")) { + // svg files don't automatically get a size by + // loading them set a default size for now + $image.attr("width", "200"); + $image.attr("height", "200"); + // can't crop an svg file, don't show the focal point + $overlay.remove(); + } + scope.dimensions.width = $image.width(); + scope.dimensions.height = $image.height(); + + if(scope.center){ scope.dimensions.left = scope.center.left * scope.dimensions.width -10; scope.dimensions.top = scope.center.top * scope.dimensions.height -10; }else{ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index e1c0118497..5c6f156aab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -46,8 +46,8 @@ Remove file - -
    + +
    • - jpeg,jpg,gif,bmp,png,tiff,tif + jpeg,jpg,gif,bmp,png,tiff,tif,svg src,alt,border,class,style,align,id,name,onclick,usemap @@ -105,7 +105,7 @@ throw - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,svg,php,htaccess + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 86e35ccc73..a7bc3c2707 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -942,4 +942,4 @@ namespace Umbraco.Web.Editors return hasPathAccess; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 777a14b768..72ca5c5c59 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -110,7 +110,7 @@ namespace Umbraco.Web.PropertyEditors _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! var ext = _mediaFileSystem.GetExtension(filepath); - if (_mediaFileSystem.IsImageFile(ext)) + if (_mediaFileSystem.IsImageFile(ext) && ext != ".svg") { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; @@ -137,4 +137,4 @@ namespace Umbraco.Web.PropertyEditors return string.Join(",", newPaths.Select(x => _mediaFileSystem.GetUrl(x))); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 0e6500f3d8..ccee138486 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -154,7 +154,7 @@ namespace Umbraco.Web.PropertyEditors _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! var ext = _mediaFileSystem.GetExtension(filepath); - if (_mediaFileSystem.IsImageFile(ext)) + if (_mediaFileSystem.IsImageFile(ext) && ext != ".svg") { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; diff --git a/src/umbraco.businesslogic/IO/IOHelper.cs b/src/umbraco.businesslogic/IO/IOHelper.cs index 8a7e10d9eb..295e0df82d 100644 --- a/src/umbraco.businesslogic/IO/IOHelper.cs +++ b/src/umbraco.businesslogic/IO/IOHelper.cs @@ -13,9 +13,9 @@ using umbraco.businesslogic.Exceptions; namespace umbraco.IO { - [Obsolete("Use Umbraco.Core.IO.IOHelper instead")] + [Obsolete("Use Umbraco.Core.IO.IOHelper instead")] public static class IOHelper - { + { public static char DirSepChar { get @@ -27,42 +27,42 @@ namespace umbraco.IO //helper to try and match the old path to a new virtual one public static string FindFile(string virtualPath) { - return Umbraco.Core.IO.IOHelper.FindFile(virtualPath); + return Umbraco.Core.IO.IOHelper.FindFile(virtualPath); } //Replaces tildes with the root dir public static string ResolveUrl(string virtualPath) { - return Umbraco.Core.IO.IOHelper.ResolveUrl(virtualPath); + return Umbraco.Core.IO.IOHelper.ResolveUrl(virtualPath); } - [Obsolete("Use Umbraco.Web.Templates.TemplateUtilities.ResolveUrlsFromTextString instead, this method on this class will be removed in future versions")] + [Obsolete("Use Umbraco.Web.Templates.TemplateUtilities.ResolveUrlsFromTextString instead, this method on this class will be removed in future versions")] public static string ResolveUrlsFromTextString(string text) { - return Umbraco.Core.IO.IOHelper.ResolveUrlsFromTextString(text); + return Umbraco.Core.IO.IOHelper.ResolveUrlsFromTextString(text); } public static string MapPath(string path, bool useHttpContext) { - return Umbraco.Core.IO.IOHelper.MapPath(path, useHttpContext); + return Umbraco.Core.IO.IOHelper.MapPath(path, useHttpContext); } public static string MapPath(string path) { - return Umbraco.Core.IO.IOHelper.MapPath(path); + return Umbraco.Core.IO.IOHelper.MapPath(path); } //use a tilde character instead of the complete path - [Obsolete("This method is no longer in use and will be removed in future versions")] + [Obsolete("This method is no longer in use and will be removed in future versions")] public static string returnPath(string settingsKey, string standardPath, bool useTilde) { - return Umbraco.Core.IO.IOHelper.ReturnPath(settingsKey, standardPath, useTilde); + return Umbraco.Core.IO.IOHelper.ReturnPath(settingsKey, standardPath, useTilde); } - [Obsolete("This method is no longer in use and will be removed in future versions")] + [Obsolete("This method is no longer in use and will be removed in future versions")] public static string returnPath(string settingsKey, string standardPath) { - return Umbraco.Core.IO.IOHelper.ReturnPath(settingsKey, standardPath); + return Umbraco.Core.IO.IOHelper.ReturnPath(settingsKey, standardPath); } @@ -75,12 +75,12 @@ namespace umbraco.IO /// true if valid, throws a FileSecurityException if not public static bool ValidateEditPath(string filePath, string validDir) { - return Umbraco.Core.IO.IOHelper.ValidateEditPath(filePath, validDir); + return Umbraco.Core.IO.IOHelper.ValidateEditPath(filePath, validDir); } - public static bool ValidateFileExtension(string filePath, List validFileExtensions) + public static bool ValidateFileExtension(string filePath, List validFileExtensions) { - return Umbraco.Core.IO.IOHelper.ValidateFileExtension(filePath, validFileExtensions); + return Umbraco.Core.IO.IOHelper.ValidateFileExtension(filePath, validFileExtensions); } @@ -92,7 +92,7 @@ namespace umbraco.IO /// private static string getRootDirectorySafe() { - return Umbraco.Core.IO.IOHelper.GetRootDirectorySafe(); + return Umbraco.Core.IO.IOHelper.GetRootDirectorySafe(); } } From 093182c56da32a257c35c7478621f36fac99ca70 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 30 Sep 2018 13:12:11 +0200 Subject: [PATCH 089/124] Allow svg uploads --- src/Umbraco.Web.UI/config/umbracoSettings.Release.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 88d350225b..af5b302ff0 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -51,7 +51,7 @@ throw - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,svg,php,htaccess + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess Textstring From c5de33dcae07c3fe5fbbdfa5e6512904a5aadf49 Mon Sep 17 00:00:00 2001 From: Mark Drake Date: Sun, 30 Sep 2018 07:38:19 -0400 Subject: [PATCH 090/124] Added an `active` css class for Umbraco Tree items (#3056) --- .../components/tree/umbtreeitem.directive.js | 12 ++++++++++-- .../src/common/services/navigation.service.js | 4 +++- src/Umbraco.Web.UI.Client/src/less/tree.less | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 0bb888a59f..93ea0d9ced 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -18,7 +18,7 @@ */ angular.module("umbraco.directives") -.directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService) { +.directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService, appState) { return { restrict: 'E', replace: true, @@ -132,7 +132,15 @@ angular.module("umbraco.directives") } if (node.selected) { css.push("umb-tree-node-checked"); - } + } + + //is this the current action node (this is not the same as the current selected node!) + var actionNode = appState.getMenuState("currentNode"); + if(actionNode) { + if(actionNode.id === node.id) { + css.push("active"); + } + } return css.join(" "); }; 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 840066adef..da2fd243e6 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 @@ -73,6 +73,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo appState.setSectionState("showSearchResults", false); appState.setGlobalState("stickyNavigation", false); appState.setGlobalState("showTray", false); + appState.setMenuState("currentNode", null); if (appState.getGlobalState("isTablet") === true) { appState.setGlobalState("showNavigation", false); @@ -347,7 +348,8 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) { //reset it to whatever is in the url - appState.setSectionState("currentSection", $routeParams.section); + appState.setSectionState("currentSection", $routeParams.section); + setMode("default-hidesectiontree"); } diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index acba5f1ac8..fae99ced3e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -185,7 +185,8 @@ line-height: 16px; } -.umb-tree div:hover { +.umb-tree div:hover, +.umb-tree div.active { background: @gray-10; } From 8fad7183470ddac9424eb2ce6ed195ea93baf373 Mon Sep 17 00:00:00 2001 From: KimHolzmann Date: Sun, 30 Sep 2018 14:08:01 +0200 Subject: [PATCH 091/124] #2916 Media picker searches entire media archive (#3061) --- .../directives/components/umbmediagrid.directive.js | 12 +++++++++++- .../overlays/mediaPicker/mediapicker.controller.js | 2 +- .../common/overlays/mediaPicker/mediapicker.html | 11 +++++++++-- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 1 + 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 12179076cd..9aa4965022 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -125,6 +125,14 @@ Use this directive to generate a thumbnail grid of media items. i--; } + if (scope.includeSubFolders !== 'true') { + if (item.parentId !== parseInt(scope.currentFolderId)) { + scope.items.splice(i, 1); + i--; + } + } + + } if (scope.items.length > 0) { @@ -307,7 +315,9 @@ Use this directive to generate a thumbnail grid of media items. itemMaxHeight: "@", itemMinWidth: "@", itemMinHeight: "@", - onlyImages: "@" + onlyImages: "@", + includeSubFolders: "@", + currentFolderId: "@" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 1b4c533123..5e582eb9c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -314,7 +314,7 @@ angular.module("umbraco") function searchMedia() { $scope.loading = true; - entityResource.getPagedDescendants($scope.startNodeId, "Media", $scope.searchOptions) + entityResource.getPagedDescendants($scope.currentFolder.id, "Media", $scope.searchOptions) .then(function(data) { // update image data to work with image grid angular.forEach(data.items, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index dce749be55..b93a2ccf3c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -11,7 +11,6 @@ -
      @@ -83,7 +88,9 @@ item-max-height="150" item-min-width="100" item-min-height="100" - only-images={{onlyImages}}> + only-images={{onlyImages}} + include-sub-folders={{showChilds}} + current-Folder-id="{{currentFolder.id}}">
      diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 7ec238408b..92263b4955 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -684,6 +684,7 @@ Embed Retrieve selected + Include subfolders in search diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index a7fad7132b..31838eb346 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -684,6 +684,7 @@ Embed Retrieve selected + Include subfolders in search Black From 63a2a155d116ed2620705eec696a94ef5021f797 Mon Sep 17 00:00:00 2001 From: agrath Date: Mon, 1 Oct 2018 02:03:52 +1300 Subject: [PATCH 092/124] User invite flow review (#3000) --- .../Security/BackOfficeUserManager.cs | 6 +++- .../Security/BackOfficeUserStore.cs | 4 ++- .../views/common/dialogs/login.controller.js | 3 ++ .../src/views/common/dialogs/login.html | 12 +++++-- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 3 +- .../umbraco/config/lang/en_us.xml | 3 +- .../Editors/AuthenticationController.cs | 32 +++++++++++++++++-- .../Editors/BackOfficeController.cs | 21 ++++++++---- 8 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 83db1a8904..fde188ff27 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -236,7 +236,10 @@ namespace Umbraco.Core.Security if (dataProtectionProvider != null) { - manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) + { + TokenLifespan = TimeSpan.FromDays(3) + }; } manager.UserLockoutEnabledByDefault = true; @@ -748,6 +751,7 @@ namespace Umbraco.Core.Security var httpContext = HttpContext.Current == null ? (HttpContextBase)null : new HttpContextWrapper(HttpContext.Current); return httpContext.GetCurrentRequestIpAddress(); } + } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index e0b91ce175..c6e3399b73 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -630,7 +630,9 @@ namespace Umbraco.Core.Security || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) { anythingChanged = true; - user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); + //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); + user.LastLoginDate = dt; } if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 3f50926b47..9b703a0987 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -29,6 +29,7 @@ function init() { // Check if it is a new user var inviteVal = $location.search().invite; + //1 = enter password, 2 = password set, 3 = invalid token if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { $q.all([ @@ -58,6 +59,8 @@ $scope.inviteStep = Number(inviteVal); }); + } else if (inviteVal && inviteVal === "3") { + $scope.inviteStep = Number(inviteVal); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index b4e330c13e..3b48bcc9f1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -99,10 +99,18 @@
      - + +