diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs index 54d6063ad78c..c666547c1d5d 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -69,7 +69,9 @@ public void Create(string connectionString) * always initializing in this way and it probably helps for non azure scenarios also (anytime persisting on a cifs mount for example). */ - var tempFile = Path.GetTempFileName(); + // Create a random file name using cryptographically strong random number generator (RNGCryptoServiceProvider) + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempConnectionString = new SqliteConnectionStringBuilder { DataSource = tempFile, Pooling = false }; using (var connection = new SqliteConnection(tempConnectionString.ConnectionString)) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 03240db872d0..7047e1643717 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -276,7 +276,7 @@ public static class RelationTypes public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; /// - /// Returns the types of relations that are automatically tracked + /// Returns the types of relations that are automatically tracked. /// /// /// Developers should not manually use these relation types since they will all be cleared whenever an entity @@ -284,7 +284,7 @@ public static class RelationTypes /// public static string[] AutomaticRelationTypes { get; } = { RelatedMediaAlias, RelatedDocumentAlias }; - // TODO: return a list of built in types so we can use that to prevent deletion in the uI + // TODO: return a list of built in types so we can use that to prevent deletion in the UI } public static class Udi diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml index 29b242d67ec0..0057d3d9a638 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml @@ -1194,7 +1194,6 @@ Vyberte verzi, kterou chcete porovnat s aktuální verzí - Současná verze Červený text nebude ve vybrané verzi zobrazen, zelený znamená přidaný].]]> Dokument byl vrácen na starší verzi Tohle zobrazuje vybranou verzi jako html, jestliže chcete vidět rozdíly mezi 2 verzemi najednou, použijte rozdílové zobrazení diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index e34cc2ab29ad..012a1bc0b93d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -1523,8 +1523,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Dewis fersiwn i gymharu efo fersiwn bresennol Newidiadau - Creuwyd - Fersiwn bresennol Ni fydd testun coch yn cael ei ddangos yn y fersiwn dewiswyd. , mae gwyrdd yn golygu wedi'i ychwanegu]]> Dogfen wedi'i rolio yn ôl Mae hyn yn dangos y fersiwn dewiswyd ar ffurf HTML, os hoffwch weld y gwahaniaeth rhwng 2 fersiwn ar yr un pryd, defnyddiwch y wedd gwahaniaethol diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 879a6b4a622f..7fbc7d0b5bd5 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1238,11 +1238,10 @@ Mange hilsner fra Umbraco robotten Ændringer - Oprettet Vælg en version at sammenligne med den nuværende version - Nuværende version Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> + Der er ingen forskelle mellem den nuværende version og den valgte version Dokument tilbagerullet Her vises den valgte version som html. Hvis du ønsker at se forskellen mellem de 2 versioner på samme tid, brug 'diff'-oversigten diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/de.xml b/src/Umbraco.Core/EmbeddedResources/Lang/de.xml index 24999a118740..448453778bd8 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/de.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/de.xml @@ -1260,7 +1260,6 @@ Wählen Sie eine Version, um diese mit der aktuellen zu vergleichen - Aktuelle Version Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. Dokument wurde zurückgesetzt Zeigt die ausgewählte Version als HTML an. Wenn Sie sich die Unterschiede zwischen zwei Versionen anzeigen lassen wollen, benutzen Sie bitte die Vergleichsansicht. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index af7b39849c44..e7d3649b4f83 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1450,10 +1450,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Changes - Created - Current version - Red text will be removed in the selected version, green text will be added]]> + Red text will be removed in the selected version, green text will be added]]> + There are no differences between the current (draft) version and the selected version Document has been rolled back Select a version to compare with the current version This displays the selected version as HTML, if you wish to see the difference between 2 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 4678acad16b4..16ccd5c8d1eb 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1490,11 +1490,10 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Changes - Created Select a version to compare with the current version - Current version - Red text will be removed in the selected version, green text will be added]]> + Red text will be removed in the selected version, green text will be added]]> + There are no differences between the current (draft) version and the selected version Document has been rolled back This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view @@ -1504,7 +1503,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont View Versions - Current draft version Current published version diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 1ed96d88078b..4a111302b350 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -898,7 +898,6 @@ Reiniciar - Versión actual Red el texto de la versión seleccionada no se mostrará. , el verde significa añadido]]> Se ha recuperado la última versión del documento. Esto muestra la versión seleccionada como html, si deseas ver la diferencia entre 2 versiones al mismo tiempo, por favor usa la vista diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml index 24d5f565e507..384981e46711 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml @@ -1251,7 +1251,6 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Sélectionnez une version à comparer avec la version actuelle - Version actuelle Le texte en Rouge signifie qu'il a été supprimé de la version choisie, vert signifie ajouté]]> Le document a été restauré à une version antérieure Ceci affiche la version choisie en tant que HTML, si vous souhaitez voir les différences entre les deux versions en même temps, utilisez la vue différentielle diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml index c52961307d2c..819e71aec9b9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/he.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/he.xml @@ -589,7 +589,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont הסר קישור - גירסה עדכנית טקסט אדום לא יוצג בגרסא שנבחרה, טקסט ירוק מייצט טקסט שנוסף.]]> המסמך שוחזר בהצלחה להלן הגרסא שנבחרה כHTML, אם הינך לצפות בשינויים בין שתי הגרסאות בו זמנית, בחר ב diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml index cea82fc4e076..c85274b80fa9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml @@ -1530,9 +1530,7 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in Modifiche - Creato Seleziona una versione da confrontare con la versione corrente - Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml index bcf9e8c9a937..28d512973fa4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ja.xml @@ -730,7 +730,6 @@ Runwayをインストールして作られた新しいウェブサイトがど リセット - 現在の版 の文字列は以前の版にはない部分で、緑の文字列は以前の版にのみある部分です。]]> ドキュメントは以前の版に戻りました 選択した版をhtmlで表示します。2つの版の比較を表示したいときは、Diff を選択してください。 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml index 6a20975bb1bc..dfe848387c1e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ko.xml @@ -575,7 +575,6 @@ 링크 삭제 - 현재 버전 빨간 텍스트는 선택한 버전에선 보이지 않습니다. 녹색은 추가되었음을 의미합니다]]> 문서가 롤백되었습니다. 선택한 버전을 html로 보여줍니다. 두 버전의 차이점을 동시에 보시려면, 차이점 보기를 사용하세요 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml index 87bcb3138ab0..c19aa1df4f55 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nb.xml @@ -621,7 +621,6 @@ Vennlig hilsen Umbraco roboten Nullstill - Gjeldende versjon Rød tekst vil ikke bli vist i den valgte versjonen. , grønn betyr lagt til]]> Dokumentet er tilbakeført til en tidligere versjon Dette viser den valgte versjonen som HTML, bruk avviksvisningen hvis du ønsker å se forksjellene mellom to versjoner samtidig. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index b2cbb5bda03a..735302e951f6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1253,9 +1253,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Wijzigingen - Aangemaakt Selecteer een versie om te vergelijken met de huidige versie - Huidige versie Rode tekst wordt niet getoond in de geselecteerde versie, groen betekent toegevoegd]]> Document is teruggezet @@ -1267,7 +1265,6 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Bekijk Versies - Conceptversie Gepubliceerde versie diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml index 4801371fd841..a4d891fd8fe8 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml @@ -881,7 +881,6 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Resetuj - Aktualna wersja Czerwony tekst nie będzie pokazany w wybranej wersji, zielony tekst został dodany]]> Dokument został przywrócony Tu widać wybraną wersję jako html, jeżeli chcesz zobaczyć różnicę pomiędzy 2 wersjami w tym samym czasie, użyj podglądu różnic diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml index 25060a4bd34d..b5fd920365f6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pt.xml @@ -565,7 +565,6 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Remover Link - Versão atual Texto vermelho não será mostrado na versão selecionada; verde significa adicionado]]> Documento foi revertido Isto mostra a versão selecionada como html se você deseja ver as diferenças entre as 2 versões ao mesmo tempo use a visão em diff diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml index d5b31e47d22a..f83dd5285ce4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml @@ -1195,7 +1195,6 @@ '%0%' была переименована в '%1%' - Текущая версия Красным отмечен текст, которого уже нет в последней версии, зеленым - текст, который добавлен]]> Произведен откат к ранней версии Текущая версия показана в виде HTML. Для просмотра различий в версиях выберите режим сравнения diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml index fa359fbbbc4e..e2e7cbcd769d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml @@ -675,7 +675,6 @@ Återställ - Nuvarande version Röd text kommer inte att synas i den valda versionen. , Grön betyder att den har tillkommit]]> Dokumentet har återgått till en tidigare version Här visas den valda sidversionen i HTML. Om du vill se skillnaden mellan två versioner samtidigt, välj istället "Diff". diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml index 47549f5f40c8..30a38ba6c7b8 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml @@ -1319,8 +1319,6 @@ Web sitenizi yönetmek için, Umbraco'nun arka ofisini açın ve içerik eklemey Değişiklikler - Oluşturuldu - Mevcut sürüm Kırmızı metin seçili sürümde gösterilmeyecektir. , yeşil eklendi demektir ]]> Belge geri alındı ​​ Mevcut sürümle karşılaştırmak için bir sürüm seçin diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml index 5d9a6e9ab359..199b185ed331 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh.xml @@ -763,7 +763,6 @@ Reset - 当前版本 红色是选中版本中没有的。绿色是新增的]]> 文档已回滚 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml index b3e3b7bdcf21..d57c1c614db5 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml @@ -753,7 +753,6 @@ 重設 - 當前版本 紅色 文字將不會顯示於所選版本,而綠色表示增加部分。]]> 文檔已回滾 這顯示所選版本的HTML格式,如果您想要比較兩版本的差異,請使用比較檢視 diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index d36baed60401..b12cbe58eff3 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -23,19 +23,15 @@ public static class PropertyTagsExtensions IDataEditor? editor = propertyEditors[property.PropertyType?.PropertyEditorAlias]; TagsPropertyEditorAttribute? tagAttribute = editor?.GetTagAttribute(); - if (tagAttribute == null) - { - return null; - } var configurationObject = property.PropertyType is null ? null : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; - TagConfiguration? configuration = ConfigurationEditor.ConfigurationAs(configurationObject); + TagConfiguration? configuration = configurationObject as TagConfiguration; if (configuration is not null && configuration.Delimiter == default) { - configuration.Delimiter = tagAttribute.Delimiter; + configuration.Delimiter = tagAttribute?.Delimiter ?? ','; } return configuration; diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index 1dec9946d306..1184f2524f47 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -16,6 +16,13 @@ public class BlockListConfiguration [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] public NumberRange ValidationLimit { get; set; } = new(); + [ConfigurationField("useSingleBlockMode", "Single block mode", "boolean", + Description = @"When in Single block mode, the output will be BlockListItem<>, instead of BlockListModel. + +**NOTE:** +Single block mode requires a maximum of one available block, and an amount set to minimum 1 and maximum 1 blocks.")] + public bool UseSingleBlockMode { get; set; } + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] public bool UseLiveEditing { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index 39d7d7e1309a..6c863d66ffec 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -14,4 +14,10 @@ public interface IDataValueReference /// /// IEnumerable GetReferences(object? value); + + /// + /// Returns all reference types that are automatically tracked. + /// + /// + IEnumerable GetAutomaticRelationTypesAliases() => Enumerable.Empty(); } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs b/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs new file mode 100644 index 000000000000..f809e787bc48 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueTags.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Resolve tags from values +/// +public interface IDataValueTags +{ + /// + /// Returns any tags contained in the value + /// + /// + /// + /// + /// + IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId); +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs index ff92c2012f2b..8206ab538bb4 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorTagsExtensions.cs @@ -5,6 +5,7 @@ namespace Umbraco.Extensions; /// /// Provides extension methods for the interface to manage tags. /// +[Obsolete] public static class PropertyEditorTagsExtensions { /// diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 849d6446a93b..d6f1584e8d94 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; /// Marks property editors that support tags. /// [AttributeUsage(AttributeTargets.Class)] +[Obsolete("Implement a custom IDataValueEditor with the IDataValueTags interface instead")] public class TagsPropertyEditorAttribute : Attribute { /// diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 09cfcf5aaf53..8ebd803aac73 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -1,7 +1,10 @@ using System.Collections; using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Mapping; @@ -44,19 +47,32 @@ public class UmbracoMapper : IUmbracoMapper private readonly ConcurrentDictionary>> _ctors = new(); - private readonly ConcurrentDictionary>> _maps = + private readonly ConcurrentDictionary>> _maps = new(); private readonly ICoreScopeProvider _scopeProvider; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// /// - public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) + [Obsolete("Please use ctor that takes an ILogger")] + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) : this(profiles, scopeProvider, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The MapDefinitionCollection + /// The scope provider + /// The logger + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider, ILogger logger) { _scopeProvider = scopeProvider; + _logger = logger; foreach (IMapDefinition profile in profiles) { @@ -119,15 +135,15 @@ public void Define( sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; } - Dictionary> sourceMaps = DefineMaps(sourceType); + ConcurrentDictionary> sourceMaps = DefineMaps(sourceType); sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); } private Dictionary> DefineCtors(Type sourceType) => _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); - private Dictionary> DefineMaps(Type sourceType) => - _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + private ConcurrentDictionary> DefineMaps(Type sourceType) => + _maps.GetOrAdd(sourceType, _ => new ConcurrentDictionary>()); #endregion @@ -428,7 +444,7 @@ public TTarget Map(TSource source, TTarget target, MapperConte return null; } - if (_maps.TryGetValue(sourceType, out Dictionary>? sourceMap) && + if (_maps.TryGetValue(sourceType, out ConcurrentDictionary>? sourceMap) && sourceMap.TryGetValue(targetType, out Action? map)) { return map; @@ -436,7 +452,7 @@ public TTarget Map(TSource source, TTarget target, MapperConte // we *may* run this more than once but it does not matter map = null; - foreach ((Type stype, Dictionary> smap) in _maps) + foreach ((Type stype, ConcurrentDictionary> smap) in _maps) { if (!stype.IsAssignableFrom(sourceType)) { @@ -462,9 +478,9 @@ public TTarget Map(TSource source, TTarget target, MapperConte { foreach (KeyValuePair> m in sourceMap) { - if (!_maps[sourceType].TryGetValue(m.Key, out _)) + if (!_maps[sourceType].TryAdd(m.Key, m.Value)) { - _maps[sourceType].Add(m.Key, m.Value); + _logger.LogDebug("Duplicate key was found, don't add to dictionary"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 1b01a3ba47d3..cc2e20fa0276 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -1745,13 +1747,62 @@ private void CreatePropertyTypeData() } } - private void CreateLanguageData() => - ConditionalInsert( - Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - "en-us", - new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, - Constants.DatabaseSchema.Tables.Language, - "id"); + private void CreateLanguageData() + { + // For languages we support the installation of records that are additional to the default installed data. + // We can do this as they are specified by ISO code, which is enough to fully detail them. + // All other customizable install data is specified by GUID, and hence we only know about the set that are installed by default. + InstallDefaultDataSettings? languageInstallDefaultDataSettings = _installDefaultDataSettings.Get(Constants.Configuration.NamedOptions.InstallDefaultData.Languages); + if (languageInstallDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values) + { + // Insert the specified languages, ensuring the first is marked as default. + bool isDefault = true; + foreach (var isoCode in languageInstallDefaultDataSettings.Values) + { + if (!TryCreateCulture(isoCode, out CultureInfo? culture)) + { + continue; + } + + var dto = new LanguageDto + { + IsoCode = culture.Name, + CultureName = culture.EnglishName, + IsDefault = isDefault, + }; + _database.Insert(Constants.DatabaseSchema.Tables.Language, "id", true, dto); + isDefault = false; + } + } + else + { + // Conditionally insert the default language. + if (TryCreateCulture("en-US", out CultureInfo? culture)) + { + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + culture.Name, + new LanguageDto { Id = 1, IsoCode = culture.Name, CultureName = culture.EnglishName, IsDefault = true }, + Constants.DatabaseSchema.Tables.Language, + "id"); + } + } + } + + private bool TryCreateCulture(string isoCode, [NotNullWhen(true)] out CultureInfo? culture) + { + try + { + culture = CultureInfo.GetCultureInfo(isoCode); + return true; + } + catch (CultureNotFoundException ex) + { + _logger.LogWarning(ex, "CultureInfo could not be created because culture '{IsoCode}' is not available. The language will not be created.", isoCode); + culture = null; + return false; + } + } private void CreateContentChildTypeData() { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 0162ba8d5239..ce14f1ec1e0b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -274,29 +274,59 @@ protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonS { foreach (IProperty property in entity.Properties) { - TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); - if (tagConfiguration == null) + if (PropertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out var editor) is false) { - continue; // not a tags property + continue; + } + + if (editor.GetValueEditor() is not IDataValueTags tagsProvider) + { + // support for legacy tag editors, everything from here down to the last continue can be removed when TagsPropertyEditorAttribute is removed + TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); + if (tagConfiguration == null) + { + continue; + } + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (IPropertyValue pvalue in property.Values) + { + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + IEnumerable cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + tags.AddRange(cultureTags); + } + + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings + IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + + continue; // not implementing IDataValueTags, continue } + object? configuration = DataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + if (property.PropertyType.VariesByCulture()) { var tags = new List(); foreach (IPropertyValue pvalue in property.Values) { - IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); - IEnumerable cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); - tags.AddRange(cultureTags); + tags.AddRange(tagsProvider.GetTags(pvalue.EditedValue, configuration, languageId)); } tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } else { - IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings - IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + IEnumerable tags = tagsProvider.GetTags(property.GetValue(), configuration, null); tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } } @@ -1042,8 +1072,10 @@ protected void PersistRelations(TEntity entity) var trackedRelations = new List(); trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); + var relationTypeAliases = GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors).ToArray(); + // First delete all auto-relations for this entity - RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); + RelationRepository.DeleteByParent(entity.Id, relationTypeAliases); if (trackedRelations.Count == 0) { @@ -1055,7 +1087,10 @@ protected void PersistRelations(TEntity entity) .ToDictionary(x => (Udi)x!, x => x!.Guid); // lookup in the DB all INT ids for the GUIDs and chuck into a dictionary - var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values)) + var keyToIds = Database.Fetch(Sql() + .Select(x => x.NodeId, x => x.UniqueId) + .From() + .WhereIn(x => x.UniqueId, udiToGuids.Values)) .ToDictionary(x => x.UniqueId, x => x.NodeId); var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? @@ -1085,6 +1120,31 @@ protected void PersistRelations(TEntity entity) RelationRepository.SaveBulk(toSave); } + private IEnumerable GetAutomaticRelationTypesAliases( + IPropertyCollection properties, + PropertyEditorCollection propertyEditors) + { + var automaticRelationTypesAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes); + + foreach (IProperty property in properties) + { + if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? editor) is false ) + { + continue; + } + + if (editor.GetValueEditor() is IDataValueReference reference) + { + foreach (var alias in reference.GetAutomaticRelationTypesAliases()) + { + automaticRelationTypesAliases.Add(alias); + } + } + } + + return automaticRelationTypesAliases; + } + /// /// Inserts property values for the content entity /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 11b381064012..fbf22398285a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference +internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags { private BlockEditorValues? _blockEditorValues; private readonly IDataTypeService _dataTypeService; @@ -77,6 +77,40 @@ public IEnumerable GetReferences(object? value) return result; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) + { + return Enumerable.Empty(); + } + + var result = new List(); + // loop through all content and settings data + foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (valueEditor is not IDataValueTags tagsProvider) + { + continue; + } + + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + + result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + } + } + + return result; + } + #region Convert database // editor // note: there is NO variant support here diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index cf85339d4881..29a3f57a923e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -69,7 +69,7 @@ private static bool IsSystemPropertyKey(string propKey) => protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags { private readonly IDataTypeService _dataTypeService; private readonly ILogger _logger; @@ -150,6 +150,36 @@ public IEnumerable GetReferences(object? value) return result; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + IReadOnlyList rows = + _nestedContentValues.GetPropertyValues(value); + + var result = new List(); + + foreach (NestedContentValues.NestedContentRowValue row in rows.ToList()) + { + foreach (KeyValuePair prop in row.PropertyValues + .ToList()) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (valueEditor is not IDataValueTags tagsProvider) + { + continue; + } + + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + + result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + } + } + + return result; + } + #region DB to String public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 88648c47fda9..ff646a039d4b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -68,18 +69,67 @@ protected override IDataValueEditor CreateValueEditor() => protected override IConfigurationEditor CreateConfigurationEditor() => new TagConfigurationEditor(_validators, _ioHelper, _localizedTextService, _editorConfigurationParser); - internal class TagPropertyValueEditor : DataValueEditor + internal class TagPropertyValueEditor : DataValueEditor, IDataValueTags { + private readonly IDataTypeService _dataTypeService; + public TagPropertyValueEditor( ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataEditorAttribute attribute) + DataEditorAttribute attribute, + IDataTypeService dataTypeService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { + _dataTypeService = dataTypeService; } + /// + public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + var strValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(strValue)) return Enumerable.Empty(); + + var tagConfiguration = ConfigurationEditor.ConfigurationAs(dataTypeConfiguration) ?? new TagConfiguration(); + + if (tagConfiguration.Delimiter == default) + tagConfiguration.Delimiter = ','; + + IEnumerable tags; + + switch (tagConfiguration.StorageType) + { + case TagsStorageType.Csv: + tags = strValue.Split(new[] { tagConfiguration.Delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + break; + + case TagsStorageType.Json: + try + { + tags = JsonConvert.DeserializeObject(strValue)?.Select(x => x.Trim()) ?? Enumerable.Empty(); + } + catch (JsonException) + { + //cannot parse, malformed + tags = Enumerable.Empty(); + } + + break; + + default: + throw new NotSupportedException($"Value \"{tagConfiguration.StorageType}\" is not a valid TagsStorageType."); + } + + return tags.Select(x => new Tag + { + Group = tagConfiguration.Group, + Text = x, + LanguageId = languageId, + }); + } + + /// public override IValueRequiredValidator RequiredValidator => new RequiredJsonValueValidator(); @@ -93,14 +143,33 @@ public TagPropertyValueEditor( return null; } + var tagConfiguration = editorValue.DataTypeConfiguration as TagConfiguration ?? new TagConfiguration(); + if (tagConfiguration.Delimiter == default) + tagConfiguration.Delimiter = ','; + + string[] trimmedTags = Array.Empty(); + if (editorValue.Value is JArray json) { - return json.HasValues ? json.Select(x => x.Value()) : null; + trimmedTags = json.HasValues ? json.Select(x => x.Value()).OfType().ToArray() : Array.Empty(); + } + else if (string.IsNullOrWhiteSpace(value) == false) + { + trimmedTags = value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); } - if (string.IsNullOrWhiteSpace(value) == false) + if (trimmedTags.Length == 0) { - return value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return null; + } + + switch (tagConfiguration.StorageType) + { + case TagsStorageType.Csv: + return string.Join(tagConfiguration.Delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(); + + case TagsStorageType.Json: + return trimmedTags.Length == 0 ? null : JsonConvert.SerializeObject(trimmedTags); } return null; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index af1c51c37e30..b56f43e0efbb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -1,9 +1,14 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using static Umbraco.Cms.Core.PropertyEditors.BlockListConfiguration; @@ -12,18 +17,70 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; [DefaultPropertyValueConverter(typeof(JsonValueConverter))] public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase { + private readonly IContentTypeService _contentTypeService; + private readonly BlockEditorConverter _blockConverter; + private readonly BlockListEditorDataConverter _blockListEditorDataConverter; private readonly IProfilingLogger _proflog; - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) + [Obsolete("Use the constructor with the IContentTypeService")] + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) : this(proflog, blockConverter, StaticServiceProvider.Instance.GetRequiredService()) { } + + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService) : base(blockConverter) { _proflog = proflog; + _blockConverter = blockConverter; + _blockListEditorDataConverter = new BlockListEditorDataConverter(); + _contentTypeService = contentTypeService; } /// public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + var isSingleBlockMode = IsSingleBlockMode(propertyType.DataType); + if (isSingleBlockMode) + { + BlockListConfiguration.BlockConfiguration? block = + ConfigurationEditor.ConfigurationAs(propertyType.DataType.Configuration)?.Blocks.FirstOrDefault(); + + ModelType? contentElementType = block?.ContentElementTypeKey is Guid contentElementTypeKey && _contentTypeService.Get(contentElementTypeKey) is IContentType contentType ? ModelType.For(contentType.Alias) : null; + ModelType? settingsElementType = block?.SettingsElementTypeKey is Guid settingsElementTypeKey && _contentTypeService.Get(settingsElementTypeKey) is IContentType settingsType ? ModelType.For(settingsType.Alias) : null; + + if (contentElementType is not null) + { + if (settingsElementType is not null) + { + return typeof(BlockListItem<,>).MakeGenericType(contentElementType, settingsElementType); + } + + return typeof(BlockListItem<>).MakeGenericType(contentElementType); + } + + return typeof(BlockListItem); + } + + return typeof(BlockListModel); + } + + private bool IsSingleBlockMode(PublishedDataType dataType) + { + BlockListConfiguration? config = + ConfigurationEditor.ConfigurationAs(dataType.Configuration); + return (config?.UseSingleBlockMode ?? false) && config?.Blocks.Length == 1 && config?.ValidationLimit?.Min == 1 && config?.ValidationLimit?.Max == 1; + } + + /// + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + /// + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source?.ToString(); + /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { @@ -44,7 +101,7 @@ public override bool IsConverter(IPublishedPropertyType propertyType) BlockListModel blockModel = UnwrapBlockModel(referenceCacheLevel, inter, preview, configuration.Blocks, CreateEmptyModel, CreateModel); - return blockModel; + return IsSingleBlockMode(propertyType.DataType) ? blockModel.FirstOrDefault() : blockModel; } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index d1f92953adec..26643da9f0f4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -467,7 +467,7 @@ public IActionResult Delete(string type, string virtualPath) { case Constants.Trees.PartialViews: if (IsDirectory( - _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.PartialViews, virtualPath)))) + _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.PartialViews, virtualPath)), _fileSystems.PartialViewsFileSystem!)) { _fileService.DeletePartialViewFolder(virtualPath); return Ok(); @@ -482,7 +482,7 @@ public IActionResult Delete(string type, string virtualPath) case Constants.Trees.PartialViewMacros: if (IsDirectory( - _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.MacroPartials, virtualPath)))) + _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.MacroPartials, virtualPath)), _fileSystems.MacroPartialsFileSystem!)) { _fileService.DeletePartialViewMacroFolder(virtualPath); return Ok(); @@ -497,7 +497,7 @@ public IActionResult Delete(string type, string virtualPath) case Constants.Trees.Scripts: if (IsDirectory( - _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoScriptsPath, virtualPath)))) + _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoScriptsPath, virtualPath)), _fileSystems.ScriptsFileSystem!)) { _fileService.DeleteScriptFolder(virtualPath); return Ok(); @@ -512,7 +512,7 @@ public IActionResult Delete(string type, string virtualPath) return new UmbracoProblemResult("No Script or folder found with the specified path", HttpStatusCode.NotFound); case Constants.Trees.Stylesheets: if (IsDirectory( - _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoCssPath, virtualPath)))) + _hostingEnvironment.MapPathWebRoot(Path.Combine(_globalSettings.UmbracoCssPath, virtualPath)), _fileSystems.StylesheetsFileSystem!)) { _fileService.DeleteStyleSheetFolder(virtualPath); return Ok(); @@ -827,13 +827,21 @@ private string NormalizeVirtualPath(string? virtualPath, string systemDirectory) return value; } - private bool IsDirectory(string path) + private bool IsDirectory(string path, IFileSystem fileSystem) { - var dirInfo = new DirectoryInfo(path); + // If it's a physical filesystem check with directory info + if (fileSystem.CanAddPhysical) + { + var dirInfo = new DirectoryInfo(path); + + // If you turn off indexing in Windows this will have the attribute: + // `FileAttributes.Directory | FileAttributes.NotContentIndexed` + return (dirInfo.Attributes & FileAttributes.Directory) != 0; + } - // If you turn off indexing in Windows this will have the attribute: - // `FileAttributes.Directory | FileAttributes.NotContentIndexed` - return (dirInfo.Attributes & FileAttributes.Directory) != 0; + // Otherwise check the filesystem abstraction to see if the folder exists + // Since this is used for delete, it presumably exists if we're trying to delete it + return fileSystem.DirectoryExists(path); } // this is an internal class for passing stylesheet data from the client to the controller while editing diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index 795257800a03..36a60843fb35 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -146,7 +146,9 @@ internal void MapPropertyValuesForPersistence( // set the value - tags are special TagsPropertyEditorAttribute? tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); - if (tagAttribute != null) + // when TagsPropertyEditorAttribute is removed this whole if can also be removed + // since the call to sovePropertyValue is all that's needed now + if (tagAttribute is not null && valueEditor is not IDataValueTags) { TagConfiguration? tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType?.Configuration); diff --git a/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs index 55001ca28c6a..0748f5cbb475 100644 --- a/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/PreviewAuthenticationMiddleware.cs @@ -51,17 +51,23 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. - if (cookieOptions.Cookie.Name is not null && - request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) + if (cookieOptions.Cookie.Name != null) { - AuthenticationTicket? unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie); - ClaimsIdentity? backOfficeIdentity = unprotected?.Principal.GetUmbracoIdentity(); - if (backOfficeIdentity != null) + var chunkingCookieManager = new ChunkingCookieManager(); + var cookie = chunkingCookieManager.GetRequestCookie(context, cookieOptions.Cookie.Name); + + if (!string.IsNullOrEmpty(cookie)) { - // Ok, we've got a real ticket, now we can add this ticket's identity to the current - // Principal, this means we'll have 2 identities assigned to the principal which we can - // use to authorize the preview and allow for a back office User. - context.User.AddIdentity(backOfficeIdentity); + AuthenticationTicket? unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie); + ClaimsIdentity? backOfficeIdentity = unprotected?.Principal.GetUmbracoIdentity(); + + if (backOfficeIdentity != null) + { + // Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Principal, this means we'll have 2 identities assigned to the principal which we can + // use to authorize the preview and allow for a back office User. + context.User.AddIdentity(backOfficeIdentity); + } } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index bddd95fbbe34..02f6164f64a8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -323,7 +323,6 @@ $scope.defaultButton = buttons.defaultButton; $scope.subButtons = buttons.subButtons; $scope.page.showPreviewButton = true; - } /** Syncs the content item to it's tree node - this occurs on first load and after saving */ @@ -938,22 +937,25 @@ }; $scope.preview = function (content) { - // Chromes popup blocker will kick in if a window is opened - // without the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?init=true', 'umbpreview'); - - // Build the correct path so both /#/ and #/ work. - var query = 'id=' + content.id; - if ($scope.culture) { - query += "#?culture=" + $scope.culture; + + const openPreviewWindow = () => { + // Chromes popup blocker will kick in if a window is opened + // without the initial scoped request. This trick will fix that. + // + const previewWindow = $window.open('preview/?init=true', 'umbpreview'); + + // Build the correct path so both /#/ and #/ work. + let query = 'id=' + content.id; + if ($scope.culture) { + query += "#?culture=" + $scope.culture; + } + previewWindow.location.href = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query; } - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query; //The user cannot save if they don't have access to do that, in which case we just want to preview //and that's it otherwise they'll get an unauthorized access message if (!_.contains(content.allowedActions, "A")) { - previewWindow.location.href = redirect; + openPreviewWindow(); } else { var selectedVariant = $scope.content.variants[0]; @@ -967,10 +969,12 @@ } } - //ensure the save flag is set + //reset save flag for all variants + $scope.content.variants.forEach(variant => variant.save = false); + //ensure the save flag is set for the active variant selectedVariant.save = true; performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function (data) { - previewWindow.location.href = redirect; + openPreviewWindow() }, function (err) { //validation issues .... }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 864e07f17c2f..d461a3b0bc36 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -206,7 +206,6 @@ * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. */ var notSupportedProperties = [ - "Umbraco.Tags", "Umbraco.UploadField", "Umbraco.ImageCropper", "Umbraco.NestedContent" @@ -654,7 +653,7 @@ blockObject.__scope.$evalAsync(); }); }); - + observer.observe(labelElement[0], {characterData: true, subtree:true}); blockObject.__watchers.push(() => { @@ -671,9 +670,9 @@ $index: this.index + 1, ... this.data }; - + this.__labelScope = Object.assign(this.__labelScope, labelVars); - + $compile(labelElement.contents())(this.__labelScope); }.bind(blockObject) } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index ec30c7e4503c..b4531276134d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -38,12 +38,18 @@ angular.module("umbraco") "disabled": vm.model.clipboardItems.length === 0 }]; - if (vm.model.openClipboard === true) { + if (vm.model.singleBlockMode === true && vm.model.openClipboard === true) { + vm.navigation.splice(0,1); + vm.activeTab = vm.navigation[0]; + } + else if (vm.model.openClipboard === true) { vm.activeTab = vm.navigation[1]; } else { vm.activeTab = vm.navigation[0]; } + + vm.activeTab.active = true; } ); @@ -55,10 +61,16 @@ angular.module("umbraco") }; vm.clickClearClipboard = function () { - vm.onNavigationChanged(vm.navigation[0]); - vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. vm.model.clickClearClipboard(); + if (vm.model.singleBlockMode !== true && vm.model.openClipboard !== true) + { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. + } + else { + vm.close(); + } }; vm.model = $scope.model; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index d270a7171ebf..d59bbcefcfa2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -73,11 +73,13 @@ getVersions(); } - function changeVersion(version) { + function canRollback(version) { + return !version.currentDraftVersion; + } - const canRollback = !version.currentDraftVersion && !version.currentPublishedVersion; + function changeVersion(version) { - if (canRollback === false) { + if (canRollback(version) === false) { return; } @@ -92,6 +94,7 @@ vm.loadingDiff = true; const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; + vm.previousVersion = null; contentResource.getRollbackVersion(version.versionId, culture) .then(function(data) { vm.previousVersion = data; @@ -99,8 +102,11 @@ vm.previousVersion.displayValue = version.displayValue + ' - ' + version.username; createDiff(vm.currentVersion, vm.previousVersion); + const changed = (part) => part.added || part.removed; + vm.diffHasChanges = vm.diff.name.some(changed) || vm.diff.properties.some((property) => property.diff.some(changed)); + vm.loadingDiff = false; - vm.rollbackButtonDisabled = false; + vm.rollbackButtonDisabled = !vm.diffHasChanges; }, function () { vm.loadingDiff = false; }); @@ -131,11 +137,14 @@ // get current backoffice user and format dates userService.getCurrentUser().then(function (currentUser) { - vm.previousVersions = data.items.map(version => { - var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); - version.displayValue = timestampFormatted; - return version; - }); + vm.previousVersions = data.items + // we don't ever want to show the draft version in the rollback list + .filter(version => version.currentDraftVersion === false) + .map(version => { + var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); + version.displayValue = timestampFormatted; + return version; + }); }); }); } @@ -173,7 +182,7 @@ // copy existing properties, so it doesn't manipulate existing properties on page oldProperty = Utilities.copy(oldProperty); property = Utilities.copy(property); - + // we have to make properties storing values as object into strings (Grid, nested content, etc.) if (property.value instanceof Object) { property.value = JSON.stringify(property.value, null, 1); @@ -188,14 +197,14 @@ // diff requires a string property.value = property.value ? property.value + '' : ''; oldProperty.value = oldProperty.value ? oldProperty.value + '' : ''; - + const diffProperty = { 'alias': property.alias, 'label': property.label, 'diff': property.isObject ? Diff.diffJson(property.value, oldProperty.value) : Diff.diffWords(property.value, oldProperty.value), 'isObject': property.isObject || oldProperty.isObject }; - + vm.diff.properties.push(diffProperty); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index 2ad16e83f6b9..e6f2a1c3a52e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -21,7 +21,7 @@ - @@ -42,17 +42,12 @@
Language
-
- Current version: - {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}}) -
-
-
@@ -61,12 +56,11 @@
Language
{{version.username}}
Current version - Current version
- Language
- + - - This shows the differences between the current version and the selected version
Red text will be + + This shows the differences between the current (draft) version and the selected version
Red text will be removed in the selected version, green text will be added
+ + There are no differences between the current (draft) version and the selected version +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less index e7953a4feada..50c66fb9f439 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.less @@ -14,12 +14,6 @@ position: relative; } - .current-version { - background: @gray-10; - padding: 15px; - margin-bottom: 12px; - } - .culture-select { margin-bottom: 12px; } @@ -29,4 +23,4 @@ font-size: 13px; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html index 85668c078686..cecc3caab90b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html @@ -132,6 +132,7 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js index a7be634d0cbe..acf4dd16adc8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js @@ -72,7 +72,6 @@ const unsubscribe = []; const vm = this; - vm.invalidAmount = false; vm.areaConfig = null; vm.locallyAvailableBlockTypes = 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 30151accc9c7..a13826b86b27 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -13,6 +13,7 @@ ng-click="vm.requestShowCreate($index, $event)" ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl" ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)" + ng-if="!vm.singleBlockMode" ng-show="!vm.readonly">
@@ -28,7 +29,7 @@
-
+
-