diff --git a/build/build.ps1 b/build/build.ps1 index c998412744..b800d45fa3 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -366,9 +366,9 @@ $ubuild.DefineMethod("PrepareBuild", { - $this.TempStoreFile("$($this.SolutionRoot)\src\Umbraco.Web.UI\web.config") - Write-Host "Create clean web.config" - $this.CopyFile("$($this.SolutionRoot)\src\Umbraco.Web.UI\web.Template.config", "$($this.SolutionRoot)\src\Umbraco.Web.UI\web.config") + # $this.TempStoreFile("$($this.SolutionRoot)\src\Umbraco.Web.UI\web.config") + # Write-Host "Create clean web.config" + # $this.CopyFile("$($this.SolutionRoot)\src\Umbraco.Web.UI\web.Template.config", "$($this.SolutionRoot)\src\Umbraco.Web.UI\web.config") Write-host "Set environment" $env:UMBRACO_VERSION=$this.Version.Semver.ToString() diff --git a/build/templates/UmbracoProject/Views/_ViewImports.cshtml b/build/templates/UmbracoProject/Views/_ViewImports.cshtml index 3aaf133335..25e6e66533 100644 --- a/build/templates/UmbracoProject/Views/_ViewImports.cshtml +++ b/build/templates/UmbracoProject/Views/_ViewImports.cshtml @@ -1,4 +1,6 @@ @using Umbraco.Web.UI.NetCore @using Umbraco.Extensions @using Umbraco.Web.PublishedModels +@using Umbraco.Cms.Core.Models.PublishedContent +@using Microsoft.AspNetCore.Html @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Umbraco.Core/Actions/ActionCollection.cs b/src/Umbraco.Core/Actions/ActionCollection.cs index 3987e89305..2084df3dad 100644 --- a/src/Umbraco.Core/Actions/ActionCollection.cs +++ b/src/Umbraco.Core/Actions/ActionCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models.Membership; @@ -8,15 +9,12 @@ namespace Umbraco.Cms.Core.Actions { public class ActionCollection : BuilderCollectionBase { - public ActionCollection(IEnumerable items) - : base(items) - { } + public ActionCollection(Func> items) : base(items) + { + } public T GetAction() - where T : IAction - { - return this.OfType().FirstOrDefault(); - } + where T : IAction => this.OfType().FirstOrDefault(); public IEnumerable GetByLetters(IEnumerable letters) { diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs index 2c9007cbe9..a61dd81368 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherCollection.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Composing; @@ -7,11 +7,11 @@ namespace Umbraco.Cms.Core.Cache { public class CacheRefresherCollection : BuilderCollectionBase { - public CacheRefresherCollection(IEnumerable items) - : base(items) - { } + public CacheRefresherCollection(Func> items) : base(items) + { + } public ICacheRefresher this[Guid id] - => this.FirstOrDefault(x => x.RefresherUniqueId == id); + => this.FirstOrDefault(x => x.RefresherUniqueId == id); } } diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs index a5bf33f9c1..1af9511fb7 100644 --- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs +++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs @@ -1,43 +1,34 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; namespace Umbraco.Cms.Core.Composing { + /// /// Provides a base class for builder collections. /// /// The type of the items. public abstract class BuilderCollectionBase : IBuilderCollection { - private readonly TItem[] _items; + private readonly LazyReadOnlyCollection _items; - /// /// Initializes a new instance of the with items. /// /// The items. - protected BuilderCollectionBase(IEnumerable items) - { - _items = items.ToArray(); - } + public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); /// - public int Count => _items.Length; + public int Count => _items.Count; /// /// Gets an enumerator. /// - public IEnumerator GetEnumerator() - { - return ((IEnumerable) _items).GetEnumerator(); - } + public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); /// /// Gets an enumerator. /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index ab33a6ebef..81d80bb57c 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.Composing /// The type of the collection. /// The type of the items. public abstract class CollectionBuilderBase : ICollectionBuilder - where TBuilder: CollectionBuilderBase + where TBuilder : CollectionBuilderBase where TCollection : class, IBuilderCollection { private readonly List _types = new List(); @@ -73,7 +73,8 @@ namespace Umbraco.Cms.Core.Composing { lock (_locker) { - if (_registeredTypes != null) return; + if (_registeredTypes != null) + return; var types = GetRegisteringTypes(_types).ToArray(); @@ -110,7 +111,7 @@ namespace Umbraco.Cms.Core.Composing /// Creates a collection item. /// protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) - => (TItem) factory.GetRequiredService(itemType); + => (TItem)factory.GetRequiredService(itemType); /// /// Creates a collection. @@ -118,9 +119,10 @@ namespace Umbraco.Cms.Core.Composing /// A collection. /// Creates a new collection each time it is invoked. public virtual TCollection CreateCollection(IServiceProvider factory) - { - return factory.CreateInstance( CreateItems(factory)); - } + => factory.CreateInstance(CreateItemsFactory(factory)); + + // used to resolve a Func> parameter + private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); protected Type EnsureType(Type type, string action) { @@ -139,7 +141,7 @@ namespace Umbraco.Cms.Core.Composing public virtual bool Has() where T : TItem { - return _types.Contains(typeof (T)); + return _types.Contains(typeof(T)); } /// diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index 1cd505027b..c39dd503e0 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Composing private readonly IProfilingLogger _profilingLogger; private readonly ILogger _logger; - public ComponentCollection(IEnumerable items, IProfilingLogger profilingLogger, ILogger logger) + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) : base(items) { _profilingLogger = profilingLogger; diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 60aa666034..56d798ea0c 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; @@ -16,11 +17,17 @@ namespace Umbraco.Cms.Core.Composing { private readonly Assembly _entryPointAssembly; private readonly ILoggerFactory _loggerFactory; + private readonly IEnumerable _additionalTargetAssemblies; + private List _discovered; - public DefaultUmbracoAssemblyProvider(Assembly entryPointAssembly, ILoggerFactory loggerFactory) + public DefaultUmbracoAssemblyProvider( + Assembly entryPointAssembly, + ILoggerFactory loggerFactory, + IEnumerable additionalTargetAssemblies = null) { _entryPointAssembly = entryPointAssembly ?? throw new ArgumentNullException(nameof(entryPointAssembly)); _loggerFactory = loggerFactory; + _additionalTargetAssemblies = additionalTargetAssemblies; } // TODO: It would be worth investigating a netcore3 version of this which would use @@ -33,8 +40,21 @@ namespace Umbraco.Cms.Core.Composing { get { - var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, Constants.Composing.UmbracoCoreAssemblyNames, true, _loggerFactory); - return finder.Find(); + if (_discovered != null) + { + return _discovered; + } + + IEnumerable additionalTargetAssemblies = Constants.Composing.UmbracoCoreAssemblyNames; + if (_additionalTargetAssemblies != null) + { + additionalTargetAssemblies = additionalTargetAssemblies.Concat(_additionalTargetAssemblies); + } + + var finder = new FindAssembliesWithReferencesTo(new[] { _entryPointAssembly }, additionalTargetAssemblies.ToArray(), true, _loggerFactory); + _discovered = finder.Find().ToList(); + + return _discovered; } } } diff --git a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs index 5b554a5321..78cdb80f58 100644 --- a/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs +++ b/src/Umbraco.Core/Composing/FindAssembliesWithReferencesTo.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Core.Composing private readonly string[] _targetAssemblies; private readonly bool _includeTargets; private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; /// /// Constructor @@ -32,6 +33,7 @@ namespace Umbraco.Cms.Core.Composing _targetAssemblies = targetAssemblyNames; _includeTargets = includeTargets; _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); } public IEnumerable Find() @@ -50,9 +52,10 @@ namespace Umbraco.Cms.Core.Composing { referenceItems.Add(Assembly.Load(target)); } - catch (FileNotFoundException) + catch (FileNotFoundException ex) { // occurs if we cannot load this ... for example in a test project where we aren't currently referencing Umbraco.Web, etc... + _logger.LogDebug(ex, "Could not load assembly " + target); } } } diff --git a/src/Umbraco.Core/Composing/ITypeFinder.cs b/src/Umbraco.Core/Composing/ITypeFinder.cs index 2cf6ca23f7..2522c2f593 100644 --- a/src/Umbraco.Core/Composing/ITypeFinder.cs +++ b/src/Umbraco.Core/Composing/ITypeFinder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; @@ -51,14 +51,5 @@ namespace Umbraco.Cms.Core.Composing Type attributeType, IEnumerable assemblies, bool onlyConcreteClasses); - - /// - /// Gets a hash value of the current runtime - /// - /// - /// This is used to detect if the runtime itself has changed, like a DLL has changed or another dynamically compiled - /// part of the application has changed. This is used to detect if we need to re-type scan. - /// - string GetRuntimeHash(); } } diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index d41813f7d8..baae385af4 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -10,6 +10,9 @@ namespace Umbraco.Cms.Core.Composing /// The type of the builder. /// The type of the collection. /// The type of the items. + /// + /// This type of collection builder is typically used when type scanning is required (i.e. plugins). + /// public abstract class LazyCollectionBuilderBase : CollectionBuilderBase where TBuilder : LazyCollectionBuilderBase where TCollection : class, IBuilderCollection @@ -29,7 +32,7 @@ namespace Umbraco.Cms.Core.Composing { types.Clear(); _producers.Clear(); - _excluded.Clear(); + _excluded.Clear(); }); return This; } @@ -45,7 +48,8 @@ namespace Umbraco.Cms.Core.Composing Configure(types => { var type = typeof(T); - if (types.Contains(type) == false) types.Add(type); + if (types.Contains(type) == false) + types.Add(type); }); return This; } @@ -60,7 +64,8 @@ namespace Umbraco.Cms.Core.Composing Configure(types => { EnsureType(type, "register"); - if (types.Contains(type) == false) types.Add(type); + if (types.Contains(type) == false) + types.Add(type); }); return This; } @@ -90,7 +95,8 @@ namespace Umbraco.Cms.Core.Composing Configure(types => { var type = typeof(T); - if (_excluded.Contains(type) == false) _excluded.Add(type); + if (_excluded.Contains(type) == false) + _excluded.Add(type); }); return This; } @@ -105,7 +111,8 @@ namespace Umbraco.Cms.Core.Composing Configure(types => { EnsureType(type, "exclude"); - if (_excluded.Contains(type) == false) _excluded.Add(type); + if (_excluded.Contains(type) == false) + _excluded.Add(type); }); return This; } diff --git a/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs new file mode 100644 index 0000000000..7fee8eaf26 --- /dev/null +++ b/src/Umbraco.Core/Composing/LazyReadOnlyCollection.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Cms.Core.Composing +{ + public sealed class LazyReadOnlyCollection : IReadOnlyCollection + { + private readonly Lazy> _lazyCollection; + private int? _count; + + public LazyReadOnlyCollection(Lazy> lazyCollection) => _lazyCollection = lazyCollection; + + public LazyReadOnlyCollection(Func> lazyCollection) => _lazyCollection = new Lazy>(lazyCollection); + + public IEnumerable Value => EnsureCollection(); + + private IEnumerable EnsureCollection() + { + if (_lazyCollection == null) + { + _count = 0; + return Enumerable.Empty(); + } + + IEnumerable val = _lazyCollection.Value; + if (_count == null) + { + _count = val.Count(); + } + return val; + } + + public int Count + { + get + { + EnsureCollection(); + return _count.Value; + } + } + + public IEnumerator GetEnumerator() => Value.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Umbraco.Core/Composing/RuntimeHash.cs b/src/Umbraco.Core/Composing/RuntimeHash.cs index 4eb70cea1f..7ca35855de 100644 --- a/src/Umbraco.Core/Composing/RuntimeHash.cs +++ b/src/Umbraco.Core/Composing/RuntimeHash.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using Umbraco.Cms.Core.Logging; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Composing { @@ -12,6 +14,7 @@ namespace Umbraco.Cms.Core.Composing { private readonly IProfilingLogger _logger; private readonly RuntimeHashPaths _paths; + private string _calculated; public RuntimeHash(IProfilingLogger logger, RuntimeHashPaths paths) { @@ -22,13 +25,18 @@ namespace Umbraco.Cms.Core.Composing public string GetHashValue() { - var allPaths = _paths.GetFolders() - .Select(x => ((FileSystemInfo) x, false)) - .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo) x.Key, x.Value))); + if (_calculated != null) + { + return _calculated; + } - var hash = GetFileHash(allPaths); + IEnumerable<(FileSystemInfo, bool)> allPaths = _paths.GetFolders() + .Select(x => ((FileSystemInfo)x, false)) + .Concat(_paths.GetFiles().Select(x => ((FileSystemInfo)x.Key, x.Value))); - return hash; + _calculated = GetFileHash(allPaths); + + return _calculated; } /// @@ -48,7 +56,7 @@ namespace Umbraco.Cms.Core.Composing using var generator = new HashGenerator(); - foreach (var (fileOrFolder, scanFileContent) in filesAndFolders) + foreach ((FileSystemInfo fileOrFolder, bool scanFileContent) in filesAndFolders) { if (scanFileContent) { @@ -56,9 +64,16 @@ namespace Umbraco.Cms.Core.Composing // normalize the content for cr/lf and case-sensitivity if (uniqContent.Add(fileOrFolder.FullName)) { - if (File.Exists(fileOrFolder.FullName) == false) continue; - var content = RemoveCrLf(File.ReadAllText(fileOrFolder.FullName)); - generator.AddCaseInsensitiveString(content); + if (File.Exists(fileOrFolder.FullName) == false) + { + continue; + } + + using (FileStream fileStream = File.OpenRead(fileOrFolder.FullName)) + { + var hash = fileStream.GetStreamHash(); + generator.AddCaseInsensitiveString(hash); + } } } else @@ -74,18 +89,5 @@ namespace Umbraco.Cms.Core.Composing } } - // fast! (yes, according to benchmarks) - private static string RemoveCrLf(string s) - { - var buffer = new char[s.Length]; - var count = 0; - // ReSharper disable once ForCanBeConvertedToForeach - no! - for (var i = 0; i < s.Length; i++) - { - if (s[i] != '\r' && s[i] != '\n') - buffer[count++] = s[i]; - } - return new string(buffer, 0, count); - } } } diff --git a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs index 12a878ee94..eac2f83bcd 100644 --- a/src/Umbraco.Core/Composing/RuntimeHashPaths.cs +++ b/src/Umbraco.Core/Composing/RuntimeHashPaths.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using System.Reflection; namespace Umbraco.Cms.Core.Composing { @@ -17,6 +18,24 @@ namespace Umbraco.Cms.Core.Composing return this; } + /// + /// Creates a runtime hash based on the assembly provider + /// + /// + /// + public RuntimeHashPaths AddAssemblies(IAssemblyProvider assemblyProvider) + { + foreach (Assembly assembly in assemblyProvider.Assemblies) + { + // TODO: We need to test this on a published website + if (!assembly.IsDynamic && assembly.Location != null) + { + AddFile(new FileInfo(assembly.Location)); + } + } + return this; + } + public void AddFile(FileInfo fileInfo, bool scanFileContent = false) => _files.Add(fileInfo, scanFileContent); public IEnumerable GetFolders() => _paths; diff --git a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs index 0bebf8bf8b..40ce3d8a46 100644 --- a/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/TypeCollectionBuilderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; @@ -37,7 +37,11 @@ namespace Umbraco.Cms.Core.Composing public TBuilder Add(IEnumerable types) { - foreach (var type in types) Add(type); + foreach (var type in types) + { + Add(type); + } + return This; } @@ -54,13 +58,12 @@ namespace Umbraco.Cms.Core.Composing } public TCollection CreateCollection(IServiceProvider factory) - { - return factory.CreateInstance(_types); - } + => factory.CreateInstance(CreateItemsFactory()); public void RegisterWith(IServiceCollection services) - { - services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); - } + => services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, ServiceLifetime.Singleton)); + + // used to resolve a Func> parameter + private Func> CreateItemsFactory() => () => _types; } } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index 1979f33d10..4299328b5d 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -17,23 +17,21 @@ namespace Umbraco.Cms.Core.Composing { private readonly ILogger _logger; private readonly IAssemblyProvider _assemblyProvider; - private readonly IRuntimeHash _runtimeHash; private volatile HashSet _localFilteredAssemblyCache; private readonly object _localFilteredAssemblyCacheLocker = new object(); private readonly List _notifiedLoadExceptionAssemblies = new List(); - private static readonly ConcurrentDictionary TypeNamesCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_typeNamesCache = new ConcurrentDictionary(); private readonly ITypeFinderConfig _typeFinderConfig; // used for benchmark tests internal bool QueryWithReferencingAssemblies { get; set; } = true; - public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, IRuntimeHash runtimeHash, ITypeFinderConfig typeFinderConfig = null) + public TypeFinder(ILogger logger, IAssemblyProvider assemblyProvider, ITypeFinderConfig typeFinderConfig = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _assemblyProvider = assemblyProvider; - _runtimeHash = runtimeHash; _typeFinderConfig = typeFinderConfig; - } + } private string[] _assembliesAcceptingLoadExceptions = null; @@ -64,9 +62,12 @@ namespace Umbraco.Cms.Core.Composing var name = a.GetName().Name; // simple name of the assembly return AssembliesAcceptingLoadExceptions.Any(pattern => { - if (pattern.Length > name.Length) return false; // pattern longer than name - if (pattern.Length == name.Length) return pattern.InvariantEquals(name); // same length, must be identical - if (pattern[pattern.Length] != '.') return false; // pattern is shorter than name, must end with dot + if (pattern.Length > name.Length) + return false; // pattern longer than name + if (pattern.Length == name.Length) + return pattern.InvariantEquals(name); // same length, must be identical + if (pattern[pattern.Length] != '.') + return false; // pattern is shorter than name, must end with dot return name.StartsWith(pattern); // and name must start with pattern }); } @@ -112,6 +113,8 @@ namespace Umbraco.Cms.Core.Composing && exclusionFilter.Any(f => x.FullName.StartsWith(f)) == false); } + // TODO: Kill this + /// /// this is our assembly filter to filter out known types that def don't contain types we'd like to find or plugins /// @@ -232,9 +235,6 @@ namespace Umbraco.Cms.Core.Composing return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); } - /// - public string GetRuntimeHash() => _runtimeHash.GetHashValue(); - /// /// Returns a Type for the string type name /// @@ -255,7 +255,7 @@ namespace Umbraco.Cms.Core.Composing } // It didn't parse, so try loading from each already loaded assembly and cache it - return TypeNamesCache.GetOrAdd(name, s => + return s_typeNamesCache.GetOrAdd(name, s => AppDomain.CurrentDomain.GetAssemblies() .Select(x => x.GetType(s)) .FirstOrDefault(x => x != null)); @@ -455,7 +455,8 @@ namespace Umbraco.Cms.Core.Composing var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); // rethrow with new message, unless accepted - if (AcceptsLoadExceptions(a) == false) throw ex; + if (AcceptsLoadExceptions(a) == false) + throw ex; // log a warning, and return what we can lock (_notifiedLoadExceptionAssemblies) diff --git a/src/Umbraco.Core/Composing/TypeFinderConfig.cs b/src/Umbraco.Core/Composing/TypeFinderConfig.cs index 7940773231..8e63958a06 100644 --- a/src/Umbraco.Core/Composing/TypeFinderConfig.cs +++ b/src/Umbraco.Core/Composing/TypeFinderConfig.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.UmbracoSettings; @@ -15,17 +16,16 @@ namespace Umbraco.Cms.Core.Composing private readonly TypeFinderSettings _settings; private IEnumerable _assembliesAcceptingLoadExceptions; - public TypeFinderConfig(IOptions settings) - { - _settings = settings.Value; - } + public TypeFinderConfig(IOptions settings) => _settings = settings.Value; public IEnumerable AssembliesAcceptingLoadExceptions { get { if (_assembliesAcceptingLoadExceptions != null) + { return _assembliesAcceptingLoadExceptions; + } var s = _settings.AssembliesAcceptingLoadExceptions; return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 39b70b831b..6af8cb5ced 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Threading; @@ -26,9 +25,8 @@ namespace Umbraco.Cms.Core.Composing /// on a hash of the DLLs in the ~/bin folder to check for cache expiration. /// public sealed class TypeLoader - { - internal const string CacheKey = "umbraco-types.list"; - + { + private readonly IRuntimeHash _runtimeHash; private readonly IAppPolicyCache _runtimeCache; private readonly ILogger _logger; private readonly IProfilingLogger _profilingLogger; @@ -45,7 +43,9 @@ namespace Umbraco.Cms.Core.Composing private bool _reportedChange; private readonly DirectoryInfo _localTempPath; private readonly Lazy _fileBasePath; - private readonly Dictionary<(string, string), IEnumerable> EmptyCache = new Dictionary<(string, string), IEnumerable>(); + private readonly Dictionary<(string, string), IEnumerable> _emptyCache = new Dictionary<(string, string), IEnumerable>(); + private string _typesListFilePath; + private string _typesHashFilePath; /// /// Initializes a new instance of the class. @@ -55,8 +55,8 @@ namespace Umbraco.Cms.Core.Composing /// Files storage location. /// A profiling logger. /// - public TypeLoader(ITypeFinder typeFinder, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfilingLogger profilingLogger, IEnumerable assembliesToScan = null) - : this(typeFinder, runtimeCache, localTempPath, logger, profilingLogger, true, assembliesToScan) + public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfiler profiler, IEnumerable assembliesToScan = null) + : this(typeFinder, runtimeHash, runtimeCache, localTempPath, logger, profiler, true, assembliesToScan) { } /// @@ -68,13 +68,22 @@ namespace Umbraco.Cms.Core.Composing /// A profiling logger. /// Whether to detect changes using hashes. /// - public TypeLoader(ITypeFinder typeFinder, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfilingLogger profilingLogger, bool detectChanges, IEnumerable assembliesToScan = null) + public TypeLoader(ITypeFinder typeFinder, IRuntimeHash runtimeHash, IAppPolicyCache runtimeCache, DirectoryInfo localTempPath, ILogger logger, IProfiler profiler, bool detectChanges, IEnumerable assembliesToScan = null) { + if (profiler is null) + { + throw new ArgumentNullException(nameof(profiler)); + } + + var runtimeHashValue = runtimeHash.GetHashValue(); + CacheKey = runtimeHashValue + "umbraco-types.list"; + TypeFinder = typeFinder ?? throw new ArgumentNullException(nameof(typeFinder)); + _runtimeHash = runtimeHash; _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); _localTempPath = localTempPath; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + _profilingLogger = new ProfilingLogger(logger, profiler); _assemblies = assembliesToScan; _fileBasePath = new Lazy(GetFileBasePath); @@ -87,6 +96,8 @@ namespace Umbraco.Cms.Core.Composing //if they have changed, we need to write the new file if (RequiresRescanning) { + _logger.LogDebug("Plugin types are being re-scanned. Cached hash value: {CachedHash}, Current hash value: {CurrentHash}", CachedAssembliesHash, CurrentAssembliesHash); + // if the hash has changed, clear out the persisted list no matter what, this will force // rescanning of all types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 @@ -115,6 +126,8 @@ namespace Umbraco.Cms.Core.Composing } } + internal string CacheKey { get; } + /// /// Returns the underlying /// @@ -198,9 +211,11 @@ namespace Umbraco.Cms.Core.Composing get { if (_currentAssembliesHash != null) + { return _currentAssembliesHash; + } - _currentAssembliesHash = TypeFinder.GetRuntimeHash(); + _currentAssembliesHash = _runtimeHash.GetHashValue(); return _currentAssembliesHash; } @@ -264,7 +279,7 @@ namespace Umbraco.Cms.Core.Composing } } - return EmptyCache; + return _emptyCache; } // internal for tests @@ -273,7 +288,7 @@ namespace Umbraco.Cms.Core.Composing var typesListFilePath = GetTypesListFilePath(); if (typesListFilePath == null || File.Exists(typesListFilePath) == false) { - return EmptyCache; + return _emptyCache; } var cache = new Dictionary<(string, string), IEnumerable>(); @@ -328,9 +343,9 @@ namespace Umbraco.Cms.Core.Composing } // internal for tests - public string GetTypesListFilePath() => _fileBasePath.Value == null ? null : _fileBasePath.Value + ".list"; + public string GetTypesListFilePath() => _typesListFilePath ??= _fileBasePath.Value == null ? null : _fileBasePath.Value + ".list"; - private string GetTypesHashFilePath() => _fileBasePath.Value == null ? null : _fileBasePath.Value + ".hash"; + private string GetTypesHashFilePath() => _typesHashFilePath ??= _fileBasePath.Value == null ? null : _fileBasePath.Value + ".hash"; /// /// Used to produce the Lazy value of _fileBasePath diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index dab615da51..e88d1f4d01 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -17,48 +17,57 @@ namespace Umbraco.Cms.Core.Configuration public string ProviderName { get; } public string Name { get; } - private string ParseProvider(string connectionString) + private static bool IsSqlCe(DbConnectionStringBuilder builder) => (builder.TryGetValue("Data Source", out var ds) + || builder.TryGetValue("DataSource", out ds)) && + ds is string dataSource && + dataSource.EndsWith(".sdf"); + + private static bool IsSqlServer(DbConnectionStringBuilder builder) => + !string.IsNullOrEmpty(GetServer(builder)) && + ((builder.TryGetValue("Database", out var db) && db is string database && + !string.IsNullOrEmpty(database)) || + (builder.TryGetValue("AttachDbFileName", out var a) && a is string attachDbFileName && + !string.IsNullOrEmpty(attachDbFileName)) || + (builder.TryGetValue("Initial Catalog", out var i) && i is string initialCatalog && + !string.IsNullOrEmpty(initialCatalog))); + + private static string GetServer(DbConnectionStringBuilder builder) + { + if(builder.TryGetValue("Server", out var s) && s is string server) + { + return server; + } + + if ((builder.TryGetValue("Data Source", out var ds) + || builder.TryGetValue("DataSource", out ds)) && ds is string dataSource) + { + return dataSource; + } + + return ""; + } + + private static string ParseProvider(string connectionString) { if (string.IsNullOrEmpty(connectionString)) { return null; } - var builder = new DbConnectionStringBuilder + var builder = new DbConnectionStringBuilder {ConnectionString = connectionString}; + if (IsSqlCe(builder)) { - ConnectionString = connectionString - }; - - if ( - (builder.TryGetValue("Data Source", out var ds) - || builder.TryGetValue("DataSource", out ds)) && ds is string dataSource) - { - if (dataSource.EndsWith(".sdf")) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlCe; - } + return Constants.DbProviderNames.SqlCe; } - if (builder.TryGetValue("Server", out var s) && s is string server && !string.IsNullOrEmpty(server)) + if (IsSqlServer(builder)) { - if (builder.TryGetValue("Database", out var db) && db is string database && !string.IsNullOrEmpty(database)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } - - if (builder.TryGetValue("AttachDbFileName", out var a) && a is string attachDbFileName && !string.IsNullOrEmpty(attachDbFileName)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } - - if (builder.TryGetValue("Initial Catalog", out var i) && i is string initialCatalog && !string.IsNullOrEmpty(initialCatalog)) - { - return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer; - } + return Constants.DbProviderNames.SqlServer; } - throw new ArgumentException("Cannot determine provider name from connection string", nameof(connectionString)); + throw new ArgumentException("Cannot determine provider name from connection string", + nameof(connectionString)); } } } diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs index 6840d974aa..7bef36dba4 100644 --- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs @@ -1,10 +1,10 @@  -namespace Umbraco.Core.Dashboards +namespace Umbraco.Cms.Core.Configuration { public class ContentDashboardSettings { private const string DefaultContentDashboardPath = "cms"; - + /// /// Gets a value indicating whether the content dashboard should be available to all users. /// diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index ab6a7e9396..f1a4f0643c 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Core.Configuration.Grid } // add manifest editors, skip duplicates - foreach (var gridEditor in _manifestParser.Manifest.GridEditors) + foreach (var gridEditor in _manifestParser.CombinedManifest.GridEditors) { if (editors.Contains(gridEditor) == false) editors.Add(gridEditor); } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index bd92b4ba3c..7767d1dbdc 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -30,5 +30,7 @@ namespace Umbraco.Cms.Core.Configuration.Models /// [DefaultValue(StaticSqlPageSize)] public int SqlPageSize { get; set; } = StaticSqlPageSize; + + public bool UnPublishedContentCompression { get; set; } = false; } } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 48e08d596a..7d4dd45fb8 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -15,6 +15,7 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticHideDisabledUsersInBackOffice = false; internal const bool StaticAllowPasswordReset = true; internal const string StaticAuthCookieName = "UMB_UCONTEXT"; + internal const string StaticAllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\"; /// /// Gets or sets a value indicating whether to keep the user logged in. @@ -50,6 +51,12 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool UsernameIsEmail { get; set; } = true; + /// + /// Gets or sets the set of allowed characters for a username + /// + [DefaultValue(StaticAllowedUserNameCharacters)] + public string AllowedUserNameCharacters { get; set; } = StaticAllowedUserNameCharacters; + /// /// Gets or sets a value for the user password settings. /// diff --git a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs index d942f47b4f..e41921de8c 100644 --- a/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/TypeFinderSettings.cs @@ -1,6 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections; +using System.Collections.Generic; + namespace Umbraco.Cms.Core.Configuration.Models { /// @@ -13,5 +16,11 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value for the assemblies that accept load exceptions during type finder operations. /// public string AssembliesAcceptingLoadExceptions { get; set; } + + /// + /// By default the entry assemblies for scanning plugin types is the Umbraco DLLs. If you require + /// scanning for plugins based on different root referenced assemblies you can add the assembly name to this list. + /// + public IEnumerable AdditionalEntryAssemblies { get; set; } } } diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs index d8b3e04772..0c6e6f0f68 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; @@ -14,7 +15,7 @@ namespace Umbraco.Cms.Core.ContentApps private readonly ILogger _logger; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public ContentAppFactoryCollection(IEnumerable items, ILogger logger, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public ContentAppFactoryCollection(Func> items, ILogger logger, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) : base(items) { _logger = logger; diff --git a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs index 3c35054e9b..a80c79a3ef 100644 --- a/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/ContentApps/ContentAppFactoryCollectionBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -21,7 +21,9 @@ namespace Umbraco.Cms.Core.ContentApps // get the logger factory just-in-time - see note below for manifest parser var loggerFactory = factory.GetRequiredService(); var backOfficeSecurityAccessor = factory.GetRequiredService(); - return new ContentAppFactoryCollection(CreateItems(factory), loggerFactory.CreateLogger(), backOfficeSecurityAccessor); + return new ContentAppFactoryCollection( + () => CreateItems(factory), + loggerFactory.CreateLogger(), backOfficeSecurityAccessor); } protected override IEnumerable CreateItems(IServiceProvider factory) @@ -31,7 +33,7 @@ namespace Umbraco.Cms.Core.ContentApps // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); var ioHelper = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.Manifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.ContentApps.Select(x => new ManifestContentAppFactory(x, ioHelper))); } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollection.cs b/src/Umbraco.Core/Dashboards/DashboardCollection.cs index 9fa13f1d3d..e5c8378139 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollection.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Dashboards { public class DashboardCollection : BuilderCollectionBase { - public DashboardCollection(IEnumerable items) - : base(items) - { } + public DashboardCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs index 55e840ad8e..348e81e383 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Dashboards // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); - var dashboardSections = Merge(base.CreateItems(factory), manifestParser.Manifest.Dashboards); + var dashboardSections = Merge(base.CreateItems(factory), manifestParser.CombinedManifest.Dashboards); return dashboardSections; } diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs index 0b65c3443c..738cfed26e 100644 --- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs @@ -1,7 +1,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Logging; namespace Umbraco.Cms.Core.DependencyInjection { @@ -10,7 +13,24 @@ namespace Umbraco.Cms.Core.DependencyInjection IServiceCollection Services { get; } IConfiguration Config { get; } TypeLoader TypeLoader { get; } + + /// + /// A Logger factory created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// ILoggerFactory BuilderLoggerFactory { get; } + + /// + /// A hosting environment created specifically for the . This is NOT the same + /// instance that will be resolved from DI. Use only if required during configuration. + /// + /// + /// This may be null. + /// + IHostingEnvironment BuilderHostingEnvironment { get; } + + IProfiler Profiler { get; } + AppCaches AppCaches { get; } TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder, new(); void Build(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 5dd2499624..648af8f082 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -52,23 +52,41 @@ namespace Umbraco.Cms.Core.DependencyInjection public TypeLoader TypeLoader { get; } + /// public ILoggerFactory BuilderLoggerFactory { get; } + /// + public IHostingEnvironment BuilderHostingEnvironment { get; } + + public IProfiler Profiler { get; } + + public AppCaches AppCaches { get; } + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class primarily for testing. /// public UmbracoBuilder(IServiceCollection services, IConfiguration config, TypeLoader typeLoader) - : this(services, config, typeLoader, NullLoggerFactory.Instance) + : this(services, config, typeLoader, NullLoggerFactory.Instance, new NoopProfiler(), AppCaches.Disabled, null) { } /// /// Initializes a new instance of the class. /// - public UmbracoBuilder(IServiceCollection services, IConfiguration config, TypeLoader typeLoader, ILoggerFactory loggerFactory) + public UmbracoBuilder( + IServiceCollection services, + IConfiguration config, + TypeLoader typeLoader, + ILoggerFactory loggerFactory, + IProfiler profiler, + AppCaches appCaches, + IHostingEnvironment hostingEnvironment) { Services = services; Config = config; BuilderLoggerFactory = loggerFactory; + BuilderHostingEnvironment = hostingEnvironment; + Profiler = profiler; + AppCaches = appCaches; TypeLoader = typeLoader; AddCoreServices(); @@ -106,6 +124,9 @@ namespace Umbraco.Cms.Core.DependencyInjection private void AddCoreServices() { + Services.AddSingleton(AppCaches); + Services.AddSingleton(Profiler); + // Register as singleton to allow injection everywhere. Services.AddSingleton(p => p.GetService); Services.AddSingleton(); diff --git a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs index 3b07be5553..91bc3e191b 100644 --- a/src/Umbraco.Core/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Core/Editors/EditorValidatorCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Editors { public class EditorValidatorCollection : BuilderCollectionBase { - public EditorValidatorCollection(IEnumerable items) - : base(items) - { } + public EditorValidatorCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs index 7df9167ce6..58fdafc341 100644 --- a/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/IScopedNotificationPublisher.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; @@ -8,6 +9,12 @@ namespace Umbraco.Cms.Core.Events { public interface IScopedNotificationPublisher { + /// + /// Suppresses all notifications from being added/created until the result object is disposed. + /// + /// + IDisposable Suppress(); + /// /// Publishes a cancelable notification to the notification subscribers /// diff --git a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs index c9b0080218..7261b514bf 100644 --- a/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs +++ b/src/Umbraco.Core/Events/ScopedNotificationPublisher.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using Umbraco.Cms.Core.Notifications; @@ -12,6 +13,8 @@ namespace Umbraco.Cms.Core.Events { private readonly IEventAggregator _eventAggregator; private readonly List _notificationOnScopeCompleted; + private readonly object _locker = new object(); + private bool _isSuppressed = false; public ScopedNotificationPublisher(IEventAggregator eventAggregator) { @@ -26,6 +29,11 @@ namespace Umbraco.Cms.Core.Events throw new ArgumentNullException(nameof(notification)); } + if (_isSuppressed) + { + return false; + } + _eventAggregator.Publish(notification); return notification.Cancel; } @@ -37,6 +45,11 @@ namespace Umbraco.Cms.Core.Events throw new ArgumentNullException(nameof(notification)); } + if (_isSuppressed) + { + return false; + } + await _eventAggregator.PublishAsync(notification); return notification.Cancel; } @@ -48,6 +61,11 @@ namespace Umbraco.Cms.Core.Events throw new ArgumentNullException(nameof(notification)); } + if (_isSuppressed) + { + return; + } + _notificationOnScopeCompleted.Add(notification); } @@ -57,7 +75,7 @@ namespace Umbraco.Cms.Core.Events { if (completed) { - foreach (var notification in _notificationOnScopeCompleted) + foreach (INotification notification in _notificationOnScopeCompleted) { _eventAggregator.Publish(notification); } @@ -68,5 +86,45 @@ namespace Umbraco.Cms.Core.Events _notificationOnScopeCompleted.Clear(); } } + + public IDisposable Suppress() + { + lock(_locker) + { + if (_isSuppressed) + { + throw new InvalidOperationException("Notifications are already suppressed"); + } + return new Suppressor(this); + } + } + + private class Suppressor : IDisposable + { + private bool _disposedValue; + private readonly ScopedNotificationPublisher _scopedNotificationPublisher; + + public Suppressor(ScopedNotificationPublisher scopedNotificationPublisher) + { + _scopedNotificationPublisher = scopedNotificationPublisher; + _scopedNotificationPublisher._isSuppressed = true; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + lock (_scopedNotificationPublisher._locker) + { + _scopedNotificationPublisher._isSuppressed = false; + } + } + _disposedValue = true; + } + } + public void Dispose() => Dispose(disposing: true); + } } } diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 5aaad5e303..8385de5e70 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -9,6 +9,8 @@ using System.Xml.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -17,6 +19,41 @@ namespace Umbraco.Extensions { public static class ContentExtensions { + /// + /// Returns the path to a media item stored in a property if the property editor is + /// + /// + /// + /// + /// + /// + /// + /// True if the file path can be resolved and the property is + public static bool TryGetMediaPath( + this IContentBase content, + string propertyTypeAlias, + MediaUrlGeneratorCollection mediaUrlGenerators, + out string mediaFilePath, + string culture = null, + string segment = null) + { + if (!content.Properties.TryGetValue(propertyTypeAlias, out IProperty property)) + { + mediaFilePath = null; + return false; + } + + if (!mediaUrlGenerators.TryGetMediaPath( + property.PropertyType.PropertyEditorAlias, + property.GetValue(culture, segment), + out mediaFilePath)) + { + return false; + } + + return true; + } + public static bool IsAnyUserPropertyDirty(this IContentBase entity) { return entity.Properties.Any(x => x.IsDirty()); @@ -204,35 +241,56 @@ namespace Umbraco.Extensions /// /// Sets the posted file value of a property. /// - public static void SetValue(this IContentBase content, MediaFileManager mediaFileManager, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IJsonSerializer serializer, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) + public static void SetValue( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string culture = null, + string segment = null) { - if (filename == null || filestream == null) return; + if (filename == null || filestream == null) + return; filename = shortStringHelper.CleanStringForSafeFileName(filename); - if (string.IsNullOrWhiteSpace(filename)) return; + if (string.IsNullOrWhiteSpace(filename)) + return; filename = filename.ToLower(); - SetUploadFile(content, mediaFileManager, contentTypeBaseServiceProvider, serializer, propertyTypeAlias, filename, filestream, culture, segment); + SetUploadFile(content, mediaFileManager, mediaUrlGenerators, contentTypeBaseServiceProvider, propertyTypeAlias, filename, filestream, culture, segment); } - private static void SetUploadFile(this IContentBase content, MediaFileManager mediaFileManager, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IJsonSerializer serializer, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) + private static void SetUploadFile( + this IContentBase content, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + string propertyTypeAlias, + string filename, + Stream filestream, + string culture = null, + string segment = null) { var property = GetProperty(content, contentTypeBaseServiceProvider, propertyTypeAlias); // Fixes https://github.com/umbraco/Umbraco-CMS/issues/3937 - Assigning a new file to an // existing IMedia with extension SetValue causes exception 'Illegal characters in path' string oldpath = null; - if (property.GetValue(culture, segment) is string svalue) + + if (content.TryGetMediaPath(property.Alias, mediaUrlGenerators, out string mediaFilePath, culture, segment)) { - if (svalue.DetectIsJson()) - { - // the property value is a JSON serialized image crop data set - grab the "src" property as the file source - svalue = serializer.DeserializeSubset(svalue, "src"); - } - oldpath = mediaFileManager.FileSystem.GetRelativePath(svalue); + oldpath = mediaFileManager.FileSystem.GetRelativePath(mediaFilePath); } var filepath = mediaFileManager.StoreFile(content, property.PropertyType, filename, filestream, oldpath); + + // NOTE: Here we are just setting the value to a string which means that any file based editor + // will need to handle the raw string value and save it to it's correct (i.e. JSON) + // format. I'm unsure how this works today with image cropper but it does (maybe events?) property.SetValue(mediaFileManager.FileSystem.GetUrl(filepath), culture, segment); } @@ -240,7 +298,8 @@ namespace Umbraco.Extensions private static IProperty GetProperty(IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias) { var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (property != null) return property; + if (property != null) + return property; var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); var propertyType = contentType.CompositionPropertyTypes @@ -274,7 +333,8 @@ namespace Umbraco.Extensions var contentType = contentTypeBaseServiceProvider.GetContentTypeOf(content); var propertyType = contentType .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); - if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); + if (propertyType == null) + throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); return mediaFileManager.StoreFile(content, propertyType, filename, filestream, filepath); } diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 255cf381af..944e0bdf49 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Security.Cryptography; using System.Text; @@ -73,14 +73,12 @@ namespace Umbraco.Cms.Core AddDateTime(f.LastWriteTimeUtc); //check if it is a file or folder - var fileInfo = f as FileInfo; - if (fileInfo != null) + if (f is FileInfo fileInfo) { AddLong(fileInfo.Length); } - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) + if (f is DirectoryInfo dirInfo) { foreach (var d in dirInfo.GetFiles()) { diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs index 2987ff1112..bcbee9036b 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.HealthChecks { public class HealthCheckCollection : BuilderCollectionBase { - public HealthCheckCollection(IEnumerable items) - : base(items) - { } + public HealthCheckCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs index 7fa8486df6..af964857d8 100644 --- a/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; @@ -6,8 +7,8 @@ namespace Umbraco.Cms.Core.HealthChecks { public class HealthCheckNotificationMethodCollection : BuilderCollectionBase { - public HealthCheckNotificationMethodCollection(IEnumerable items) - : base(items) - { } + public HealthCheckNotificationMethodCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 6f1f47007f..23be195e4b 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -1,5 +1,7 @@ -using System; +using System; using System.IO; +using System.Security.Cryptography; +using System.Text; using System.Threading; using Umbraco.Cms.Core.IO; @@ -7,6 +9,25 @@ namespace Umbraco.Extensions { public static class FileSystemExtensions { + public static string GetStreamHash(this Stream fileStream) + { + if (fileStream.CanSeek) + { + fileStream.Seek(0, SeekOrigin.Begin); + } + + using HashAlgorithm alg = SHA1.Create(); + + // create a string output for the hash + var stringBuilder = new StringBuilder(); + var hashedByteArray = alg.ComputeHash(fileStream); + foreach (var b in hashedByteArray) + { + stringBuilder.Append(b.ToString("x2")); + } + return stringBuilder.ToString(); + } + /// /// Attempts to open the file at filePath up to maxRetries times, /// with a thread sleep time of sleepPerRetryInMilliseconds between retries. diff --git a/src/Umbraco.Core/IO/IMediaPathScheme.cs b/src/Umbraco.Core/IO/IMediaPathScheme.cs index 2b2696ec5d..5daf5490a8 100644 --- a/src/Umbraco.Core/IO/IMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/IMediaPathScheme.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Umbraco.Cms.Core.IO { @@ -14,9 +14,9 @@ namespace Umbraco.Cms.Core.IO /// The (content, media) item unique identifier. /// The property type unique identifier. /// The file name. - /// A previous filename. + /// /// The filesystem-relative complete file path. - string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename, string previous = null); + string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename); /// /// Gets the directory that can be deleted when the file is deleted. diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index de89d7ca87..96680d3f84 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -3,8 +3,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -15,6 +20,9 @@ namespace Umbraco.Cms.Core.IO private readonly IMediaPathScheme _mediaPathScheme; private readonly ILogger _logger; private readonly IShortStringHelper _shortStringHelper; + private readonly IServiceProvider _serviceProvider; + private MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly ContentSettings _contentSettings; /// /// Gets the media filesystem. @@ -25,11 +33,15 @@ namespace Umbraco.Cms.Core.IO IFileSystem fileSystem, IMediaPathScheme mediaPathScheme, ILogger logger, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider, + IOptions contentSettings) { _mediaPathScheme = mediaPathScheme; _logger = logger; _shortStringHelper = shortStringHelper; + _serviceProvider = serviceProvider; + _contentSettings = contentSettings.Value; FileSystem = fileSystem; } @@ -96,33 +108,46 @@ namespace Umbraco.Cms.Core.IO return _mediaPathScheme.GetFilePath(this, cuid, puid, filename); } - /// - /// Gets the file path of a media file. - /// - /// The file name. - /// A previous file path. - /// The unique identifier of the content/media owning the file. - /// The unique identifier of the property type owning the file. - /// The filesystem-relative path to the media file. - /// In the old, legacy, number-based scheme, we try to re-use the media folder - /// specified by . Else, we CREATE a new one. Each time we are invoked. - public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) - { - filename = Path.GetFileName(filename); - if (filename == null) - { - throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); - } - - filename = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant()); - - return _mediaPathScheme.GetFilePath(this, cuid, puid, filename, prevpath); - } - #endregion #region Associated Media Files + /// + /// Returns a stream (file) for a content item or null if there is no file. + /// + /// + /// The file path if a file was found + /// + /// + /// + public Stream GetFile( + IContentBase content, + out string mediaFilePath, + string propertyTypeAlias = Constants.Conventions.Media.File, + string culture = null, + string segment = null) + { + // TODO: If collections were lazy we could just inject them + if (_mediaUrlGenerators == null) + { + _mediaUrlGenerators = _serviceProvider.GetRequiredService(); + } + + if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators, out mediaFilePath, culture, segment)) + { + return null; + } + + Stream stream = FileSystem.OpenFile(mediaFilePath); + if (stream != null) + { + return stream; + } + + mediaFilePath = null; + return null; + } + /// /// Stores a media file associated to a property of a content item. /// @@ -171,8 +196,7 @@ namespace Umbraco.Cms.Core.IO } // get the filepath, store the data - // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme - var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); FileSystem.AddFile(filepath, filestream); return filepath; } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index f3c15a13d6..b4127e080d 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Umbraco.Cms.Core.IO.MediaPathSchemes @@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes public class CombinedGuidsMediaPathScheme : IMediaPathScheme { /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs deleted file mode 100644 index d1cea0c46b..0000000000 --- a/src/Umbraco.Core/IO/MediaPathSchemes/OriginalMediaPathScheme.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Threading; - -namespace Umbraco.Cms.Core.IO.MediaPathSchemes -{ - /// - /// Implements the original media path scheme. - /// - /// - /// Path is "{number}/{filename}" or "{number}-{filename}" where number is an incremented counter. - /// Use '/' or '-' depending on UploadAllowDirectories setting. - /// - // scheme: path is "/" where number is an incremented counter - public class OriginalMediaPathScheme : IMediaPathScheme - { - private readonly object _folderCounterLock = new object(); - private long _folderCounter; - private bool _folderCounterInitialized; - - /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) - { - string directory; - if (previous != null) - { - // old scheme, with a previous path - // prevpath should be "/" OR "-" - // and we want to reuse the "" part, so try to find it - - const string sep = "/"; - var pos = previous.IndexOf(sep, StringComparison.Ordinal); - var s = pos > 0 ? previous.Substring(0, pos) : null; - - directory = pos > 0 && int.TryParse(s, out _) ? s : GetNextDirectory(fileManager.FileSystem); - } - else - { - directory = GetNextDirectory(fileManager.FileSystem); - } - - if (directory == null) - throw new InvalidOperationException("Cannot use a null directory."); - - return Path.Combine(directory, filename).Replace('\\', '/'); - } - - /// - public string GetDeleteDirectory(MediaFileManager fileSystem, string filepath) - { - return Path.GetDirectoryName(filepath); - } - - private string GetNextDirectory(IFileSystem fileSystem) - { - EnsureFolderCounterIsInitialized(fileSystem); - return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); - } - - private void EnsureFolderCounterIsInitialized(IFileSystem fileSystem) - { - lock (_folderCounterLock) - { - if (_folderCounterInitialized) return; - - _folderCounter = 1000; // seed - var directories = fileSystem.GetDirectories(""); - foreach (var directory in directories) - { - if (long.TryParse(directory, out var folderNumber) && folderNumber > _folderCounter) - _folderCounter = folderNumber; - } - - // note: not multi-domains ie LB safe as another domain could create directories - // while we read and parse them - don't fix, move to new scheme eventually - - _folderCounterInitialized = true; - } - } - } -} diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs index 203c8aaed8..ebce899697 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/TwoGuidsMediaPathScheme.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Umbraco.Cms.Core.IO.MediaPathSchemes @@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes public class TwoGuidsMediaPathScheme : IMediaPathScheme { /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/'); } diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs index 55a9cd4574..c247c37032 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/UniqueMediaPathScheme.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Umbraco.Cms.Core.IO.MediaPathSchemes @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Core.IO.MediaPathSchemes private const int DirectoryLength = 8; /// - public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename, string previous = null) + public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) { var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid); var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index e517f0be63..8c35e80988 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -146,17 +146,21 @@ namespace Umbraco.Cms.Core.IO var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); if (exists && overrideExisting == false) + { throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + } var directory = Path.GetDirectoryName(fullPath); if (directory == null) throw new InvalidOperationException("Could not get directory."); Directory.CreateDirectory(directory); // ensure it exists - if (stream.CanSeek) // TODO: what if we cannot? + if (stream.CanSeek) + { stream.Seek(0, 0); + } - using (var destination = (Stream) File.Create(fullPath)) - stream.CopyTo(destination); + using var destination = (Stream)File.Create(fullPath); + stream.CopyTo(destination); } /// diff --git a/src/Umbraco.Core/Logging/ProfilingLogger.cs b/src/Umbraco.Core/Logging/ProfilingLogger.cs index 9c66c95d24..a4843ccc29 100644 --- a/src/Umbraco.Core/Logging/ProfilingLogger.cs +++ b/src/Umbraco.Core/Logging/ProfilingLogger.cs @@ -27,6 +27,15 @@ namespace Umbraco.Cms.Core.Logging Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); } + /// + /// Initializes a new instance of the class. + /// + public ProfilingLogger(ILogger logger, IProfiler profiler) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + } + public DisposableTimer TraceDuration(string startMessage, object[] startMessageArgs = null) => TraceDuration(startMessage, "Completed.", startMessageArgs: startMessageArgs); diff --git a/src/Umbraco.Core/Manifest/BundleOptions.cs b/src/Umbraco.Core/Manifest/BundleOptions.cs new file mode 100644 index 0000000000..810efb6c45 --- /dev/null +++ b/src/Umbraco.Core/Manifest/BundleOptions.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Cms.Core.Manifest +{ + public enum BundleOptions + { + /// + /// The default bundling behavior for assets in the package folder. + /// + /// + /// The assets will be bundled with the typical packages bundle. + /// + Default = 0, + + /// + /// The assets in the package will not be processed at all and will all be requested as individual assets. + /// + /// + /// This will essentially be a bundle that has composite processing turned off for both debug and production. + /// + None = 1, + + /// + /// The packages assets will be processed as it's own separate bundle. (in debug, files will not be processed) + /// + Independent = 2 + } +} diff --git a/src/Umbraco.Core/Manifest/CompositePackageManifest.cs b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs new file mode 100644 index 0000000000..939d635fc3 --- /dev/null +++ b/src/Umbraco.Core/Manifest/CompositePackageManifest.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Core.Manifest +{ + + /// + /// A package manifest made up of all combined manifests + /// + public class CompositePackageManifest + { + public CompositePackageManifest( + IReadOnlyList propertyEditors, + IReadOnlyList parameterEditors, + IReadOnlyList gridEditors, + IReadOnlyList contentApps, + IReadOnlyList dashboards, + IReadOnlyList sections, + IReadOnlyDictionary> scripts, + IReadOnlyDictionary> stylesheets) + { + PropertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + ParameterEditors = parameterEditors ?? throw new ArgumentNullException(nameof(parameterEditors)); + GridEditors = gridEditors ?? throw new ArgumentNullException(nameof(gridEditors)); + ContentApps = contentApps ?? throw new ArgumentNullException(nameof(contentApps)); + Dashboards = dashboards ?? throw new ArgumentNullException(nameof(dashboards)); + Sections = sections ?? throw new ArgumentNullException(nameof(sections)); + Scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); + Stylesheets = stylesheets ?? throw new ArgumentNullException(nameof(stylesheets)); + } + + /// + /// Gets or sets the property editors listed in the manifest. + /// + public IReadOnlyList PropertyEditors { get; } + + /// + /// Gets or sets the parameter editors listed in the manifest. + /// + public IReadOnlyList ParameterEditors { get; } + + /// + /// Gets or sets the grid editors listed in the manifest. + /// + public IReadOnlyList GridEditors { get; } + + /// + /// Gets or sets the content apps listed in the manifest. + /// + public IReadOnlyList ContentApps { get; } + + /// + /// Gets or sets the dashboards listed in the manifest. + /// + public IReadOnlyList Dashboards { get; } + + /// + /// Gets or sets the sections listed in the manifest. + /// + public IReadOnlyCollection Sections { get; } + + public IReadOnlyDictionary> Scripts { get; } + + public IReadOnlyDictionary> Stylesheets { get; } + } +} diff --git a/src/Umbraco.Core/Manifest/IManifestParser.cs b/src/Umbraco.Core/Manifest/IManifestParser.cs index dc3a19714e..09d3ccbe1c 100644 --- a/src/Umbraco.Core/Manifest/IManifestParser.cs +++ b/src/Umbraco.Core/Manifest/IManifestParser.cs @@ -4,13 +4,13 @@ namespace Umbraco.Cms.Core.Manifest { public interface IManifestParser { - string Path { get; set; } + string AppPluginsPath { get; set; } /// /// Gets all manifests, merged into a single manifest object. /// /// - PackageManifest Manifest { get; } + CompositePackageManifest CombinedManifest { get; } /// /// Parses a manifest. diff --git a/src/Umbraco.Core/Manifest/ManifestAssets.cs b/src/Umbraco.Core/Manifest/ManifestAssets.cs new file mode 100644 index 0000000000..ad5dfaa0f0 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestAssets.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Manifest +{ + public class ManifestAssets + { + public ManifestAssets(string packageName, IReadOnlyList assets) + { + PackageName = packageName ?? throw new ArgumentNullException(nameof(packageName)); + Assets = assets ?? throw new ArgumentNullException(nameof(assets)); + } + + public string PackageName { get; } + public IReadOnlyList Assets { get; } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs index 20a3468c36..9c692f69b3 100644 --- a/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs +++ b/src/Umbraco.Core/Manifest/ManifestFilterCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Manifest @@ -8,12 +9,9 @@ namespace Umbraco.Cms.Core.Manifest /// public class ManifestFilterCollection : BuilderCollectionBase { - /// - /// Initializes a new instance of the class. - /// - public ManifestFilterCollection(IEnumerable items) - : base(items) - { } + public ManifestFilterCollection(Func> items) : base(items) + { + } /// /// Filters package manifests. diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 56c69ebb15..753ec0613a 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -6,6 +6,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest { + /// /// Represents the content of a package manifest. /// @@ -47,6 +48,8 @@ namespace Umbraco.Cms.Core.Manifest /// [IgnoreDataMember] public string Source { get; set; } + [DataMember(Name = "bundleOptions")] + public BundleOptions BundleOptions { get; set; } /// /// Gets or sets the scripts listed in the manifest. diff --git a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs index d9cc08ad43..27d4ad73d0 100644 --- a/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs +++ b/src/Umbraco.Core/Mapping/MapDefinitionCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Mapping { public class MapDefinitionCollection : BuilderCollectionBase { - public MapDefinitionCollection(IEnumerable items) - : base(items) - { } + public MapDefinitionCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs index 490ff64357..615d16f51c 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/EmbedProvidersCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Media.EmbedProviders { public class EmbedProvidersCollection : BuilderCollectionBase { - public EmbedProvidersCollection(IEnumerable items) - : base(items) - { } + public EmbedProvidersCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 32c8ddd98d..10a372ce9f 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Configuration.Models; @@ -66,20 +66,24 @@ namespace Umbraco.Cms.Core.Media } else { - // if anything goes wrong, just reset the properties - try + // it might not exist if the media item has been created programatically but doesn't have a file persisted yet. + if (_mediaFileManager.FileSystem.FileExists(filepath)) { - using (var filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) + // if anything goes wrong, just reset the properties + try { - var extension = (Path.GetExtension(filepath) ?? "").TrimStart(Constants.CharArrays.Period); - var size = _imageUrlGenerator.IsSupportedImageFormat(extension) ? (ImageSize?)_imageDimensionExtractor.GetDimensions(filestream) : null; - SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); + using (var filestream = _mediaFileManager.FileSystem.OpenFile(filepath)) + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart(Constants.CharArrays.Period); + var size = _imageUrlGenerator.IsSupportedImageFormat(extension) ? (ImageSize?)_imageDimensionExtractor.GetDimensions(filestream) : null; + SetProperties(content, autoFillConfig, size, filestream.Length, extension, culture, segment); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); + ResetProperties(content, autoFillConfig, culture, segment); } - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not populate upload auto-fill properties for file '{File}'.", filepath); - ResetProperties(content, autoFillConfig, culture, segment); } } } diff --git a/src/Umbraco.Core/Models/ContentBaseExtensions.cs b/src/Umbraco.Core/Models/ContentBaseExtensions.cs index 45fe9e41d9..9aae08c74b 100644 --- a/src/Umbraco.Core/Models/ContentBaseExtensions.cs +++ b/src/Umbraco.Core/Models/ContentBaseExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models; @@ -25,8 +25,19 @@ namespace Umbraco.Extensions if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, culture)).FirstOrDefault(u => u != null); - url ??= new DefaultUrlSegmentProvider(shortStringHelper).GetUrlSegment(content, culture); // be safe + if (url == null) + { + if (s_defaultUrlSegmentProvider == null) + { + s_defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper); + } + + url = s_defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe + } + return url; } + + private static DefaultUrlSegmentProvider s_defaultUrlSegmentProvider; } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs new file mode 100644 index 0000000000..a9651e4f7a --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByKeys.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing +{ + /// + /// A model for retrieving multiple content types based on their keys. + /// + [DataContract(Name = "contentTypes", Namespace = "")] + public class ContentTypesByKeys + { + /// + /// ID of the parent of the content type. + /// + [DataMember(Name = "parentId")] + [Required] + public int ParentId { get; set; } + + /// + /// The id of every content type to get. + /// + [DataMember(Name = "contentTypeKeys")] + [Required] + public Guid[] ContentTypeKeys { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs index 7e8cf39ffd..c77500c531 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UmbracoEntityTypes.cs @@ -1,15 +1,10 @@ -namespace Umbraco.Cms.Core.Models.ContentEditing +namespace Umbraco.Cms.Core.Models.ContentEditing { /// /// Represents the type's of Umbraco entities that can be resolved from the EntityController /// public enum UmbracoEntityTypes { - /// - /// Domain - /// - Domain, - /// /// Language /// @@ -65,6 +60,16 @@ /// Stylesheet, + /// + /// Script + /// + Script, + + /// + /// Partial View + /// + PartialView, + /// /// Member /// diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index a50890bee0..ece903d8e6 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -40,21 +40,48 @@ namespace Umbraco.Extensions /// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible that /// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact same time. /// - public static void AdjustDates(this IContent content, DateTime date) + public static void AdjustDates(this IContent content, DateTime date, bool publishing) { + foreach(var culture in content.EditedCultures.ToList()) + { + if (!content.CultureInfos.TryGetValue(culture, out ContentCultureInfos editedInfos)) + { + continue; + } + + // if it's not dirty, it means it hasn't changed so there's nothing to adjust + if (!editedInfos.IsDirty()) + { + continue; + } + + content.CultureInfos.AddOrUpdate(culture, editedInfos.Name, date); + } + + if (!publishing) + { + return; + } + foreach (var culture in content.PublishedCultures.ToList()) { - if (!content.PublishCultureInfos.TryGetValue(culture, out var publishInfos)) + if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos)) + { continue; + } // if it's not dirty, it means it hasn't changed so there's nothing to adjust if (!publishInfos.IsDirty()) + { continue; + } content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date); - if (content.CultureInfos.TryGetValue(culture, out var infos)) + if (content.CultureInfos.TryGetValue(culture, out ContentCultureInfos infos)) + { SetCultureInfo(content, culture, infos.Name, date); + } } } diff --git a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs index 5d649ecd8a..0119f1bd24 100644 --- a/src/Umbraco.Core/Models/IMediaUrlGenerator.cs +++ b/src/Umbraco.Core/Models/IMediaUrlGenerator.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models { /// /// Used to generate paths to media items for a specified property editor alias @@ -8,11 +8,11 @@ /// /// Tries to get a media path for a given property editor alias /// - /// The property editor alias + /// The property editor alias /// The value of the property /// /// True if a media path was returned /// - bool TryGetMediaPath(string alias, object value, out string mediaPath); + bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath); } } diff --git a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs index d2fd28f510..b185bb586e 100644 --- a/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/CodeFileMapDefinition.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Core.Models.Mapping @@ -8,9 +8,14 @@ namespace Umbraco.Cms.Core.Models.Mapping public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); - mapper.Define((source, context) => new CodeFileDisplay(), Map); mapper.Define((source, context) => new CodeFileDisplay(), Map); + + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); + + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new CodeFileDisplay(), Map); + mapper.Define(Map); mapper.Define(Map); @@ -27,6 +32,28 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Path = source.Path; } + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IScript source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } + + // Umbraco.Code.MapAll -Trashed -Udi -Icon + private static void Map(IPartialView source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + } + // Umbraco.Code.MapAll -FileType -Notifications -Path -Snippet private static void Map(IPartialView source, CodeFileDisplay target, MapperContext context) { diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index a7571d6317..616dbd1d12 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -12,16 +12,11 @@ namespace Umbraco.Extensions /// public static string GetUrl(this IMedia media, string propertyAlias, MediaUrlGeneratorCollection mediaUrlGenerators) { - if (!media.Properties.TryGetValue(propertyAlias, out var property)) - return string.Empty; - - // TODO: would need to be adjusted to variations, when media become variants - if (mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaUrl)) + if (media.TryGetMediaPath(propertyAlias, mediaUrlGenerators, out var mediaPath)) { - return mediaUrl; + return mediaPath; } - // Without knowing what it is, just adding a string here might not be very nice return string.Empty; } @@ -29,11 +24,9 @@ namespace Umbraco.Extensions /// Gets the URLs of a media item. /// public static string[] GetUrls(this IMedia media, ContentSettings contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators) - { - return contentSettings.Imaging.AutoFillImageProperties + => contentSettings.Imaging.AutoFillImageProperties .Select(field => media.GetUrl(field.Alias, mediaUrlGenerators)) .Where(link => string.IsNullOrWhiteSpace(link) == false) .ToArray(); - } } } diff --git a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs index 5f6c3991a0..6e963bd2da 100644 --- a/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs +++ b/src/Umbraco.Core/Models/Packaging/CompiledPackage.cs @@ -14,8 +14,11 @@ namespace Umbraco.Cms.Core.Models.Packaging public string Name { get; set; } public InstallWarnings Warnings { get; set; } = new InstallWarnings(); public IEnumerable Macros { get; set; } // TODO: make strongly typed + public IEnumerable MacroPartialViews { get; set; } // TODO: make strongly typed public IEnumerable Templates { get; set; } // TODO: make strongly typed public IEnumerable Stylesheets { get; set; } // TODO: make strongly typed + public IEnumerable Scripts { get; set; } // TODO: make strongly typed + public IEnumerable PartialViews { get; set; } // TODO: make strongly typed public IEnumerable DataTypes { get; set; } // TODO: make strongly typed public IEnumerable Languages { get; set; } // TODO: make strongly typed public IEnumerable DictionaryItems { get; set; } // TODO: make strongly typed diff --git a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs index 6957da7f70..b9cda7722c 100644 --- a/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentRefreshNotification.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Notifications { + [Obsolete("This is only used for the internal cache and will change, use saved notifications instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class ContentRefreshNotification : EntityRefreshNotification diff --git a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs index 2a4929930e..2b6b3b9e1c 100644 --- a/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs +++ b/src/Umbraco.Core/Packaging/CompiledPackageXmlParser.cs @@ -38,8 +38,11 @@ namespace Umbraco.Cms.Core.Packaging PackageFile = null, Name = package.Element("name")?.Value, Macros = xml.Root.Element("Macros")?.Elements("macro") ?? Enumerable.Empty(), + MacroPartialViews = xml.Root.Element("MacroPartialViews")?.Elements("View") ?? Enumerable.Empty(), + PartialViews = xml.Root.Element("PartialViews")?.Elements("View") ?? Enumerable.Empty(), Templates = xml.Root.Element("Templates")?.Elements("Template") ?? Enumerable.Empty(), - Stylesheets = xml.Root.Element("Stylesheets")?.Elements("styleSheet") ?? Enumerable.Empty(), + Stylesheets = xml.Root.Element("Stylesheets")?.Elements("Stylesheet") ?? Enumerable.Empty(), + Scripts = xml.Root.Element("Scripts")?.Elements("Script") ?? Enumerable.Empty(), DataTypes = xml.Root.Element("DataTypes")?.Elements("DataType") ?? Enumerable.Empty(), Languages = xml.Root.Element("Languages")?.Elements("Language") ?? Enumerable.Empty(), DictionaryItems = xml.Root.Element("DictionaryItems")?.Elements("DictionaryItem") ?? Enumerable.Empty(), diff --git a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 27e96d8c82..0d71b9f70e 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.Packaging if (xElement == null) throw new FormatException("Missing \"Name\" element"); - return _fileService.GetStylesheetByName(xElement.Value) as IFile; + return _fileService.GetStylesheet(xElement.Value) as IFile; }) .Where(v => v != null); } diff --git a/src/Umbraco.Core/Packaging/InstallationSummary.cs b/src/Umbraco.Core/Packaging/InstallationSummary.cs index 3c519a4a4c..005d5859fb 100644 --- a/src/Umbraco.Core/Packaging/InstallationSummary.cs +++ b/src/Umbraco.Core/Packaging/InstallationSummary.cs @@ -27,6 +27,8 @@ namespace Umbraco.Cms.Core.Packaging public IEnumerable DocumentTypesInstalled { get; set; } = Enumerable.Empty(); public IEnumerable MediaTypesInstalled { get; set; } = Enumerable.Empty(); public IEnumerable StylesheetsInstalled { get; set; } = Enumerable.Empty(); + public IEnumerable ScriptsInstalled { get; set; } = Enumerable.Empty(); + public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); public string PackageName { get; } diff --git a/src/Umbraco.Core/Packaging/PackageDefinition.cs b/src/Umbraco.Core/Packaging/PackageDefinition.cs index 3c808d4de0..345d669b15 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinition.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinition.cs @@ -52,6 +52,9 @@ namespace Umbraco.Cms.Core.Packaging [DataMember(Name = "templates")] public IList Templates { get; set; } = new List(); + [DataMember(Name = "partialViews")] + public IList PartialViews { get; set; } = new List(); + [DataMember(Name = "documentTypes")] public IList DocumentTypes { get; set; } = new List(); @@ -61,6 +64,9 @@ namespace Umbraco.Cms.Core.Packaging [DataMember(Name = "stylesheets")] public IList Stylesheets { get; set; } = new List(); + [DataMember(Name = "scripts")] + public IList Scripts { get; set; } = new List(); + [DataMember(Name = "dataTypes")] public IList DataTypes { get; set; } = new List(); diff --git a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs index 69c109aee4..7cca061401 100644 --- a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs +++ b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs @@ -12,6 +12,10 @@ namespace Umbraco.Cms.Core.Packaging /// public class PackageDefinitionXmlParser { + private static readonly IList s_emptyStringList = new List(); + private static readonly IList s_emptyGuidUdiList = new List(); + + public PackageDefinition ToPackageDefinition(XElement xml) { if (xml == null) @@ -27,16 +31,18 @@ namespace Umbraco.Cms.Core.Packaging PackageId = xml.AttributeValue("packageGuid"), ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty, ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false, - MediaUdis = xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? new List(), + MediaUdis = xml.Element("media")?.Elements("nodeUdi").Select(x => (GuidUdi)UdiParser.Parse(x.Value)).ToList() ?? s_emptyGuidUdiList, MediaLoadChildNodes = xml.Element("media")?.AttributeValue("loadChildNodes") ?? false, - Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - MediaTypes = xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), - DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(), + Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + Scripts = xml.Element("scripts")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + PartialViews = xml.Element("partialViews")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + MediaTypes = xml.Element("mediaTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, + DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? s_emptyStringList, }; return retVal; @@ -57,6 +63,8 @@ namespace Umbraco.Cms.Core.Packaging new XElement("templates", string.Join(",", def.Templates ?? Array.Empty())), new XElement("stylesheets", string.Join(",", def.Stylesheets ?? Array.Empty())), + new XElement("scripts", string.Join(",", def.Scripts ?? Array.Empty())), + new XElement("partialViews", string.Join(",", def.PartialViews ?? Array.Empty())), new XElement("documentTypes", string.Join(",", def.DocumentTypes ?? Array.Empty())), new XElement("mediaTypes", string.Join(",", def.MediaTypes ?? Array.Empty())), new XElement("macros", string.Join(",", def.Macros ?? Array.Empty())), diff --git a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs index 82cdb31427..2f407df88f 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs @@ -1,28 +1,63 @@ -using System; +using System; using System.IO; +using System.IO.Compression; using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Xml; using System.Xml.Linq; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Packaging { public static class PackageMigrationResource { - public static XDocument GetEmbeddedPackageDataManifest(Type planType) + private static Stream GetEmbeddedPackageStream(Type planType) { - // lookup the embedded resource by convention + // lookup the embedded resource by convention Assembly currentAssembly = planType.Assembly; - var fileName = $"{planType.Namespace}.package.xml"; + var fileName = $"{planType.Namespace}.package.zip"; Stream stream = currentAssembly.GetManifestResourceStream(fileName); if (stream == null) { throw new FileNotFoundException("Cannot find the embedded file.", fileName); } - XDocument xml; - using (stream) + return stream; + } + + public static string GetEmbeddedPackageDataManifestHash(Type planType) + { + // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run + // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. + // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are + // several very large package.zips. + + using Stream stream = GetEmbeddedPackageStream(planType); + return stream.GetStreamHash(); + } + + public static ZipArchive GetEmbeddedPackageDataManifest(Type planType, out XDocument packageXml) + => GetPackageDataManifest(GetEmbeddedPackageStream(planType), out packageXml); + + public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) + { + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); + ZipArchiveEntry packageXmlEntry = zip.GetEntry("package.xml"); + if (packageXmlEntry == null) { - xml = XDocument.Load(stream); + throw new InvalidOperationException("Zip package does not contain the required package.xml file"); } - return xml; + + using (Stream packageXmlStream = packageXmlEntry.Open()) + using (var xmlReader = XmlReader.Create(packageXmlStream, new XmlReaderSettings + { + IgnoreWhitespace = true + })) + { + packageXml = XDocument.Load(xmlReader); + } + + return zip; } } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index b86f5d8695..ffc67663cc 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Text; using System.Xml.Linq; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -35,6 +37,8 @@ namespace Umbraco.Cms.Core.Packaging private readonly PackageDefinitionXmlParser _parser; private readonly IMediaService _mediaService; private readonly IMediaTypeService _mediaTypeService; + private readonly MediaFileManager _mediaFileManager; + private readonly FileSystems _fileSystems; /// /// Constructor @@ -66,6 +70,8 @@ namespace Umbraco.Cms.Core.Packaging IOptions globalSettings, IMediaService mediaService, IMediaTypeService mediaTypeService, + MediaFileManager mediaFileManager, + FileSystems fileSystems, string packageRepositoryFileName, string tempFolderPath = null, string packagesFolderPath = null, @@ -90,6 +96,8 @@ namespace Umbraco.Cms.Core.Packaging _parser = new PackageDefinitionXmlParser(); _mediaService = mediaService; _mediaTypeService = mediaTypeService; + _mediaFileManager = mediaFileManager; + _fileSystems = fileSystems; } private string CreatedPackagesFile => _packagesFolderPath.EnsureEndsWith('/') + _packageRepositoryFileName; @@ -183,7 +191,7 @@ namespace Umbraco.Cms.Core.Packaging try { //Init package file - var compiledPackageXml = CreateCompiledPackageXml(out var root); + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); //Info section root.Add(GetPackageInfoXml(definition)); @@ -193,23 +201,38 @@ namespace Umbraco.Cms.Core.Packaging PackageMediaTypes(definition, root); PackageTemplates(definition, root); PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem); PackageMacros(definition, root); PackageDictionaryItems(definition, root); PackageLanguages(definition, root); PackageDataTypes(definition, root); - PackageMedia(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); - var packageXmlFileName = temporaryPath + "/package.xml"; + var tempPackagePath = temporaryPath + "/package.zip"; - if (File.Exists(packageXmlFileName)) + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - File.Delete(packageXmlFileName); + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) + { + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } + } } - compiledPackageXml.Save(packageXmlFileName); - - // check if there's a packages directory below media - var directoryName = _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); @@ -218,18 +241,19 @@ namespace Umbraco.Cms.Core.Packaging Directory.CreateDirectory(directoryName); } - var packPath = Path.Combine(directoryName, "package.xml"); + var finalPackagePath = Path.Combine(directoryName, "package.zip"); - if (File.Exists(packPath)) + if (File.Exists(finalPackagePath)) { - File.Delete(packPath); + File.Delete(finalPackagePath); } - File.Move(packageXmlFileName, packPath); - definition.PackagePath = packPath; + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; SavePackage(definition); - return packPath; + return finalPackagePath; } finally { @@ -305,7 +329,7 @@ namespace Umbraco.Cms.Core.Packaging var processed = new Dictionary(); while (processed.Count < itemCount) { - foreach(Guid key in items.Keys.ToList()) + foreach (Guid key in items.Keys.ToList()) { (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; @@ -352,34 +376,85 @@ namespace Umbraco.Cms.Core.Packaging private void PackageMacros(PackageDefinition definition, XContainer root) { + var packagedMacros = new List(); var macros = new XElement("Macros"); foreach (var macroId in definition.Macros) { - if (!int.TryParse(macroId, out var outInt)) + if (!int.TryParse(macroId, out int outInt)) + { continue; + } - var macroXml = GetMacroXml(outInt, out var macro); + XElement macroXml = GetMacroXml(outInt, out IMacro macro); if (macroXml == null) + { continue; + } + macros.Add(macroXml); + packagedMacros.Add(macro); } + root.Add(macros); + + // get the partial views for macros and package those + IEnumerable views = packagedMacros.Select(x => x.MacroSource).Where(x => x.EndsWith(".cshtml")); + PackageMacroPartialViews(views, root); } private void PackageStylesheets(PackageDefinition definition, XContainer root) { var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheetName in definition.Stylesheets) + foreach (var stylesheet in definition.Stylesheets) { - if (stylesheetName.IsNullOrWhiteSpace()) + if (stylesheet.IsNullOrWhiteSpace()) + { continue; - var xml = GetStylesheetXml(stylesheetName, true); + } + + XElement xml = GetStylesheetXml(stylesheet, true); if (xml != null) + { stylesheetsXml.Add(xml); + } } root.Add(stylesheetsXml); } + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) + { + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem.FileExists(file)) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem.OpenFile(file)) + using (var reader = new StreamReader(stream)) + { + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); + } + } + root.Add(scriptsXml); + } + private void PackageTemplates(PackageDefinition definition, XContainer root) { var templatesXml = new XElement("Templates"); @@ -395,6 +470,33 @@ namespace Umbraco.Cms.Core.Packaging root.Add(templatesXml); } + private void PackageMacroPartialViews(IEnumerable viewPaths, XContainer root) + { + var viewsXml = new XElement("MacroPartialViews"); + foreach (var viewPath in viewPaths) + { + // TODO: See TODO note in MacrosController about the inconsistencies of usages of partial views + // and how paths are saved. We have no choice currently but to assume that all views are 100% always + // on the content path. + + var physicalPath = _hostingEnvironment.MapPathContentRoot(viewPath); + if (!File.Exists(physicalPath)) + { + throw new InvalidOperationException("Could not find partial view at path " + viewPath); + } + + var fileContents = File.ReadAllText(physicalPath, Encoding.UTF8); + + viewsXml.Add( + new XElement( + "View", + new XAttribute("path", viewPath), + new XCData(fileContents))); + } + + root.Add(viewsXml); + } + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) { var contentTypes = new HashSet(); @@ -518,14 +620,43 @@ namespace Umbraco.Cms.Core.Packaging } - private void PackageMedia(PackageDefinition definition, XElement root) + private Dictionary PackageMedia(PackageDefinition definition, XElement root) { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); + + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } + } + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - root.Add( - new XElement( - "MediaItems", - medias.Select(x => new XElement("MediaSet", _serializer.Serialize(x, definition.MediaLoadChildNodes))))); + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; } // TODO: Delete this @@ -542,34 +673,23 @@ namespace Umbraco.Cms.Core.Packaging /// /// Converts a umbraco stylesheet to a package xml node /// - /// The name of the stylesheet. + /// The path of the stylesheet. /// if set to true [include properties]. /// - private XElement GetStylesheetXml(string name, bool includeProperties) + private XElement GetStylesheetXml(string path, bool includeProperties) { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - var sts = _fileService.GetStylesheetByName(name); - if (sts == null) - return null; - var stylesheetXml = new XElement("Stylesheet"); - stylesheetXml.Add(new XElement("Name", sts.Alias)); - stylesheetXml.Add(new XElement("FileName", sts.Name)); - stylesheetXml.Add(new XElement("Content", new XCData(sts.Content))); - - if (!includeProperties) - return stylesheetXml; - - var properties = new XElement("Properties"); - foreach (var ssP in sts.Properties) + if (string.IsNullOrWhiteSpace(path)) { - var property = new XElement("Property"); - property.Add(new XElement("Name", ssP.Name)); - property.Add(new XElement("Alias", ssP.Alias)); - property.Add(new XElement("Value", ssP.Value)); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); } - stylesheetXml.Add(properties); - return stylesheetXml; + + IStylesheet stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); } private void AddDocumentType(IContentType dt, HashSet dtl) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs index d4ddc21827..0c4ca93fc1 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.PropertyEditors { public class DataEditorCollection : BuilderCollectionBase { - public DataEditorCollection(IEnumerable items) - : base(items) - { } + public DataEditorCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 9a9efa5e2c..a83f925dd3 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -7,9 +7,9 @@ namespace Umbraco.Cms.Core.PropertyEditors { public class DataValueReferenceFactoryCollection : BuilderCollectionBase { - public DataValueReferenceFactoryCollection(IEnumerable items) - : base(items) - { } + public DataValueReferenceFactoryCollection(System.Func> items) : base(items) + { + } // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index f0973f3157..61f31a85c9 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -9,7 +9,12 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// public interface IPropertyCacheCompression - { - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias); + {/// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index 86bda9e799..a63029fc3d 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -4,6 +4,13 @@ namespace Umbraco.Cms.Core.PropertyEditors { public interface IPropertyCacheCompressionOptions { - bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor); + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// The datatype of the property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs index 45a2dc51bb..9e26362bc2 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Composing; @@ -8,9 +8,9 @@ namespace Umbraco.Cms.Core.PropertyEditors { public class ManifestValueValidatorCollection : BuilderCollectionBase { - public ManifestValueValidatorCollection(IEnumerable items) - : base(items) - { } + public ManifestValueValidatorCollection(Func> items) : base(items) + { + } public IManifestValueValidator Create(string name) { diff --git a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs index 2247d3e62f..66a967c828 100644 --- a/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/ManifestValueValidatorCollectionBuilder.cs @@ -1,8 +1,8 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.PropertyEditors { - public class ManifestValueValidatorCollectionBuilder : LazyCollectionBuilderBase + public class ManifestValueValidatorCollectionBuilder : SetCollectionBuilderBase { protected override ManifestValueValidatorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs index c3c168ed1f..f3adb6a05f 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollection.cs @@ -6,15 +6,23 @@ namespace Umbraco.Cms.Core.PropertyEditors { public class MediaUrlGeneratorCollection : BuilderCollectionBase { - public MediaUrlGeneratorCollection(IEnumerable items) : base(items) + public MediaUrlGeneratorCollection(System.Func> items) : base(items) { } - public bool TryGetMediaPath(string alias, object value, out string mediaPath) + public bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath) { - foreach(var generator in this) + // We can't get a media path from a null value + // The value will be null when uploading a brand new image, since we try to get the "old path" which doesn't exist yet. + if (value is null) { - if (generator.TryGetMediaPath(alias, value, out var mp)) + mediaPath = null; + return false; + } + + foreach(IMediaUrlGenerator generator in this) + { + if (generator.TryGetMediaPath(propertyEditorAlias, value, out var mp)) { mediaPath = mp; return true; diff --git a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs index 3ad03cb13c..57ab93832b 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaUrlGeneratorCollectionBuilder.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.PropertyEditors { - public class MediaUrlGeneratorCollectionBuilder : LazyCollectionBuilderBase + public class MediaUrlGeneratorCollectionBuilder : SetCollectionBuilderBase { protected override MediaUrlGeneratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index f2020ecbca..7e91d8e3ee 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -7,6 +7,6 @@ namespace Umbraco.Cms.Core.PropertyEditors /// public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor) => false; + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false; } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs index 5ad20665f4..39647bb753 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorCollection.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Manifest; @@ -7,9 +7,9 @@ namespace Umbraco.Cms.Core.PropertyEditors public class ParameterEditorCollection : BuilderCollectionBase { public ParameterEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(dataEditors + : base(() => dataEditors .Where(x => (x.Type & EditorType.MacroParameter) > 0) - .Union(manifestParser.Manifest.PropertyEditors)) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { } // note: virtual so it can be mocked diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index 5216e3158f..3d6c0adff6 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors { /// @@ -16,13 +16,13 @@ namespace Umbraco.Core.PropertyEditors private readonly IPropertyCacheCompressionOptions _compressionOptions; private readonly IReadOnlyDictionary _contentTypes; private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache; + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> _isCompressedCache; public PropertyCacheCompression( IPropertyCacheCompressionOptions compressionOptions, IReadOnlyDictionary contentTypes, PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache) + ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache) { _compressionOptions = compressionOptions; _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); @@ -30,9 +30,9 @@ namespace Umbraco.Core.PropertyEditors _isCompressedCache = compressedStoragePropertyEditorCache; } - public bool IsCompressed(IReadOnlyContentBase content, string alias) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) { - var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x => + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) return false; @@ -44,7 +44,7 @@ namespace Umbraco.Core.PropertyEditors if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return false; - return _compressionOptions.IsCompressed(content, propertyType, propertyEditor); + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published); }); return compressedStorage; diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index e5b6e96c7b..1ddf150f93 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -7,13 +7,13 @@ namespace Umbraco.Cms.Core.PropertyEditors public class PropertyEditorCollection : BuilderCollectionBase { public PropertyEditorCollection(DataEditorCollection dataEditors, IManifestParser manifestParser) - : base(dataEditors + : base(() => dataEditors .Where(x => (x.Type & EditorType.PropertyValue) > 0) - .Union(manifestParser.Manifest.PropertyEditors)) + .Union(manifestParser.CombinedManifest.PropertyEditors)) { } public PropertyEditorCollection(DataEditorCollection dataEditors) - : base(dataEditors + : base(() => dataEditors .Where(x => (x.Type & EditorType.PropertyValue) > 0)) { } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs index 136967e7ab..a60ba4a6e5 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Composing; @@ -8,9 +8,9 @@ namespace Umbraco.Cms.Core.PropertyEditors { public class PropertyValueConverterCollection : BuilderCollectionBase { - public PropertyValueConverterCollection(IEnumerable items) - : base(items) - { } + public PropertyValueConverterCollection(Func> items) : base(items) + { + } private readonly object _locker = new object(); private Dictionary _defaultConverters; diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..dc7f080acf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + /// + /// Compress large, non published text properties + /// + public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + { + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) + { + if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) + { + //Only compress non published content that supports publishing and the property is text + return true; + } + return false; + } + } +} diff --git a/src/Umbraco.Core/Routing/ContentFinderCollection.cs b/src/Umbraco.Core/Routing/ContentFinderCollection.cs index d63c269f50..8965d9d447 100644 --- a/src/Umbraco.Core/Routing/ContentFinderCollection.cs +++ b/src/Umbraco.Core/Routing/ContentFinderCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Routing { public class ContentFinderCollection : BuilderCollectionBase { - public ContentFinderCollection(IEnumerable items) - : base(items) - { } + public ContentFinderCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs index 0ac0ae265e..264be41d60 100644 --- a/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/MediaUrlProviderCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Routing { public class MediaUrlProviderCollection : BuilderCollectionBase { - public MediaUrlProviderCollection(IEnumerable items) - : base(items) - { } + public MediaUrlProviderCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Routing/UrlProviderCollection.cs b/src/Umbraco.Core/Routing/UrlProviderCollection.cs index 4a383f82bf..c17417c83c 100644 --- a/src/Umbraco.Core/Routing/UrlProviderCollection.cs +++ b/src/Umbraco.Core/Routing/UrlProviderCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Routing { public class UrlProviderCollection : BuilderCollectionBase { - public UrlProviderCollection(IEnumerable items) - : base(items) - { } + public UrlProviderCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Sections/SectionCollection.cs b/src/Umbraco.Core/Sections/SectionCollection.cs index a93b36a161..5ff0157d14 100644 --- a/src/Umbraco.Core/Sections/SectionCollection.cs +++ b/src/Umbraco.Core/Sections/SectionCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Sections { public class SectionCollection : BuilderCollectionBase { - public SectionCollection(IEnumerable items) - : base(items) - { } + public SectionCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs index 0c5b2d7ba9..219d634261 100644 --- a/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs +++ b/src/Umbraco.Core/Sections/SectionCollectionBuilder.cs @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Sections // its dependencies too, and that can create cycles or other oddities var manifestParser = factory.GetRequiredService(); - return base.CreateItems(factory).Concat(manifestParser.Manifest.Sections); + return base.CreateItems(factory).Concat(manifestParser.CombinedManifest.Sections); } } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index cef8a8c6d6..e58664fef8 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -236,13 +236,13 @@ namespace Umbraco.Cms.Core.Services /// /// Saves a document. /// - OperationResult Save(IContent content, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + OperationResult Save(IContent content, int userId = Constants.Security.SuperUserId); /// /// Saves documents. /// // TODO: why only 1 result not 1 per content?! - OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId); /// /// Deletes a document. @@ -325,12 +325,12 @@ namespace Umbraco.Cms.Core.Services /// /// Sorts documents. /// - OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId); /// /// Sorts documents. /// - OperationResult Sort(IEnumerable ids, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + OperationResult Sort(IEnumerable ids, int userId = Constants.Security.SuperUserId); #endregion @@ -349,8 +349,7 @@ namespace Umbraco.Cms.Core.Services /// The document to publish. /// The culture to publish. /// The identifier of the user performing the action. - /// A value indicating whether to raise events. - PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId); /// /// Saves and publishes a document. @@ -363,8 +362,7 @@ namespace Umbraco.Cms.Core.Services /// The document to publish. /// The cultures to publish. /// The identifier of the user performing the action. - /// A value indicating whether to raise events. - PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId); /// /// Saves and publishes a document branch. diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs index 9ab7fc7acc..a62d039abb 100644 --- a/src/Umbraco.Core/Services/IContentServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentServiceBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Services where TItem: class, IContentBase { TItem GetById(Guid key); - Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + Attempt Save(IEnumerable contents, int userId = Constants.Security.SuperUserId); } /// diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index f8e91d07d8..c7b13c04e1 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; @@ -68,15 +68,7 @@ namespace Umbraco.Cms.Core.Services /// to save /// Id of the user issuing the save void Save(IEnumerable dataTypeDefinitions, int userId = Constants.Security.SuperUserId); - - /// - /// Saves a collection of - /// - /// to save - /// Id of the user issuing the save - /// Boolean indicating whether or not to raise events - void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents); - + /// /// Deletes an /// diff --git a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs index 20de556395..c3e8a29e8e 100644 --- a/src/Umbraco.Core/Services/IEntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/IEntityXmlSerializer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml.Linq; using Umbraco.Cms.Core.Models; @@ -22,7 +23,8 @@ namespace Umbraco.Cms.Core.Services /// XElement Serialize( IMedia media, - bool withDescendants = false); + bool withDescendants = false, + Action onMediaItemSerialized = null); /// /// Exports an IMember item as an XElement. @@ -54,7 +56,7 @@ namespace Umbraco.Cms.Core.Services /// containing the xml representation of the IDictionaryItem object XElement Serialize(IDictionaryItem dictionaryItem, bool includeChildren); - XElement Serialize(Stylesheet stylesheet); + XElement Serialize(IStylesheet stylesheet, bool includeProperties); /// /// Exports a list of items to xml as an diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 5df5602bc6..fcf5f2b2f3 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -15,6 +15,13 @@ namespace Umbraco.Cms.Core.Services void CreatePartialViewMacroFolder(string folderPath); void DeletePartialViewFolder(string folderPath); void DeletePartialViewMacroFolder(string folderPath); + + /// + /// Gets a list of all objects + /// + /// An enumerable list of objects + IEnumerable GetPartialViews(params string[] names); + IPartialView GetPartialView(string path); IPartialView GetPartialViewMacro(string path); Attempt CreatePartialView(IPartialView partialView, string snippetName = null, int userId = Constants.Security.SuperUserId); @@ -70,14 +77,14 @@ namespace Umbraco.Cms.Core.Services /// Gets a list of all objects /// /// An enumerable list of objects - IEnumerable GetStylesheets(params string[] names); + IEnumerable GetStylesheets(params string[] paths); /// /// Gets a object by its name /// - /// Name of the stylesheet incl. extension + /// Path of the stylesheet incl. extension /// A object - IStylesheet GetStylesheetByName(string name); + IStylesheet GetStylesheet(string path); /// /// Saves a @@ -127,12 +134,18 @@ namespace Umbraco.Cms.Core.Services /// The size of the stylesheet. long GetStylesheetFileSize(string filepath); + /// + /// Gets a list of all objects + /// + /// An enumerable list of objects + IEnumerable GetScripts(params string[] names); + /// /// Gets a object by its name /// /// Name of the script incl. extension /// A object - IScript GetScriptByName(string name); + IScript GetScript(string name); /// /// Saves a diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index fc4d4fd612..19b509134e 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Umbraco.Cms.Core.Models; @@ -198,16 +198,14 @@ namespace Umbraco.Cms.Core.Services /// /// The to save /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + Attempt Save(IMedia media, int userId = Constants.Security.SuperUserId); /// /// Saves a collection of objects /// /// Collection of to save /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + Attempt Save(IEnumerable medias, int userId = Constants.Security.SuperUserId); /// /// Gets an object by its 'UniqueId' @@ -302,9 +300,8 @@ namespace Umbraco.Cms.Core.Services /// /// /// - /// /// True if sorting succeeded, otherwise False - bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId, bool raiseEvents = true); + bool Sort(IEnumerable items, int userId = Constants.Security.SuperUserId); /// /// Creates an object using the alias of the diff --git a/src/Umbraco.Core/Services/IMemberGroupService.cs b/src/Umbraco.Core/Services/IMemberGroupService.cs index 16028ded3f..0b72906c2f 100644 --- a/src/Umbraco.Core/Services/IMemberGroupService.cs +++ b/src/Umbraco.Core/Services/IMemberGroupService.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Services IMemberGroup GetById(Guid id); IEnumerable GetByIds(IEnumerable ids); IMemberGroup GetByName(string name); - void Save(IMemberGroup memberGroup, bool raiseEvents = true); + void Save(IMemberGroup memberGroup); void Delete(IMemberGroup memberGroup); } } diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index c91eba5250..6093c0a4fe 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -125,18 +125,14 @@ namespace Umbraco.Cms.Core.Services /// /// An can be of type or /// or to Save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - void Save(T entity, bool raiseEvents = true); + void Save(T entity); /// /// Saves a list of objects /// /// An can be of type or /// to save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - void Save(IEnumerable entities, bool raiseEvents = true); + void Save(IEnumerable entities); /// /// Finds a list of objects by a partial email string diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index a4af73e084..25ce49b65d 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; @@ -243,9 +243,7 @@ namespace Umbraco.Cms.Core.Services /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in /// than all users will be removed from this group and only these users will be added /// - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true); + void Save(IUserGroup userGroup, int[] userIds = null); /// /// Deletes a UserGroup diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index dc20774142..e0d10668ea 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -34,17 +34,6 @@ namespace Umbraco.Extensions public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens) => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); - /// - /// Localize using the current thread culture - /// - /// - /// - /// - /// - /// - public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary tokens = null) - => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); - /// /// Localize a key without any variables /// @@ -55,7 +44,7 @@ namespace Umbraco.Extensions /// /// public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string[] tokens) - => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); + => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); /// /// Convert an array of strings to a dictionary of indices -> values @@ -76,7 +65,7 @@ namespace Umbraco.Extensions if (text == null) return null; - if (text.StartsWith("#") == false) + if (text.StartsWith("#") == false || text.IndexOf('_') == -1) return text; text = text.Substring(1); @@ -88,6 +77,9 @@ namespace Umbraco.Extensions var areaAndKey = text.Split('_'); + if (areaAndKey.Length < 2) + return text; + value = manager.Localize(areaAndKey[0], areaAndKey[1]); return value.StartsWith("[") ? text : value; } diff --git a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs index 7307c8964a..551efc475a 100644 --- a/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs +++ b/src/Umbraco.Core/Strings/UrlSegmentProviderCollection.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Strings { public class UrlSegmentProviderCollection : BuilderCollectionBase { - public UrlSegmentProviderCollection(IEnumerable items) - : base(items) - { } + public UrlSegmentProviderCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Templates/HtmlUrlParser.cs b/src/Umbraco.Core/Templates/HtmlUrlParser.cs index 97bab9276a..50dc937466 100644 --- a/src/Umbraco.Core/Templates/HtmlUrlParser.cs +++ b/src/Umbraco.Core/Templates/HtmlUrlParser.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Templates private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public HtmlUrlParser(IOptions contentSettings, ILogger logger ,IProfilingLogger profilingLogger, IIOHelper ioHelper) + public HtmlUrlParser(IOptions contentSettings, ILogger logger, IProfilingLogger profilingLogger, IIOHelper ioHelper) { _contentSettings = contentSettings.Value; _logger = logger; @@ -36,7 +36,8 @@ namespace Umbraco.Cms.Core.Templates /// public string EnsureUrls(string text) { - if (_contentSettings.ResolveUrlsFromTextString == false) return text; + if (_contentSettings.ResolveUrlsFromTextString == false) + return text; using (var timer = _profilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) { diff --git a/src/Umbraco.Core/Tour/TourFilterCollection.cs b/src/Umbraco.Core/Tour/TourFilterCollection.cs index 5c55d0e0cd..2864abbced 100644 --- a/src/Umbraco.Core/Tour/TourFilterCollection.cs +++ b/src/Umbraco.Core/Tour/TourFilterCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Tour @@ -8,11 +9,8 @@ namespace Umbraco.Cms.Core.Tour /// public class TourFilterCollection : BuilderCollectionBase { - /// - /// Initializes a new instance of the class. - /// - public TourFilterCollection(IEnumerable items) - : base(items) - { } + public TourFilterCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs index 8f1b20a7b1..ff42b5e8c3 100644 --- a/src/Umbraco.Core/Trees/SearchableTreeCollection.cs +++ b/src/Umbraco.Core/Trees/SearchableTreeCollection.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Trees { private readonly Dictionary _dictionary; - public SearchableTreeCollection(IEnumerable items, ITreeService treeService) + public SearchableTreeCollection(Func> items, ITreeService treeService) : base(items) { _dictionary = CreateDictionary(treeService); diff --git a/src/Umbraco.Core/Trees/TreeCollection.cs b/src/Umbraco.Core/Trees/TreeCollection.cs index 45a405217f..59fa99819c 100644 --- a/src/Umbraco.Core/Trees/TreeCollection.cs +++ b/src/Umbraco.Core/Trees/TreeCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Trees @@ -8,8 +9,9 @@ namespace Umbraco.Cms.Core.Trees /// public class TreeCollection : BuilderCollectionBase { - public TreeCollection(IEnumerable items) - : base(items) - { } + + public TreeCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2a30778d80..8008961f0c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -59,4 +59,8 @@ + + + + diff --git a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs index 9ff5073d17..66ad608881 100644 --- a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs +++ b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Composing; @@ -6,8 +6,8 @@ namespace Umbraco.Cms.Core { public class UmbracoApiControllerTypeCollection : BuilderCollectionBase { - public UmbracoApiControllerTypeCollection(IEnumerable items) - : base(items) - { } + public UmbracoApiControllerTypeCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/WebAssets/BundlingOptions.cs b/src/Umbraco.Core/WebAssets/BundlingOptions.cs new file mode 100644 index 0000000000..6a3c0b9bd1 --- /dev/null +++ b/src/Umbraco.Core/WebAssets/BundlingOptions.cs @@ -0,0 +1,44 @@ +using System; + +namespace Umbraco.Cms.Core.WebAssets +{ + public struct BundlingOptions : IEquatable + { + public static BundlingOptions OptimizedAndComposite => new BundlingOptions(true, true); + public static BundlingOptions OptimizedNotComposite => new BundlingOptions(true, false); + public static BundlingOptions NotOptimizedNotComposite => new BundlingOptions(false, false); + public static BundlingOptions NotOptimizedAndComposite => new BundlingOptions(false, true); + + public BundlingOptions(bool optimizeOutput = true, bool enabledCompositeFiles = true) + { + OptimizeOutput = optimizeOutput; + EnabledCompositeFiles = enabledCompositeFiles; + } + + /// + /// If true, the files in the bundle will be minified + /// + public bool OptimizeOutput { get; } + + /// + /// If true, the files in the bundle will be combined, if false the files + /// will be served as individual files. + /// + public bool EnabledCompositeFiles { get; } + + public override bool Equals(object obj) => obj is BundlingOptions options && Equals(options); + public bool Equals(BundlingOptions other) => OptimizeOutput == other.OptimizeOutput && EnabledCompositeFiles == other.EnabledCompositeFiles; + + public override int GetHashCode() + { + int hashCode = 2130304063; + hashCode = hashCode * -1521134295 + OptimizeOutput.GetHashCode(); + hashCode = hashCode * -1521134295 + EnabledCompositeFiles.GetHashCode(); + return hashCode; + } + + public static bool operator ==(BundlingOptions left, BundlingOptions right) => left.Equals(right); + + public static bool operator !=(BundlingOptions left, BundlingOptions right) => !(left == right); + } +} diff --git a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs index 8648a59d32..2595afe40e 100644 --- a/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs +++ b/src/Umbraco.Core/WebAssets/CustomBackOfficeAssetsCollection.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Composing; @@ -5,8 +6,8 @@ namespace Umbraco.Cms.Core.WebAssets { public class CustomBackOfficeAssetsCollection : BuilderCollectionBase { - public CustomBackOfficeAssetsCollection(IEnumerable items) - : base(items) - { } + public CustomBackOfficeAssetsCollection(Func> items) : base(items) + { + } } } diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index f287c722ad..7ca1cd883b 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.WebAssets /// /// Thrown if any of the paths specified are not absolute /// - void CreateCssBundle(string bundleName, bool optimizeOutput, params string[] filePaths); + void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths); /// /// Renders the html link tag for the bundle @@ -48,7 +48,7 @@ namespace Umbraco.Cms.Core.WebAssets /// /// Thrown if any of the paths specified are not absolute /// - void CreateJsBundle(string bundleName, bool optimizeOutput, params string[] filePaths); + void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths); /// /// Renders the html script tag for the bundle diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 134ffc070c..8b34289c9c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -93,6 +94,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection factory.GetRequiredService>(), factory.GetRequiredService(), factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), packageRepoFileName); private static LocalizedTextServiceFileSources SourcesFactory(IServiceProvider container) diff --git a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs index 997cbfe19f..4c745c8503 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs @@ -1,8 +1,11 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Examine; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -17,64 +20,38 @@ namespace Umbraco.Cms.Infrastructure.Examine public class MediaValueSetBuilder : BaseValueSetBuilder { private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly IUserService _userService; - private readonly ILogger _logger; private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer _serializer; + private readonly ContentSettings _contentSettings; - public MediaValueSetBuilder(PropertyEditorCollection propertyEditors, + public MediaValueSetBuilder( + PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, - IUserService userService, ILogger logger, IShortStringHelper shortStringHelper, IJsonSerializer serializer) + MediaUrlGeneratorCollection mediaUrlGenerators, + IUserService userService, + IShortStringHelper shortStringHelper, + IOptions contentSettings) : base(propertyEditors, false) { _urlSegmentProviders = urlSegmentProviders; + _mediaUrlGenerators = mediaUrlGenerators; _userService = userService; - _logger = logger; _shortStringHelper = shortStringHelper; - _serializer = serializer; + _contentSettings = contentSettings.Value; } /// public override IEnumerable GetValueSets(params IMedia[] media) { - foreach (var m in media) + foreach (IMedia m in media) { + var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); - var umbracoFilePath = string.Empty; - var umbracoFile = string.Empty; - - var umbracoFileSource = m.GetValue(Constants.Conventions.Media.File); - - if (umbracoFileSource.DetectIsJson()) - { - ImageCropperValue cropper = null; - try - { - cropper = _serializer.Deserialize( - m.GetValue(Constants.Conventions.Media.File)); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Could not Deserialize ImageCropperValue for item with key {m.Key} "); - } - - if (cropper != null) - { - umbracoFilePath = cropper.Src; - } - } - else - { - umbracoFilePath = umbracoFileSource; - } - - if (!string.IsNullOrEmpty(umbracoFilePath)) - { - // intentional dummy Uri - var uri = new Uri("https://localhost/" + umbracoFilePath); - umbracoFile = uri.Segments.Last(); - } + IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) + .Select(x => Path.GetFileName(x)) + .Distinct(); var values = new Dictionary> { @@ -92,7 +69,7 @@ namespace Umbraco.Cms.Infrastructure.Examine {"path", m.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", m.ContentType.Id.ToString().Yield() }, {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, - {UmbracoExamineFieldNames.UmbracoFileFieldName, umbracoFile.Yield()} + {UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles} }; foreach (var property in m.Properties) diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index 7d98a19091..529a148093 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -62,22 +62,22 @@ namespace Umbraco.Cms.Core.Manifest /// /// Initializes a new instance of the class. /// - private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string path, ILogger logger, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) + private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ManifestFilterCollection filters, string appPluginsPath, ILogger logger, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) { if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); _cache = appCaches.RuntimeCache; _validators = validators ?? throw new ArgumentNullException(nameof(validators)); _filters = filters ?? throw new ArgumentNullException(nameof(filters)); - if (path == null) throw new ArgumentNullException(nameof(path)); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(path)); + if (appPluginsPath == null) throw new ArgumentNullException(nameof(appPluginsPath)); + if (string.IsNullOrWhiteSpace(appPluginsPath)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(appPluginsPath)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _ioHelper = ioHelper; _hostingEnvironment = hostingEnvironment; - Path = path; + AppPluginsPath = appPluginsPath; } - public string Path + public string AppPluginsPath { get => _path; set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; @@ -87,11 +87,12 @@ namespace Umbraco.Cms.Core.Manifest /// Gets all manifests, merged into a single manifest object. /// /// - public PackageManifest Manifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => + public CompositePackageManifest CombinedManifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => { - var manifests = GetManifests(); + IEnumerable manifests = GetManifests(); return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0)); /// @@ -130,10 +131,10 @@ namespace Umbraco.Cms.Core.Manifest /// /// Merges all manifests into one. /// - private static PackageManifest MergeManifests(IEnumerable manifests) + private static CompositePackageManifest MergeManifests(IEnumerable manifests) { - var scripts = new HashSet(); - var stylesheets = new HashSet(); + var scripts = new Dictionary>(); + var stylesheets = new Dictionary>(); var propertyEditors = new List(); var parameterEditors = new List(); var gridEditors = new List(); @@ -141,10 +142,28 @@ namespace Umbraco.Cms.Core.Manifest var dashboards = new List(); var sections = new List(); - foreach (var manifest in manifests) + foreach (PackageManifest manifest in manifests) { - if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); - if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); + if (manifest.Scripts != null) + { + if (!scripts.TryGetValue(manifest.BundleOptions, out List scriptsPerBundleOption)) + { + scriptsPerBundleOption = new List(); + scripts[manifest.BundleOptions] = scriptsPerBundleOption; + } + scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); + } + + if (manifest.Stylesheets != null) + { + if (!stylesheets.TryGetValue(manifest.BundleOptions, out List stylesPerBundleOption)) + { + stylesPerBundleOption = new List(); + stylesheets[manifest.BundleOptions] = stylesPerBundleOption; + } + stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); + } + if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); @@ -153,17 +172,15 @@ namespace Umbraco.Cms.Core.Manifest if (manifest.Sections != null) sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias.ToLowerInvariant())); } - return new PackageManifest - { - Scripts = scripts.ToArray(), - Stylesheets = stylesheets.ToArray(), - PropertyEditors = propertyEditors.ToArray(), - ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray(), - ContentApps = contentApps.ToArray(), - Dashboards = dashboards.ToArray(), - Sections = sections.ToArray() - }; + return new CompositePackageManifest( + propertyEditors, + parameterEditors, + gridEditors, + contentApps, + dashboards, + sections, + scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), + stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); } // gets all manifest files (recursively) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 5f699d66d5..ae2098b75a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -9,6 +9,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table public class CreateTableOfDtoBuilder : IExecutableBuilder { private readonly IMigrationContext _context; + + // TODO: This doesn't do anything. private readonly DatabaseType[] _supportedDatabaseTypes; public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 83adbc6e8a..a7cf92e2a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.Extensions.Logging; using NPoco; @@ -151,7 +151,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picke (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 954ec0bf0a..f8d480bc8c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -211,7 +211,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade Merge() .To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}") .To("{4695D0C9-0729-4976-985B-048D503665D8}") - + .To("{5C424554-A32D-4852-8ED1-A13508187901}") // to 9.0.0 .With() .To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}") @@ -222,6 +222,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade //FINAL .As("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + + + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. + // - 8.15 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} + // - 8.15 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} + // - 9.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} + To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs index 5f9d87269d..ca4ec9bfe1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; @@ -9,11 +11,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes { private readonly ILogger _logger; - public PreValueMigratorCollection(IEnumerable items, ILogger logger) + public PreValueMigratorCollection(Func> items, ILogger logger) : base(items) { _logger = logger; - _logger.LogDebug("Migrators: " + string.Join(", ", items.Select(x => x.GetType().Name))); } public IPreValueMigrator GetMigrator(string editorAlias) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 2f0caa4939..df453cad43 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,6 +1,9 @@ +using NPoco; using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 { @@ -14,12 +17,49 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + // allow null for the `data` field + if (DatabaseType.IsSqlCe()) + { + // SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it + // All column ordering must remain the same as what is defined in the DTO so we need to create a temp table, + // drop orig and then re-create/copy. + Create.Table(withoutKeysAndIndexes: true).Do(); + Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do(); + Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do(); + Create.Table().Do(); + Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do(); + } + else + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + } + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, "dataRaw"); // allow null - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + } + + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] + public int NodeId { get; set; } + + [Column("published")] + public bool Published { get; set; } + + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Data { get; set; } + + [Column("rv")] + public long Rv { get; set; } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs new file mode 100644 index 0000000000..868343374d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +{ + public class UpdateCmsPropertyGroupIdSeed : MigrationBase + { + public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (DatabaseType.IsSqlCe()) + { + Database.Execute(Sql("ALTER TABLE [cmsPropertyTypeGroup] ALTER COLUMN [id] IDENTITY (56,1)")); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index 14cc8e31f8..9e5101550a 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Core.Models +namespace Umbraco.Cms.Core.Models { /// /// Represents a media item with local crops. diff --git a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs index 304e650bb6..92e1086fbd 100644 --- a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs @@ -1,7 +1,9 @@ using System; -using System.Xml.Linq; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; @@ -12,54 +14,35 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan { - private XDocument _xdoc; + protected AutomaticPackageMigrationPlan(string packageName) + : this(packageName, packageName) + { } - protected AutomaticPackageMigrationPlan(string name) - : base(name) - { - } - - protected AutomaticPackageMigrationPlan(string packageName, string planName) : base(packageName, planName) - { - } + protected AutomaticPackageMigrationPlan(string packageName, string planName) + : base(packageName, planName) + { } protected sealed override void DefinePlan() { // calculate the final state based on the hash value of the embedded resource - var finalId = PackageDataManifest.ToString(SaveOptions.DisableFormatting).ToGuid(); + Type planType = GetType(); + var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); + + var finalId = hash.ToGuid(); To(finalId); } - /// - /// Get the extracted package data xml manifest - /// - private XDocument PackageDataManifest - { - get - { - if (_xdoc != null) - { - return _xdoc; - } - - Type planType = GetType(); - _xdoc = PackageMigrationResource.GetEmbeddedPackageDataManifest(planType); - return _xdoc; - } - } - private class MigrateToPackageData : PackageMigrationBase { - public MigrateToPackageData(IPackagingService packagingService, IMigrationContext context) - : base(packagingService, context) + public MigrateToPackageData(IPackagingService packagingService, IMediaService mediaService, MediaFileManager mediaFileManager, MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IMigrationContext context) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context) { } protected override void Migrate() { var plan = (AutomaticPackageMigrationPlan)Context.Plan; - XDocument xml = plan.PackageDataManifest; - ImportPackage.FromXmlDataManifest(xml).Do(); + + ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs index 09c4ec74a5..c14d3e5119 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs @@ -1,6 +1,10 @@ using System; using System.Xml.Linq; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; @@ -9,8 +13,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging { internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, IExecutableBuilder { - public ImportPackageBuilder(IPackagingService packagingService, IMigrationContext context) - : base(new ImportPackageBuilderExpression(packagingService, context)) + public ImportPackageBuilder( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : base(new ImportPackageBuilderExpression( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context)) { } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index 441109f2ca..b16326ea56 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -1,19 +1,48 @@ using System; +using System.IO; +using System.IO.Compression; +using System.Linq; using System.Xml.Linq; +using System.Xml.XPath; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Packaging { internal class ImportPackageBuilderExpression : MigrationExpressionBase { private readonly IPackagingService _packagingService; + private readonly IMediaService _mediaService; + private readonly MediaFileManager _mediaFileManager; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IShortStringHelper _shortStringHelper; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private bool _executed; - public ImportPackageBuilderExpression(IPackagingService packagingService, IMigrationContext context) : base(context) - => _packagingService = packagingService; + public ImportPackageBuilderExpression( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) : base(context) + { + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } /// /// The type of the migration which dictates the namespace of the embedded resource @@ -37,19 +66,64 @@ namespace Umbraco.Cms.Infrastructure.Packaging throw new InvalidOperationException($"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); } - XDocument xml; + InstallationSummary installationSummary; if (EmbeddedResourceMigrationType != null) { // get the embedded resource - xml = PackageMigrationResource.GetEmbeddedPackageDataManifest(EmbeddedResourceMigrationType); + using (ZipArchive zipPackage = PackageMigrationResource.GetEmbeddedPackageDataManifest( + EmbeddedResourceMigrationType, + out XDocument xml)) + { + // first install the package + installationSummary = _packagingService.InstallCompiledPackageData(xml); + + // then we need to save each file to the saved media items + var mediaWithFiles = xml.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); + + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) + { + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + { + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) + { + throw new InvalidOperationException("No media file found in package zip for path " + entryPath); + } + + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) + { + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath), + mediaStream); + } + + _mediaService.Save(media); + } + } + } } else { - xml = PackageDataManifest; + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); } - InstallationSummary installationSummary = _packagingService.InstallCompiledPackageData(xml); - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index e680471486..fcf4ada7c0 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net; +using System.Text; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; @@ -39,6 +41,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private readonly IConfigurationEditorJsonSerializer _serializer; private readonly IMediaService _mediaService; private readonly IMediaTypeService _mediaTypeService; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly IContentService _contentService; @@ -59,7 +62,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging IOptions globalSettings, IConfigurationEditorJsonSerializer serializer, IMediaService mediaService, - IMediaTypeService mediaTypeService) + IMediaTypeService mediaTypeService, + IHostingEnvironment hostingEnvironment) { _dataValueEditorFactory = dataValueEditorFactory; _logger = logger; @@ -74,6 +78,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging _serializer = serializer; _mediaService = mediaService; _mediaTypeService = mediaTypeService; + _hostingEnvironment = hostingEnvironment; _entityService = entityService; _contentTypeService = contentTypeService; _contentService = contentService; @@ -91,7 +96,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId), LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId), DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId), - MacrosInstalled = ImportMacros(compiledPackage.Macros, userId), + MacrosInstalled = ImportMacros(compiledPackage.Macros, compiledPackage.MacroPartialViews, userId), TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId), DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId), MediaTypesInstalled = ImportMediaTypes(compiledPackage.MediaTypes, userId), @@ -102,6 +107,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging var importedMediaTypes = installationSummary.MediaTypesInstalled.ToDictionary(x => x.Alias, x => x); installationSummary.StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId); + installationSummary.PartialViewsInstalled = ImportPartialViews(compiledPackage.PartialViews, userId); + installationSummary.ScriptsInstalled = ImportScripts(compiledPackage.Scripts, userId); installationSummary.ContentInstalled = ImportContentBase(compiledPackage.Documents, importedDocTypes, userId, _contentTypeService, _contentService); installationSummary.MediaInstalled = ImportContentBase(compiledPackage.Media, importedMediaTypes, userId, _mediaTypeService, _mediaService); @@ -168,7 +175,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging var contents = ParseContentBaseRootXml(roots, parentId, importedDocumentTypes, typeService, service).ToList(); if (contents.Any()) + { service.Save(contents, userId); + } return contents; @@ -189,29 +198,35 @@ namespace Umbraco.Cms.Infrastructure.Packaging // "'DocumentSet' (for structured imports) nor is the first element a Document (for single document import)."); } - private IEnumerable ParseContentBaseRootXml( + private IEnumerable ParseContentBaseRootXml( IEnumerable roots, int parentId, - IDictionary importedContentTypes, - IContentTypeBaseService typeService, - IContentServiceBase service) - where T : class, IContentBase - where S : IContentTypeComposition + IDictionary importedContentTypes, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition { - var contents = new List(); - foreach (var root in roots) + var contents = new List(); + foreach (XElement root in roots) { var contentTypeAlias = root.Name.LocalName; if (!importedContentTypes.ContainsKey(contentTypeAlias)) { - var contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + if (contentType == null) + { + throw new InvalidOperationException("Could not find content type with alias " + contentTypeAlias); + } importedContentTypes.Add(contentTypeAlias, contentType); } - var content = CreateContentFromXml(root, importedContentTypes[contentTypeAlias], default, parentId, service); + TContentBase content = CreateContentFromXml(root, importedContentTypes[contentTypeAlias], default, parentId, service); if (content == null) + { continue; + } contents.Add(content); @@ -280,7 +295,6 @@ namespace Umbraco.Cms.Infrastructure.Packaging return null; } - var id = element.Attribute("id").Value; var level = element.Attribute("level").Value; var sortOrder = element.Attribute("sortOrder").Value; var nodeName = element.Attribute("nodeName").Value; @@ -1030,7 +1044,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (dataTypes.Count > 0) { - _dataTypeService.Save(dataTypes, userId, true); + _dataTypeService.Save(dataTypes, userId); } return dataTypes; @@ -1236,18 +1250,43 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Xml to import /// Optional id of the User performing the operation /// - public IReadOnlyList ImportMacros(IEnumerable macroElements, int userId) + public IReadOnlyList ImportMacros( + IEnumerable macroElements, + IEnumerable macroPartialViewsElements, + int userId) { var macros = macroElements.Select(ParseMacroElement).ToList(); - foreach (var macro in macros) + foreach (IMacro macro in macros) { _macroService.Save(macro, userId); } + ImportMacroPartialViews(macroPartialViewsElements); + return macros; } + private void ImportMacroPartialViews(IEnumerable viewElements) + { + foreach (XElement element in viewElements) + { + var path = element.AttributeValue("path"); + if (path == null) + { + throw new InvalidOperationException("No path attribute found"); + } + var contents = element.Value ?? string.Empty; + + var physicalPath = _hostingEnvironment.MapPathContentRoot(path); + // TODO: Do we overwrite? IMO I don't think so since these will be views a user will change. + if (!System.IO.File.Exists(physicalPath)) + { + System.IO.File.WriteAllText(physicalPath, contents, Encoding.UTF8); + } + } + } + private IMacro ParseMacroElement(XElement macroElement) { var macroKey = Guid.Parse(macroElement.Element("key").Value); @@ -1328,30 +1367,94 @@ namespace Umbraco.Cms.Infrastructure.Packaging #endregion + public IReadOnlyList ImportScripts(IEnumerable scriptElements, int userId) + { + var result = new List(); + + foreach (XElement scriptXml in scriptElements) + { + var path = scriptXml.AttributeValue("path"); + + if (path.IsNullOrWhiteSpace()) + { + continue; + } + + IScript script = _fileService.GetScript(path); + + // only update if it doesn't exist + if (script == null) + { + var content = scriptXml.Value; + if (content == null) + { + continue; + } + + script = new Script(path) { Content = content }; + _fileService.SaveScript(script, userId); + result.Add(script); + } + } + + return result; + } + + public IReadOnlyList ImportPartialViews(IEnumerable partialViewElements, int userId) + { + var result = new List(); + + foreach (XElement partialViewXml in partialViewElements) + { + var path = partialViewXml.AttributeValue("path"); + + if (path == null) + { + throw new InvalidOperationException("No path attribute found"); + } + + IPartialView partialView = _fileService.GetPartialView(path); + + // only update if it doesn't exist + if (partialView == null) + { + var content = partialViewXml.Value ?? string.Empty; + + partialView = new PartialView(PartialViewType.PartialView, path) { Content = content }; + _fileService.SavePartialView(partialView, userId); + result.Add(partialView); + } + } + + return result; + } + #region Stylesheets public IReadOnlyList ImportStylesheets(IEnumerable stylesheetElements, int userId) { var result = new List(); - foreach (var n in stylesheetElements) + foreach (XElement n in stylesheetElements) { - var stylesheetName = n.Element("Name")?.Value; - if (stylesheetName.IsNullOrWhiteSpace()) - continue; + var stylesheetPath = n.Element("FileName")?.Value; - var s = _fileService.GetStylesheetByName(stylesheetName); + if (stylesheetPath.IsNullOrWhiteSpace()) + { + continue; + } + + IStylesheet s = _fileService.GetStylesheet(stylesheetPath); if (s == null) { - var fileName = n.Element("FileName")?.Value; - if (fileName == null) - continue; var content = n.Element("Content")?.Value; if (content == null) + { continue; + } - s = new Stylesheet(fileName) { Content = content }; - _fileService.SaveStylesheet(s); + s = new Stylesheet(stylesheetPath) { Content = content }; + _fileService.SaveStylesheet(s, userId); } foreach (var prop in n.XPathSelectElements("Properties/Property")) @@ -1379,7 +1482,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging sp.Alias = alias; sp.Value = prop.Element("Value")?.Value; } - _fileService.SaveStylesheet(s); + _fileService.SaveStylesheet(s, userId); result.Add(s); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs index ebe6f82a8f..3166cdbd4f 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs @@ -1,7 +1,8 @@ -using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; namespace Umbraco.Cms.Infrastructure.Packaging { @@ -9,11 +10,39 @@ namespace Umbraco.Cms.Infrastructure.Packaging public abstract class PackageMigrationBase : MigrationBase { private readonly IPackagingService _packagingService; + private readonly IMediaService _mediaService; + private readonly MediaFileManager _mediaFileManager; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IShortStringHelper _shortStringHelper; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - public PackageMigrationBase(IPackagingService packagingService, IMigrationContext context) : base(context) - => _packagingService = packagingService; + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : base(context) + { + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } - public IImportPackageBuilder ImportPackage => BeginBuild(new ImportPackageBuilder(_packagingService, Context)); + public IImportPackageBuilder ImportPackage => BeginBuild( + new ImportPackageBuilder( + _packagingService, + _mediaService, + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Context)); } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs index 2a17add2e6..aa390dcaa4 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Composing; namespace Umbraco.Cms.Core.Packaging @@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.Packaging /// public class PackageMigrationPlanCollection : BuilderCollectionBase { - public PackageMigrationPlanCollection(IEnumerable items) : base(items) + public PackageMigrationPlanCollection(Func> items) : base(items) { } } diff --git a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs index 4ce6abe7a0..a5524b44de 100644 --- a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs @@ -13,10 +13,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + if (!records.Any()) return 0; - return BulkInsertRecordsWithCommands(database, recordsA); + return BulkInsertRecordsWithCommands(database, records.ToArray()); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 8814c01761..797400b7cc 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using NPoco; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence @@ -73,7 +74,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence return mapperFactory.Mappers; } - return new NPocoMapperCollection(Array.Empty()); + return new NPocoMapperCollection(() => Enumerable.Empty()); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 3fc65b28a5..5c56f642c8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -24,12 +24,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Index(IndexTypes.NonClustered)] public int UserId { get; set; } + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// [Column("loginProvider")] [Length(4000)] // TODO: This value seems WAY too high, this is just a name [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_LoginProvider")] public string LoginProvider { get; set; } + /// + /// Stores the key the provider uses to lookup the login + /// [Column("providerKey")] [Length(4000)] [NullSetting(NullSetting = NullSettings.NotNull)] diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs index 5f3b2a9572..1fbc1b734c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs @@ -12,7 +12,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos internal class PropertyTypeGroupDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 12)] + [PrimaryKeyColumn(IdentitySeed = 56)] public int Id { get; set; } [Column("contenttypeNodeId")] diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs index a719308443..feaacd714b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using Umbraco.Cms.Core.Composing; @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers { public class MapperCollection : BuilderCollectionBase, IMapperCollection { - public MapperCollection(IEnumerable items) + public MapperCollection(Func> items) : base(items) { diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs index a1b61198d3..3d71c0225e 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Composing; @@ -6,7 +7,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { public sealed class NPocoMapperCollection : BuilderCollectionBase { - public NPocoMapperCollection(IEnumerable items) : base(items) + public NPocoMapperCollection(Func> items) : base(items) { } } diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs index 71e22a4837..38e6c23e70 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs @@ -40,9 +40,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - // only real columns, exclude result columns + // only real columns, exclude result/computed columns + // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 _readerColumns = pd.Columns - .Where(x => x.Value.ResultColumn == false) + .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) .Select(x => x.Value) .ToArray(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 2055006415..c72e11f595 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -37,7 +37,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement where TEntity : class, IContentBase where TRepository : class, IRepository { - private readonly Lazy _propertyEditors; private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; private readonly IEventAggregator _eventAggregator; @@ -58,7 +57,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, + PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IEventAggregator eventAggregator) @@ -68,7 +67,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; - _propertyEditors = propertyEditors; + PropertyEditors = propertyEditors; _dataValueReferenceFactories = dataValueReferenceFactories; _eventAggregator = eventAggregator; } @@ -85,7 +84,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected IRelationRepository RelationRepository { get; } protected IRelationTypeRepository RelationTypeRepository { get; } - protected PropertyEditorCollection PropertyEditors => _propertyEditors.Value; + protected PropertyEditorCollection PropertyEditors { get; } #region Versions diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 001f2db000..f70048c4f9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -29,14 +29,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository { - private readonly Lazy _editors; + private readonly PropertyEditorCollection _editors; private readonly IConfigurationEditorJsonSerializer _serializer; private readonly ILogger _dataTypeLogger; public DataTypeRepository( IScopeAccessor scopeAccessor, AppCaches cache, - Lazy editors, + PropertyEditorCollection editors, ILogger logger, ILoggerFactory loggerFactory, IConfigurationEditorJsonSerializer serializer) @@ -68,7 +68,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } var dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors.Value, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -79,7 +79,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var dtos = Database.Fetch(sql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors.Value, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); } #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 828c521f93..79663c292e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, + PropertyEditorCollection propertyEditorCollection, IDataTypeService dataTypeService, DataValueReferenceFactoryCollection dataValueReferenceFactories, IJsonSerializer serializer, diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index b246839440..0353106e17 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, + PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IJsonSerializer serializer, @@ -451,20 +451,27 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out var editedCultures); - foreach (var propertyDataDto in propertyDataDtos) + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out HashSet editedCultures); + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { Database.Insert(propertyDataDto); + } // if !publishing, we may have a new name != current publish name, // also impacts 'edited' if (!publishing && entity.PublishName != entity.Name) + { edited = true; + } // persist the document dto // at that point, when publishing, the entity still has its old Published value // so we need to explicitly update the dto to persist the correct value if (entity.PublishedState == PublishedState.Publishing) + { dto.Published = true; + } + dto.NodeId = nodeDto.NodeId; entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Insert(dto); @@ -472,19 +479,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement //insert the schedule PersistContentSchedule(entity, false); - // persist the variations if (entity.ContentType.VariesByCulture()) { - // bump dates to align cultures to version - if (publishing) - entity.AdjustDates(contentVersionDto.VersionDate); - // names also impact 'edited' // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos) + foreach (ContentCultureInfos cultureInfo in entity.CultureInfos) + { if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + { + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + } + } + + // refresh content + entity.SetCultureEdited(editedCultures); + + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); // insert content variations Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); @@ -493,9 +505,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } - // refresh content - entity.SetCultureEdited(editedCultures); - // trigger here, before we reset Published etc OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); @@ -547,7 +556,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // check if we need to make any database changes at all if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) + { return; // no change to save, do nothing, don't even update dates + } // whatever we do, we must check that we are saving the current version var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); @@ -633,30 +644,30 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.Insert(documentVersionDto); } - // replace the property data (rather than updating) + // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - // insert property data - ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out var editedCultures); + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + + // insert property data + ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out HashSet editedCultures); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' if (!publishing && entity.PublishName != entity.Name) + { edited = true; + } if (entity.ContentType.VariesByCulture()) { - // bump dates to align cultures to version - if (publishing) - entity.AdjustDates(contentVersionDto.VersionDate); - // names also impact 'edited' // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos) + foreach (var cultureInfo in entity.CultureInfos) + { if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) { edited = true; - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); // TODO: change tracking // at the moment, we don't do any dirty tracking on property values, so we don't know whether the @@ -664,6 +675,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // when the name is set, and it all works because the controller does it - but, if someone uses a // service to change a property value and save (without setting name), the update date does not change. } + } + + // refresh content + entity.SetCultureEdited(editedCultures); + + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); // replace the content version variations (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet @@ -687,27 +705,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } - // refresh content - entity.SetCultureEdited(editedCultures); - // update the document dto // at that point, when un/publishing, the entity still has its old Published value // so we need to explicitly update the dto to persist the correct value if (entity.PublishedState == PublishedState.Publishing) + { dto.Published = true; + } else if (entity.PublishedState == PublishedState.Unpublishing) + { dto.Published = false; + } + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited Database.Update(dto); //update the schedule - if (entity.IsPropertyDirty("ContentSchedule")) + if (entity.IsPropertyDirty(nameof(entity.ContentSchedule))) + { PersistContentSchedule(entity, true); + } // if entity is publishing, update tags, else leave tags there // means that implicitly unpublished, or trashed, entities *still* have tags in db if (entity.PublishedState == PublishedState.Publishing) + { SetEntityTags(entity, _tagRepository, _serializer); + } } // trigger here, before we reset Published etc @@ -1224,7 +1248,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { Template1Id = dto.DocumentVersionDto.TemplateId }; - if (dto.Published) temp.Template2Id = dto.PublishedVersionDto.TemplateId; + if (dto.Published) + temp.Template2Id = dto.PublishedVersionDto.TemplateId; temps.Add(temp); } @@ -1397,7 +1422,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement if (temp.PublishedVersionId > 0) versions.Add(temp.PublishedVersionId); } - if (versions.Count == 0) return new Dictionary>(); + if (versions.Count == 0) + return new Dictionary>(); var dtos = Database.FetchByGroups(versions, 2000, batch => Sql() @@ -1467,7 +1493,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // if not publishing, we're just updating the 'current' (non-published) version, // so there are no DTOs to create for the 'published' version which remains unchanged - if (!publishing) yield break; + if (!publishing) + yield break; // create dtos for the 'published' version, for published cultures (those having a name) // ReSharper disable once UseDeconstruction @@ -1588,7 +1615,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private void EnsureVariantNamesAreUnique(IContent content, bool publishing) { - if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos.Count == 0) return; + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos.Count == 0) + return; // get names per culture, at same level (ie all siblings) var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); @@ -1596,7 +1624,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .GroupBy(x => x.LanguageId) .ToDictionary(x => x.Key, x => x); - if (names.Count == 0) return; + if (names.Count == 0) + return; // note: the code below means we are going to unique-ify every culture names, regardless // of whether the name has changed (ie the culture has been updated) - some saving culture @@ -1605,14 +1634,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement foreach (var cultureInfo in content.CultureInfos) { var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); - if (!langId.HasValue) continue; - if (!names.TryGetValue(langId.Value, out var cultureNames)) continue; + if (!langId.HasValue) + continue; + if (!names.TryGetValue(langId.Value, out var cultureNames)) + continue; // get a unique name var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); - if (uniqueName == content.GetCultureName(cultureInfo.Culture)) continue; + if (uniqueName == content.GetCultureName(cultureInfo.Culture)) + continue; // update the name, and the publish name if published content.SetCultureName(uniqueName, cultureInfo.Culture); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index b69907d71b..cc188787ba 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, + PropertyEditorCollection propertyEditorCollection, MediaUrlGeneratorCollection mediaUrlGenerators, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 698d0fffa7..cc1ce7aec0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -51,7 +51,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, IPasswordHasher passwordHasher, - Lazy propertyEditors, + PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IJsonSerializer serializer, diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs index e1ced4f375..f1f3233a45 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Linq; using NPoco; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -14,8 +15,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// public class SqlContext : ISqlContext { - private readonly Lazy _mappers; - /// /// Initializes a new instance of the class. /// @@ -23,21 +22,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// The Poco data factory. /// The database type. /// The mappers. - public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection mappers = null) - : this(sqlSyntax, databaseType, pocoDataFactory, new Lazy(() => mappers ?? new MapperCollection(Enumerable.Empty()))) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The sql syntax provider. - /// The Poco data factory. - /// The database type. - /// The mappers. - public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, Lazy mappers) + public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection mappers = null) { // for tests - _mappers = mappers; + Mappers = mappers; SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); @@ -67,6 +55,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence public IPocoDataFactory PocoDataFactory { get; } /// - public IMapperCollection Mappers => _mappers.Value; + public IMapperCollection Mappers { get; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs index 8b80010335..d67c97f2c4 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs @@ -39,6 +39,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// The number of records that were inserted. private int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) { + // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. + // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader + // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + // create command against the original database.Connection using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) { @@ -50,7 +54,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider; if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + { + BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. + DestinationTableName = tableName, + // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 + BatchSize = 4096 + }) using (var bulkReader = new PocoDataDataReader(records, pocoData, syntax)) { //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index c6c0c6b0bb..756490c531 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Data.Common; +using System.Linq; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -49,10 +51,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence } public void CreateDatabase(string providerName) - { - throw new NotSupportedException("Embedded databases are not supported"); - } + => throw new NotSupportedException("Embedded databases are not supported"); - public NPocoMapperCollection ProviderSpecificMappers(string providerName) => new NPocoMapperCollection(Array.Empty()); + public NPocoMapperCollection ProviderSpecificMappers(string providerName) + => new NPocoMapperCollection(() => Enumerable.Empty()); } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 0f973101e3..03977a0abe 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly NPocoMapperCollection _npocoMappers; private readonly IOptions _globalSettings; - private readonly Lazy _mappers; + private readonly IMapperCollection _mappers; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -81,7 +81,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence ILoggerFactory loggerFactory, IOptions globalSettings, IOptions connectionStrings, - Lazy mappers, + IMapperCollection mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, NPocoMapperCollection npocoMappers) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 49cc6b2902..550a64b14d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -26,18 +26,14 @@ namespace Umbraco.Cms.Core.PropertyEditors { public const string ContentTypeKeyPropertyKey = "contentTypeKey"; public const string UdiPropertyKey = "udi"; - private readonly Lazy _propertyEditors; public BlockEditorPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, - Lazy propertyEditors) + PropertyEditorCollection propertyEditors) : base(dataValueEditorFactory) - { - _propertyEditors = propertyEditors; - } + => PropertyEditors = propertyEditors; - // has to be lazy else circular dep in ctor - private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; + private PropertyEditorCollection PropertyEditors { get; } #region Value Editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index 469d908cb3..0955aa5198 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public BlockListPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, - Lazy propertyEditors, + PropertyEditorCollection propertyEditors, IIOHelper ioHelper) : base(dataValueEditorFactory, propertyEditors) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs index 3511a4ce0e..6cfc248827 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs @@ -69,9 +69,9 @@ namespace Umbraco.Cms.Core.PropertyEditors return editor; } - public bool TryGetMediaPath(string alias, object value, out string mediaPath) + public bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath) { - if (alias == Alias) + if (propertyEditorAlias == Alias) { mediaPath = value?.ToString(); return true; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs index 3892071e57..805d92a267 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -92,11 +92,13 @@ namespace Umbraco.Cms.Core.PropertyEditors } // process the file - var filepath = editorFile == null ? null : ProcessFile(editorValue, file, currentPath, cuid, puid); + var filepath = editorFile == null ? null : ProcessFile(file, cuid, puid); // remove all temp files - foreach (var f in uploads) + foreach (ContentPropertyFile f in uploads) + { File.Delete(f.TempFilePath); + } // remove current file if replaced if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) @@ -109,16 +111,18 @@ namespace Umbraco.Cms.Core.PropertyEditors } - private string ProcessFile(ContentPropertyData editorValue, ContentPropertyFile file, string currentPath, Guid cuid, Guid puid) + private string ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid) { // process the file // no file, invalid file, reject change if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) == false) + { return null; + } // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) - var filepath = _mediaFileManager.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path + var filepath = _mediaFileManager.GetMediaPath(file.FileName, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index f9425cbade..f21a18401c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -68,9 +68,9 @@ namespace Umbraco.Cms.Core.PropertyEditors _logger = loggerFactory.CreateLogger(); } - public bool TryGetMediaPath(string alias, object value, out string mediaPath) + public bool TryGetMediaPath(string propertyEditorAlias, object value, out string mediaPath) { - if (alias == Alias) + if (propertyEditorAlias == Alias) { mediaPath = GetFileSrcFromPropertyValue(value, out _, false); return true; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index c375b6f576..10a99ccd99 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -142,15 +142,19 @@ namespace Umbraco.Cms.Core.PropertyEditors } // process the file - var filepath = editorJson == null ? null : ProcessFile(editorValue, file, currentPath, cuid, puid); + var filepath = editorJson == null ? null : ProcessFile(file, cuid, puid); // remove all temp files - foreach (var f in uploads) + foreach (ContentPropertyFile f in uploads) + { File.Delete(f.TempFilePath); + } // remove current file if replaced if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) + { _mediaFileManager.FileSystem.DeleteFile(currentPath); + } // update json and return if (editorJson == null) return null; @@ -158,16 +162,18 @@ namespace Umbraco.Cms.Core.PropertyEditors return editorJson.ToString(); } - private string ProcessFile(ContentPropertyData editorValue, ContentPropertyFile file, string currentPath, Guid cuid, Guid puid) + private string ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid) { // process the file // no file, invalid file, reject change if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) == false) + { return null; + } // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) - var filepath = _mediaFileManager.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path + var filepath = _mediaFileManager.GetMediaPath(file.FileName, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs index f92b2911dd..80769663c6 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -28,11 +28,10 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly IMediaService _mediaService; private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private readonly MediaFileManager _mediaFileManager; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly IShortStringHelper _shortStringHelper; private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IJsonSerializer _serializer; - - const string TemporaryImageDataAttribute = "data-tmpimg"; + private const string TemporaryImageDataAttribute = "data-tmpimg"; public RichTextEditorPastedImages( IUmbracoContextAccessor umbracoContextAccessor, @@ -41,9 +40,9 @@ namespace Umbraco.Cms.Core.PropertyEditors IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, - IPublishedUrlProvider publishedUrlProvider, - IJsonSerializer serializer) + IPublishedUrlProvider publishedUrlProvider) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -51,9 +50,9 @@ namespace Umbraco.Cms.Core.PropertyEditors _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; _shortStringHelper = shortStringHelper; _publishedUrlProvider = publishedUrlProvider; - _serializer = serializer; } /// @@ -107,7 +106,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); using (fileStream) { - mediaFile.SetValue(_mediaFileManager, _shortStringHelper, _contentTypeBaseServiceProvider, _serializer, Constants.Conventions.Media.File, safeFileName, fileStream); + mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); } _mediaService.Save(mediaFile, userId); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index 51bf3c477c..ae1290d6a4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Serialization; -using Umbraco.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index dc25007e7c..f206e063a3 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Runtime; @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime loggerFactory, _globalSettings, connectionStrings, - new Lazy(() => new MapperCollection(Enumerable.Empty())), + new MapperCollection(() => Enumerable.Empty()), dbProviderFactoryCreator, databaseSchemaCreatorFactory, npocoMappers); diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 3d4c6dfbbe..4ea399f500 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -379,10 +379,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// /// - /// /// - Attempt IContentServiceBase.Save(IEnumerable contents, int userId, - bool raiseEvents) => Attempt.Succeed(Save(contents, userId, raiseEvents)); + Attempt IContentServiceBase.Save(IEnumerable contents, int userId) => Attempt.Succeed(Save(contents, userId)); /// /// Gets objects by Ids @@ -756,7 +754,7 @@ namespace Umbraco.Cms.Core.Services.Implement #region Save, Publish, Unpublish /// - public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) { PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) @@ -774,13 +772,13 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new ContentSavingNotification(content, eventMessages); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return OperationResult.Cancel(eventMessages); } - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); if (content.HasIdentity == false) { @@ -800,10 +798,12 @@ namespace Umbraco.Cms.Core.Services.Implement _documentRepository.Save(content); - if (raiseEvents) - { - scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification)); + + // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?! + // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone + // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf + // reasons like bulk import and in those cases we don't want this occuring. scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages)); if (culturesChanging != null) @@ -823,7 +823,7 @@ namespace Umbraco.Cms.Core.Services.Implement } /// - public OperationResult Save(IEnumerable contents, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public OperationResult Save(IEnumerable contents, int userId = Cms.Core.Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); IContent[] contentsA = contents.ToArray(); @@ -831,7 +831,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new ContentSavingNotification(contentsA, eventMessages); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return OperationResult.Cancel(eventMessages); @@ -850,11 +850,10 @@ namespace Umbraco.Cms.Core.Services.Implement _documentRepository.Save(content); } - if (raiseEvents) - { - scope.Notifications.Publish(new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification)); + // TODO: See note above about supressing events scope.Notifications.Publish(new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages)); + Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Saved multiple content"); scope.Complete(); @@ -864,7 +863,7 @@ namespace Umbraco.Cms.Core.Services.Implement } /// - public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); @@ -912,14 +911,14 @@ namespace Umbraco.Cms.Core.Services.Implement // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); return result; } } /// - public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = 0, bool raiseEvents = true) + public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = 0) { if (content == null) throw new ArgumentNullException(nameof(content)); if (cultures == null) throw new ArgumentNullException(nameof(cultures)); @@ -938,7 +937,7 @@ namespace Umbraco.Cms.Core.Services.Implement var evtMsgs = EventMessagesFactory.Get(); var savingNotification = new ContentSavingNotification(content, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); } @@ -948,7 +947,7 @@ namespace Umbraco.Cms.Core.Services.Implement if (cultures.Length == 0 && !varies) { //no cultures specified and doesn't vary, so publish it, else nothing to publish - return SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents); + return SaveAndPublish(content, userId: userId); } if (cultures.Any(x => x == null || x == "*")) @@ -959,9 +958,11 @@ namespace Umbraco.Cms.Core.Services.Implement // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. foreach (var impact in impacts) + { content.PublishCulture(impact); + } - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); return result; } @@ -1067,7 +1068,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// The document is *always* saved, even when publishing fails. /// internal PublishResult CommitDocumentChanges(IContent content, - int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + int userId = Cms.Core.Constants.Security.SuperUserId) { using (var scope = ScopeProvider.CreateScope()) { @@ -1083,7 +1084,7 @@ namespace Umbraco.Cms.Core.Services.Implement var allLangs = _languageRepository.GetMany().ToList(); - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); return result; } @@ -1097,7 +1098,6 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// /// - /// /// /// /// @@ -1111,8 +1111,8 @@ namespace Umbraco.Cms.Core.Services.Implement private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, EventMessages eventMessages, IReadOnlyCollection allLangs, IDictionary notificationState, - int userId = Cms.Core.Constants.Security.SuperUserId, - bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + int userId = Constants.Security.SuperUserId, + bool branchOne = false, bool branchRoot = false) { if (scope == null) { @@ -1269,10 +1269,7 @@ namespace Umbraco.Cms.Core.Services.Implement SaveDocument(content); // raise the Saved event, always - if (raiseEvents) - { - scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithState(notificationState)); - } + scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithState(notificationState)); if (unpublishing) // we have tried to unpublish - won't happen in a branch { @@ -2381,9 +2378,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// /// - /// /// Result indicating what action was taken when handling the command. - public OperationResult Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public OperationResult Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); @@ -2394,7 +2390,7 @@ namespace Umbraco.Cms.Core.Services.Implement { scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); - var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents); + var ret = Sort(scope, itemsA, userId, evtMsgs); scope.Complete(); return ret; } @@ -2410,9 +2406,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// /// - /// /// Result indicating what action was taken when handling the command. - public OperationResult Sort(IEnumerable ids, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public OperationResult Sort(IEnumerable ids, int userId = Cms.Core.Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); @@ -2424,29 +2419,27 @@ namespace Umbraco.Cms.Core.Services.Implement scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); var itemsA = GetByIds(idsA).ToArray(); - var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents); + var ret = Sort(scope, itemsA, userId, evtMsgs); scope.Complete(); return ret; } } - private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages eventMessages, bool raiseEvents) + private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages eventMessages) { var sortingNotification = new ContentSortingNotification(itemsA, eventMessages); var savingNotification = new ContentSavingNotification(itemsA, eventMessages); - if (raiseEvents) - { - // raise cancelable sorting event - if (scope.Notifications.PublishCancelable(sortingNotification)) - { - return OperationResult.Cancel(eventMessages); - } - // raise cancelable saving event - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return OperationResult.Cancel(eventMessages); - } + // raise cancelable sorting event + if (scope.Notifications.PublishCancelable(sortingNotification)) + { + return OperationResult.Cancel(eventMessages); + } + + // raise cancelable saving event + if (scope.Notifications.PublishCancelable(savingNotification)) + { + return OperationResult.Cancel(eventMessages); } var published = new List(); @@ -2479,16 +2472,13 @@ namespace Umbraco.Cms.Core.Services.Implement _documentRepository.Save(content); } - if (raiseEvents) - { - //first saved, then sorted - scope.Notifications.Publish(new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification)); - } + //first saved, then sorted + scope.Notifications.Publish(new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification)); scope.Notifications.Publish(new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages)); - if (raiseEvents && published.Any()) + if (published.Any()) { scope.Notifications.Publish(new ContentPublishedNotification(published, eventMessages)); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs index b20be692df..50caca0ec8 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/DataTypeService.cs @@ -434,18 +434,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// to save /// Id of the user issuing the save - public void Save(IEnumerable dataTypeDefinitions, int userId = Cms.Core.Constants.Security.SuperUserId) - { - Save(dataTypeDefinitions, userId, true); - } - - /// - /// Saves a collection of - /// - /// to save - /// Id of the user issuing the save - /// Boolean indicating whether or not to raise events - public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) + public void Save(IEnumerable dataTypeDefinitions, int userId) { var evtMsgs = EventMessagesFactory.Get(); var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); @@ -453,7 +442,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var savingDataTypeNotification = new DataTypeSavingNotification(dataTypeDefinitions, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingDataTypeNotification)) + if (scope.Notifications.PublishCancelable(savingDataTypeNotification)) { scope.Complete(); return; @@ -465,10 +454,8 @@ namespace Umbraco.Cms.Core.Services.Implement _dataTypeRepository.Save(dataTypeDefinition); } - if (raiseEvents) - { - scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification)); - } + scope.Notifications.Publish(new DataTypeSavedNotification(dataTypeDefinitions, evtMsgs).WithStateFrom(savingDataTypeNotification)); + Audit(AuditType.Save, userId, -1); scope.Complete(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs index 88b19e6270..11fbd87232 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net; using System.Xml.Linq; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -98,7 +100,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// public XElement Serialize( IMedia media, - bool withDescendants = false) + bool withDescendants = false, + Action onMediaItemSerialized = null) { if (_mediaService == null) throw new ArgumentNullException(nameof(_mediaService)); if (_dataTypeService == null) throw new ArgumentNullException(nameof(_dataTypeService)); @@ -110,7 +113,9 @@ namespace Umbraco.Cms.Core.Services.Implement var nodeName = media.ContentType.Alias.ToSafeAlias(_shortStringHelper); const bool published = false; // always false for media - var xml = SerializeContentBase(media, media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), nodeName, published); + string urlValue = media.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); + XElement xml = SerializeContentBase(media, urlValue, nodeName, published); + xml.Add(new XAttribute("nodeType", media.ContentType.Id)); xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias)); @@ -123,6 +128,8 @@ namespace Umbraco.Cms.Core.Services.Implement //xml.Add(new XAttribute("template", 0)); // no template for media + onMediaItemSerialized?.Invoke(media, xml); + if (withDescendants) { const int pageSize = 500; @@ -131,7 +138,7 @@ namespace Umbraco.Cms.Core.Services.Implement while (page * pageSize < total) { var children = _mediaService.GetPagedChildren(media.Id, page++, pageSize, out total); - SerializeChildren(children, xml); + SerializeChildren(children, xml, onMediaItemSerialized); } } @@ -257,13 +264,18 @@ namespace Umbraco.Cms.Core.Services.Implement return xml; } - public XElement Serialize(Stylesheet stylesheet) + public XElement Serialize(IStylesheet stylesheet, bool includeProperties) { var xml = new XElement("Stylesheet", new XElement("Name", stylesheet.Alias), new XElement("FileName", stylesheet.Path), new XElement("Content", new XCData(stylesheet.Content))); + if (!includeProperties) + { + return xml; + } + var props = new XElement("Properties"); xml.Add(props); @@ -619,12 +631,12 @@ namespace Umbraco.Cms.Core.Services.Implement } // exports an IMedia item descendants. - private void SerializeChildren(IEnumerable children, XElement xml) + private void SerializeChildren(IEnumerable children, XElement xml, Action onMediaItemSerialized) { foreach (var child in children) { // add the child xml - var childXml = Serialize(child); + var childXml = Serialize(child, onMediaItemSerialized: onMediaItemSerialized); xml.Add(childXml); const int pageSize = 500; @@ -634,7 +646,7 @@ namespace Umbraco.Cms.Core.Services.Implement { var grandChildren = _mediaService.GetPagedChildren(child.Id, page++, pageSize, out total); // recurse - SerializeChildren(grandChildren, childXml); + SerializeChildren(grandChildren, childXml, onMediaItemSerialized); } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs index 6d7d5b4490..ed7bdc9c29 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/FileService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/FileService.cs @@ -56,20 +56,20 @@ namespace Umbraco.Cms.Core.Services.Implement #region Stylesheets /// - public IEnumerable GetStylesheets(params string[] names) + public IEnumerable GetStylesheets(params string[] paths) { using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _stylesheetRepository.GetMany(names); + return _stylesheetRepository.GetMany(paths); } } /// - public IStylesheet GetStylesheetByName(string name) + public IStylesheet GetStylesheet(string path) { using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _stylesheetRepository.Get(name); + return _stylesheetRepository.Get(path); } } @@ -177,7 +177,16 @@ namespace Umbraco.Cms.Core.Services.Implement #region Scripts /// - public IScript GetScriptByName(string name) + public IEnumerable GetScripts(params string[] names) + { + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _scriptRepository.GetMany(names); + } + } + + /// + public IScript GetScript(string name) { using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { @@ -647,6 +656,14 @@ namespace Umbraco.Cms.Core.Services.Implement } } + public IEnumerable GetPartialViews(params string[] names) + { + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _partialViewRepository.GetMany(names); + } + } + public IPartialView GetPartialView(string path) { using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs index 34d1c2a5ce..0239ad8e60 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaService.cs @@ -653,15 +653,14 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// The to save /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - public Attempt Save(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public Attempt Save(IMedia media, int userId = Cms.Core.Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new MediaSavingNotification(media, eventMessages); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return OperationResult.Attempt.Cancel(eventMessages); @@ -685,10 +684,8 @@ namespace Umbraco.Cms.Core.Services.Implement } _mediaRepository.Save(media); - if (raiseEvents) - { - scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); + // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages)); Audit(AuditType.Save, userId, media.Id); @@ -703,8 +700,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Collection of to save /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - public Attempt Save(IEnumerable medias, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public Attempt Save(IEnumerable medias, int userId = Cms.Core.Constants.Security.SuperUserId) { EventMessages messages = EventMessagesFactory.Get(); IMedia[] mediasA = medias.ToArray(); @@ -712,7 +708,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new MediaSavingNotification(mediasA, messages); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return OperationResult.Attempt.Cancel(messages); @@ -731,10 +727,8 @@ namespace Umbraco.Cms.Core.Services.Implement _mediaRepository.Save(media); } - if (raiseEvents) - { - scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); + // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(treeChanges, messages)); Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Bulk save media"); @@ -1093,9 +1087,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// /// - /// /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId, bool raiseEvents = true) + public bool Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId) { IMedia[] itemsA = items.ToArray(); if (itemsA.Length == 0) @@ -1108,7 +1101,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new MediaSavingNotification(itemsA, messages); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return false; @@ -1135,10 +1128,8 @@ namespace Umbraco.Cms.Core.Services.Implement _mediaRepository.Save(media); } - if (raiseEvents) - { - scope.Notifications.Publish(new MediaSavedNotification(itemsA, messages).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MediaSavedNotification(itemsA, messages).WithStateFrom(savingNotification)); + // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, messages)); Audit(AuditType.Sort, userId, 0); diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs index 096ff164a0..9d68415ad5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberGroupService.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Core.Services.Implement } } - public void Save(IMemberGroup memberGroup, bool raiseEvents = true) + public void Save(IMemberGroup memberGroup) { if (string.IsNullOrWhiteSpace(memberGroup.Name)) { @@ -76,7 +76,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var savingNotification = new MemberGroupSavingNotification(memberGroup, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -85,10 +85,7 @@ namespace Umbraco.Cms.Core.Services.Implement _memberGroupRepository.Save(memberGroup); scope.Complete(); - if (raiseEvents) - { - scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, evtMsgs).WithStateFrom(savingNotification)); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 94476ff1e1..e4633714ff 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -155,9 +155,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// Is the member approved /// IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) - { - return CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias); - } + => CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias); /// /// Creates and persists a new @@ -170,29 +168,19 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) - { - return CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias, isApproved); - } + => CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias, isApproved); public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias) - { - return CreateMemberWithIdentity(username, email, username, "", memberTypeAlias); - } + => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias); public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias, bool isApproved) - { - return CreateMemberWithIdentity(username, email, username, "", memberTypeAlias, isApproved); - } + => CreateMemberWithIdentity(username, email, username, "", memberTypeAlias, isApproved); public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) - { - return CreateMemberWithIdentity(username, email, name, "", memberTypeAlias); - } + => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias); public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias, bool isApproved) - { - return CreateMemberWithIdentity(username, email, name, "", memberTypeAlias, isApproved); - } + => CreateMemberWithIdentity(username, email, name, "", memberTypeAlias, isApproved); /// /// Creates and persists a Member @@ -207,26 +195,29 @@ namespace Umbraco.Cms.Core.Services.Implement /// public IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, string memberTypeAlias, bool isApproved = true) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { // locking the member tree secures member types too scope.WriteLock(Constants.Locks.MemberTree); - var memberType = GetMemberType(scope, memberTypeAlias); // + locks // + locks + IMemberType memberType = GetMemberType(scope, memberTypeAlias); // + locks // + locks if (memberType == null) + { throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); // causes rollback // causes rollback + } var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1); Save(member); + + scope.Complete(); + return member; } } public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, username, "", memberType); - } + => CreateMemberWithIdentity(username, email, username, "", memberType); /// /// Creates and persists a Member @@ -238,14 +229,10 @@ namespace Umbraco.Cms.Core.Services.Implement /// MemberType the Member should be based on /// public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType, bool isApproved) - { - return CreateMemberWithIdentity(username, email, username, "", memberType, isApproved); - } + => CreateMemberWithIdentity(username, email, username, "", memberType, isApproved); public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, name, "", memberType); - } + => CreateMemberWithIdentity(username, email, name, "", memberType); /// /// Creates and persists a Member @@ -258,9 +245,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// MemberType the Member should be based on /// public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType, bool isApproved) - { - return CreateMemberWithIdentity(username, email, name, "", memberType, isApproved); - } + => CreateMemberWithIdentity(username, email, name, "", memberType, isApproved); /// /// Creates and persists a Member @@ -286,10 +271,16 @@ namespace Umbraco.Cms.Core.Services.Implement var vrfy = GetMemberType(scope, memberType.Alias); // + locks if (vrfy == null || vrfy.Id != memberType.Id) + { throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback + } + var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved, -1); Save(member); + + scope.Complete(); + return member; } } @@ -767,7 +758,7 @@ namespace Umbraco.Cms.Core.Services.Implement } /// - public void Save(IMember member, bool raiseEvents = true) + public void Save(IMember member) { // trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); @@ -778,7 +769,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { var savingNotification = new MemberSavingNotification(member, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -793,10 +784,7 @@ namespace Umbraco.Cms.Core.Services.Implement _memberRepository.Save(member); - if (raiseEvents) - { - scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); Audit(AuditType.Save, 0, member.Id); @@ -805,7 +793,7 @@ namespace Umbraco.Cms.Core.Services.Implement } /// - public void Save(IEnumerable members, bool raiseEvents = true) + public void Save(IEnumerable members) { var membersA = members.ToArray(); @@ -814,7 +802,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var savingNotification = new MemberSavingNotification(membersA, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -831,10 +819,8 @@ namespace Umbraco.Cms.Core.Services.Implement _memberRepository.Save(member); } - if (raiseEvents) - { - scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new MemberSavedNotification(membersA, evtMsgs).WithStateFrom(savingNotification)); + Audit(AuditType.Save, 0, -1, "Save multiple Members"); scope.Complete(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 743b4816da..0500c18bdd 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data.Common; using System.Globalization; @@ -273,16 +273,14 @@ namespace Umbraco.Cms.Core.Services.Implement /// Saves an /// /// to Save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IUser entity, bool raiseEvents = true) + public void Save(IUser entity) { var evtMsgs = EventMessagesFactory.Get(); using (var scope = ScopeProvider.CreateScope()) { var savingNotification = new UserSavingNotification(entity, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -297,10 +295,7 @@ namespace Umbraco.Cms.Core.Services.Implement try { _userRepository.Save(entity); - if (raiseEvents) - { - scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new UserSavedNotification(entity, evtMsgs).WithStateFrom(savingNotification)); scope.Complete(); } @@ -321,9 +316,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// Saves a list of objects /// /// to save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IEnumerable entities, bool raiseEvents = true) + public void Save(IEnumerable entities) { var evtMsgs = EventMessagesFactory.Get(); @@ -332,7 +325,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var savingNotification = new UserSavingNotification(entitiesA, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -350,10 +343,7 @@ namespace Umbraco.Cms.Core.Services.Implement } - if (raiseEvents) - { - scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); - } + scope.Notifications.Publish(new UserSavedNotification(entitiesA, evtMsgs).WithStateFrom(savingNotification)); //commit the whole lot in one go scope.Complete(); @@ -816,9 +806,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// If null than no changes are made to the users who are assigned to this group, however if a value is passed in /// than all users will be removed from this group and only these users will be added /// - /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events - public void Save(IUserGroup userGroup, int[] userIds = null, bool raiseEvents = true) + public void Save(IUserGroup userGroup, int[] userIds = null) { var evtMsgs = EventMessagesFactory.Get(); @@ -843,7 +832,7 @@ namespace Umbraco.Cms.Core.Services.Implement // this is the default/expected notification for the IUserGroup entity being saved var savingNotification = new UserGroupSavingNotification(userGroup, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification)) + if (scope.Notifications.PublishCancelable(savingNotification)) { scope.Complete(); return; @@ -851,7 +840,7 @@ namespace Umbraco.Cms.Core.Services.Implement // this is an additional notification for special auditing var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, evtMsgs); - if (raiseEvents && scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) + if (scope.Notifications.PublishCancelable(savingUserGroupWithUsersNotification)) { scope.Complete(); return; @@ -859,11 +848,8 @@ namespace Umbraco.Cms.Core.Services.Implement _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, userIds); - if (raiseEvents) - { - scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); - } + scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, evtMsgs).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new UserGroupWithUsersSavedNotification(userGroupWithUsers, evtMsgs).WithStateFrom(savingUserGroupWithUsersNotification)); scope.Complete(); } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index d759c8da9b..712323656d 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -111,10 +111,6 @@ - - - - diff --git a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs index 74668d3090..00cbdce532 100644 --- a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs +++ b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs @@ -21,6 +21,8 @@ namespace Umbraco.Cms.Infrastructure.WebAssets public const string UmbracoInitCssBundleName = "umbraco-backoffice-init-css"; public const string UmbracoCoreJsBundleName = "umbraco-backoffice-js"; public const string UmbracoExtensionsJsBundleName = "umbraco-backoffice-extensions-js"; + public const string UmbracoNonOptimizedPackageJsBundleName = "umbraco-backoffice-non-optimized-js"; + public const string UmbracoNonOptimizedPackageCssBundleName = "umbraco-backoffice-non-optimized-css"; public const string UmbracoTinyMceJsBundleName = "umbraco-tinymce-js"; public const string UmbracoUpgradeCssBundleName = "umbraco-authorize-upgrade-css"; @@ -51,47 +53,132 @@ namespace Umbraco.Cms.Infrastructure.WebAssets { // Create bundles - _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, false, + _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths("lib/bootstrap-social/bootstrap-social.css", "assets/css/umbraco.min.css", "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, false, + _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths("assets/css/umbraco.min.css", "lib/bootstrap-social/bootstrap-social.css", "lib/font-awesome/css/font-awesome.min.css")); - _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, false, + _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths("assets/css/canvasdesigner.min.css")); - _runtimeMinifier.CreateJsBundle(UmbracoPreviewJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoPreviewJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForPreview())); - _runtimeMinifier.CreateJsBundle(UmbracoTinyMceJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoTinyMceJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForTinyMce())); - _runtimeMinifier.CreateJsBundle(UmbracoCoreJsBundleName, false, + _runtimeMinifier.CreateJsBundle(UmbracoCoreJsBundleName, + BundlingOptions.NotOptimizedAndComposite, FormatPaths(GetScriptsForBackOfficeCore())); + + // get the property editor assets var propertyEditorAssets = ScanPropertyEditors() .GroupBy(x => x.AssetType) .ToDictionary(x => x.Key, x => x.Select(c => c.FilePath)); + // get the back office custom assets var customAssets = _customBackOfficeAssetsCollection.GroupBy(x => x.DependencyType).ToDictionary(x => x.Key, x => x.Select(c => c.FilePath)); - var jsAssets = (customAssets.TryGetValue(AssetType.Javascript, out var customScripts) ? customScripts : Enumerable.Empty()) - .Union(propertyEditorAssets.TryGetValue(AssetType.Javascript, out var scripts) ? scripts : Enumerable.Empty()); + // This bundle includes all scripts from property editor assets, + // custom back office assets, and any scripts found in package manifests + // that have the default bundle options. + + IEnumerable jsAssets = (customAssets.TryGetValue(AssetType.Javascript, out IEnumerable customScripts) ? customScripts : Enumerable.Empty()) + .Union(propertyEditorAssets.TryGetValue(AssetType.Javascript, out IEnumerable scripts) ? scripts : Enumerable.Empty()); + _runtimeMinifier.CreateJsBundle( - UmbracoExtensionsJsBundleName, true, + UmbracoExtensionsJsBundleName, + BundlingOptions.OptimizedAndComposite, FormatPaths( GetScriptsForBackOfficeExtensions(jsAssets))); - var cssAssets = (customAssets.TryGetValue(AssetType.Css, out var customStyles) ? customStyles : Enumerable.Empty()) - .Union(propertyEditorAssets.TryGetValue(AssetType.Css, out var styles) ? styles : Enumerable.Empty()); + // Create a bundle per package manifest that is declaring an Independent bundle type + RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest.Scripts, AssetType.Javascript); + + // Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option + RegisterPackageBundlesForNoneOption(_parser.CombinedManifest.Scripts, UmbracoNonOptimizedPackageJsBundleName); + + // This bundle includes all CSS from property editor assets, + // custom back office assets, and any CSS found in package manifests + // that have the default bundle options. + + IEnumerable cssAssets = (customAssets.TryGetValue(AssetType.Css, out IEnumerable customStyles) ? customStyles : Enumerable.Empty()) + .Union(propertyEditorAssets.TryGetValue(AssetType.Css, out IEnumerable styles) ? styles : Enumerable.Empty()); + _runtimeMinifier.CreateCssBundle( - UmbracoCssBundleName, true, + UmbracoCssBundleName, + BundlingOptions.OptimizedAndComposite, FormatPaths( GetStylesheetsForBackOffice(cssAssets))); + + // Create a bundle per package manifest that is declaring an Independent bundle type + RegisterPackageBundlesForIndependentOptions(_parser.CombinedManifest.Stylesheets, AssetType.Css); + + // Create a single non-optimized (no file processing) bundle for all manifests declaring None as a bundle option + RegisterPackageBundlesForNoneOption(_parser.CombinedManifest.Stylesheets, UmbracoNonOptimizedPackageCssBundleName); + } + + public static string GetIndependentPackageBundleName(ManifestAssets manifestAssets, AssetType assetType) + => $"{manifestAssets.PackageName.ToLowerInvariant()}-{(assetType == AssetType.Css ? "css" : "js")}"; + + private void RegisterPackageBundlesForNoneOption( + IReadOnlyDictionary> combinedPackageManifestAssets, + string bundleName) + { + var assets = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + // Create a bundle per package manifest that is declaring the matching BundleOptions + if (combinedPackageManifestAssets.TryGetValue(BundleOptions.None, out IReadOnlyList manifestAssetList)) + { + foreach(var asset in manifestAssetList.SelectMany(x => x.Assets)) + { + assets.Add(asset); + } + } + + _runtimeMinifier.CreateJsBundle( + bundleName, + // no optimization, no composite files, just render individual files + BundlingOptions.NotOptimizedNotComposite, + FormatPaths(assets.ToArray())); + } + + private void RegisterPackageBundlesForIndependentOptions( + IReadOnlyDictionary> combinedPackageManifestAssets, + AssetType assetType) + { + // Create a bundle per package manifest that is declaring the matching BundleOptions + if (combinedPackageManifestAssets.TryGetValue(BundleOptions.Independent, out IReadOnlyList manifestAssetList)) + { + foreach (ManifestAssets manifestAssets in manifestAssetList) + { + string bundleName = GetIndependentPackageBundleName(manifestAssets, assetType); + string[] filePaths = FormatPaths(manifestAssets.Assets.ToArray()); + + switch (assetType) + { + case AssetType.Javascript: + _runtimeMinifier.CreateJsBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths); + break; + case AssetType.Css: + _runtimeMinifier.CreateCssBundle(bundleName, BundlingOptions.OptimizedAndComposite, filePaths); + break; + default: + throw new IndexOutOfRangeException(); + } + } + } } /// @@ -100,10 +187,15 @@ namespace Umbraco.Cms.Infrastructure.WebAssets /// private string[] GetScriptsForBackOfficeExtensions(IEnumerable propertyEditorScripts) { - var scripts = new HashSet(); - foreach (string script in _parser.Manifest.Scripts) + var scripts = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + // only include scripts with the default bundle options here + if (_parser.CombinedManifest.Scripts.TryGetValue(BundleOptions.Default, out IReadOnlyList manifestAssets)) { - scripts.Add(script); + foreach (string script in manifestAssets.SelectMany(x => x.Assets)) + { + scripts.Add(script); + } } foreach (string script in propertyEditorScripts) @@ -130,11 +222,15 @@ namespace Umbraco.Cms.Infrastructure.WebAssets /// private string[] GetStylesheetsForBackOffice(IEnumerable propertyEditorStyles) { - var stylesheets = new HashSet(); + var stylesheets = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string script in _parser.Manifest.Stylesheets) + // only include css with the default bundle options here + if (_parser.CombinedManifest.Stylesheets.TryGetValue(BundleOptions.Default, out IReadOnlyList manifestAssets)) { - stylesheets.Add(script); + foreach (string script in manifestAssets.SelectMany(x => x.Assets)) + { + stylesheets.Add(script); + } } foreach (string stylesheet in propertyEditorStyles) diff --git a/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs b/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs deleted file mode 100644 index f33d48d8fd..0000000000 --- a/src/Umbraco.Infrastructure/WebAssets/RuntimeMinifierExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Core.WebAssets; -using Umbraco.Cms.Infrastructure.WebAssets; - -namespace Umbraco.Extensions -{ - public static class RuntimeMinifierExtensions - { - /// - /// Returns the JavaScript to load the back office's assets - /// - /// - public static async Task GetScriptForLoadingBackOfficeAsync(this IRuntimeMinifier minifier, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) - { - var coreScripts = await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoCoreJsBundleName); - var extensionsScripts = await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoExtensionsJsBundleName); - var result = BackOfficeJavaScriptInitializer.GetJavascriptInitialization(coreScripts.Union(extensionsScripts), "umbraco", globalSettings, hostingEnvironment); - result += await GetStylesheetInitializationAsync(minifier); - - return result; - } - - /// - /// Gets the back office css bundle paths and formats a JS call to lazy load them - /// - private static async Task GetStylesheetInitializationAsync(IRuntimeMinifier minifier) - { - var files = await minifier.GetCssAssetPathsAsync(BackOfficeWebAssets.UmbracoCssBundleName); - var sb = new StringBuilder(); - foreach (var file in files) - sb.AppendFormat("{0}LazyLoad.css('{1}');", Environment.NewLine, file); - return sb.ToString(); - } - - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs index 667c5262d5..e6ed41548d 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs @@ -17,13 +17,12 @@ namespace Umbraco.Cms.Persistence.SqlCe public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + if (!records.Any()) return 0; var pocoData = database.PocoDataFactory.ForType(typeof(T)); if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - return BulkInsertRecordsSqlCe(database, pocoData, recordsA); + return BulkInsertRecordsSqlCe(database, pocoData, records.ToArray()); } diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs index 0e53561929..3646218fce 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Persistence.SqlCe @@ -6,6 +6,6 @@ namespace Umbraco.Cms.Persistence.SqlCe public class SqlCeSpecificMapperFactory : IProviderSpecificMapperFactory { public string ProviderName => Constants.DatabaseProviders.SqlCe; - public NPocoMapperCollection Mappers => new NPocoMapperCollection(new[] {new SqlCeImageMapper()}); + public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] {new SqlCeImageMapper()}); } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs index 65412f10d2..cbc40a9630 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs @@ -14,12 +14,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Deserialize the data into a /// - ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published); /// - /// Serializes the + /// Serializes the /// - ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published); } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs index 8fc953f353..9f1bfd39e2 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource DateFormatString = "o" }; private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 66c01cf1dc..7caf6eac7d 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -39,7 +39,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource _options = defaultOptions .WithResolver(resolver) - .WithCompression(MessagePackCompression.Lz4BlockArray); + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData); } public string ToJson(byte[] bin) @@ -48,12 +49,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource return json; } - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (byteData != null) { var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); - Expand(content, cacheModel); + Expand(content, cacheModel, published); return cacheModel; } else if (stringData != null) @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); var cacheModel = MessagePackSerializer.Deserialize(bin, _options); - Expand(content, cacheModel); + Expand(content, cacheModel, published); return cacheModel; } else @@ -70,9 +71,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { - Compress(content, model); + Compress(content, model, published); var bytes = MessagePackSerializer.Serialize(model, _options); return new ContentCacheDataSerializationResult(null, bytes); } @@ -80,7 +81,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Used during serialization to compress properties /// + /// /// + /// /// /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed /// but this will go a step further and double compress property data so that it is stored in the nucache file @@ -88,11 +91,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant /// memory savings but could also affect performance of first rendering pages while decompression occurs. /// - private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { foreach(var propertyAliasToData in model.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { @@ -105,12 +108,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Used during deserialization to map the property data as lazy or expand the value /// + /// /// - private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published) { foreach (var propertyAliasToData in nestedData.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published)) { foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index f93cd71ad2..732d51b3b1 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; -using Umbraco.Core.PropertyEditors; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { @@ -14,7 +13,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource private readonly IMemberTypeService _memberTypeService; private readonly PropertyEditorCollection _propertyEditors; private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); + private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string, bool), bool>(); public MsgPackContentNestedDataSerializerFactory( IContentTypeService contentTypeService, diff --git a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs index 7ef655b2a8..c52c1271ad 100644 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -68,7 +68,20 @@ namespace Umbraco.Extensions throw new IndexOutOfRangeException(); } }); - builder.Services.AddSingleton(); + + builder.Services.AddSingleton(s => + { + IOptions options = s.GetRequiredService>(); + + if (options.Value.NuCacheSerializerType == NuCacheSerializerType.MessagePack && + options.Value.UnPublishedContentCompression) + { + return new UnPublishedContentPropertyCacheCompressionOptions(); + } + + return new NoopPropertyCacheCompressionOptions(); + }); + builder.Services.AddSingleton(s => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); // add the NuCache health check (hidden from type finder) diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index a141872957..2919edb8c3 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -415,7 +415,7 @@ AND cmsContentNu.nodeId IS NULL UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders) }; - var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData); + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published); var dto = new ContentNuDto { @@ -817,12 +817,13 @@ AND cmsContentNu.nodeId IS NULL } else { - var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = false; + var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); d = new ContentData { Name = dto.EditName, - Published = false, + Published = published, TemplateId = dto.EditTemplateId, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, @@ -847,13 +848,14 @@ AND cmsContentNu.nodeId IS NULL } else { - var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw); + bool published = true; + var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published); p = new ContentData { Name = dto.PubName, UrlSegment = deserializedContent.UrlSegment, - Published = true, + Published = published, TemplateId = dto.PubTemplateId, VersionId = dto.VersionId, VersionDate = dto.PubVersionDate, @@ -883,12 +885,13 @@ AND cmsContentNu.nodeId IS NULL if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = true; + var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); var p = new ContentData { Name = dto.EditName, - Published = true, + Published = published, TemplateId = -1, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, diff --git a/src/Umbraco.TestData/Configuration/TestDataSettings.cs b/src/Umbraco.TestData/Configuration/TestDataSettings.cs new file mode 100644 index 0000000000..78084f726a --- /dev/null +++ b/src/Umbraco.TestData/Configuration/TestDataSettings.cs @@ -0,0 +1,16 @@ +namespace Umbraco.TestData.Configuration +{ + public class TestDataSettings + { + /// + /// Gets or sets a value indicating whether the test data generation is enabled. + /// + public bool Enabled { get; set; } = false; + + /// + /// Gets or sets a value indicating whether persisted local database cache files for content and media are disabled. + /// + /// The URL path. + public bool IgnoreLocalDb { get; set; } = false; + } +} diff --git a/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..7e3e2a70b1 --- /dev/null +++ b/src/Umbraco.TestData/Extensions/UmbracoBuilderExtensions.cs @@ -0,0 +1,54 @@ +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.TestData.Configuration; + +namespace Umbraco.TestData.Extensions +{ + public static class UmbracoBuilderExtensions + { + public static IUmbracoBuilder AddUmbracoTestData(this IUmbracoBuilder builder) + { + if (builder.Services.Any(x => x.ServiceType == typeof(LoadTestController))) + { + // We assume the test data project is composed if any implementations of LoadTestController exist. + return builder; + } + + IConfigurationSection testDataSection = builder.Config.GetSection("Umbraco:CMS:TestData"); + TestDataSettings config = testDataSection.Get(); + if (config == null || config.Enabled == false) + { + return builder; + } + + builder.Services.Configure(testDataSection); + + if (config.IgnoreLocalDb) + { + builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions + { + IgnoreLocalDb = true + }); + } + + builder.Services.Configure(options => + options.AddFilter(new UmbracoPipelineFilter(nameof(LoadTestController)) + { + Endpoints = app => app.UseEndpoints(endpoints => + endpoints.MapControllerRoute( + "LoadTest", + "/LoadTest/{action}", + new { controller = "LoadTest", Action = "Index" })) + })); + + builder.Services.AddScoped(typeof(LoadTestController)); + + return builder; + } + } +} diff --git a/src/Umbraco.TestData/LoadTestComponent.cs b/src/Umbraco.TestData/LoadTestComponent.cs deleted file mode 100644 index cfd923cd07..0000000000 --- a/src/Umbraco.TestData/LoadTestComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Web.Mvc; -using System.Web.Routing; -using System.Configuration; -using Umbraco.Cms.Core.Composing; - -// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting - -namespace Umbraco.TestData -{ - public class LoadTestComponent : IComponent - { - public void Initialize() - { - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - - - RouteTable.Routes.MapRoute( - name: "LoadTest", - url: "LoadTest/{action}", - defaults: new - { - controller = "LoadTest", - action = "Index" - }, - namespaces: new[] { "Umbraco.TestData" } - ); - } - - public void Terminate() - { - } - } -} diff --git a/src/Umbraco.TestData/LoadTestComposer.cs b/src/Umbraco.TestData/LoadTestComposer.cs index e5b16e5ab1..8d66c4965d 100644 --- a/src/Umbraco.TestData/LoadTestComposer.cs +++ b/src/Umbraco.TestData/LoadTestComposer.cs @@ -1,31 +1,13 @@ -using System.Configuration; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.TestData.Extensions; // see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting namespace Umbraco.TestData { - public class LoadTestComposer : ComponentComposer, IUserComposer + public class LoadTestComposer : IUserComposer { - public override void Compose(IUmbracoBuilder builder) - { - base.Compose(builder); - - if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") - return; - - builder.Services.AddScoped(typeof(LoadTestController), typeof(LoadTestController)); - - if (ConfigurationManager.AppSettings["Umbraco.TestData.IgnoreLocalDb"] == "true") - { - builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions - { - IgnoreLocalDb = true - }); - } - } + public void Compose(IUmbracoBuilder builder) => builder.AddUmbracoTestData(); } } diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs index e1494fbdab..3033d2febb 100644 --- a/src/Umbraco.TestData/LoadTestController.cs +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -1,21 +1,15 @@ -using System; -using System.Configuration; +using System; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Text; using System.Threading; -using System.Web; -using System.Web.Hosting; -using System.Web.Mvc; -using System.Web.Routing; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.DependencyInjection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using System.IO; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Hosting; // see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting @@ -23,27 +17,17 @@ namespace Umbraco.TestData { public class LoadTestController : Controller { - public LoadTestController( - ServiceContext serviceContext, - IShortStringHelper shortStringHelper, - IHostingEnvironment hostingEnvironment) - { - _serviceContext = serviceContext; - _shortStringHelper = shortStringHelper; - _hostingEnvironment = hostingEnvironment; - } + private static readonly Random s_random = new Random(); + private static readonly object s_locko = new object(); - private static readonly Random _random = new Random(); - private static readonly object _locko = new object(); + private static volatile int s_containerId = -1; - private static volatile int _containerId = -1; + private const string ContainerAlias = "LoadTestContainer"; + private const string ContentAlias = "LoadTestContent"; + private const int TextboxDefinitionId = -88; + private const int MaxCreate = 1000; - private const string _containerAlias = "LoadTestContainer"; - private const string _contentAlias = "LoadTestContent"; - private const int _textboxDefinitionId = -88; - private const int _maxCreate = 1000; - - private static readonly string HeadHtml = @" + private static readonly string s_headHtml = @" LoadTest - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Ailosod cyfrinair wedi dymuno -

-

- Eich enw defnyddiwr ar gyfer swyddfa gefn Umbraco yw: %0% -

-

- - - - - - -
- - Cliciwch y ddolen yma er mwyn ailosod eich cyfrinair - -
-

-

Os na allwch glicio ar y ddolen yma, copïwch a gludwch y URL i mewn i'ch porwr:

- - - - -
- - %1% - -
-

-
-
-


-
-
- - - ]]> - - - - Dashfwrdd - Adrannau - Cynnwys - - - Dewis tudalen uwchben... - %0% wedi copïo i %1% - Dewiswch ble ddylai'r ddogfen %0% gael ei gopïo i isod - %0% wedi ei symud i %1% - Dewiswch ble ddylai'r ddogfen %0% gael ei symud i isod - wedi ei ddewis fel gwraidd eich cynnwys newydd, cliciwch 'iawn' isod. - Dim nod wedi'i ddewis eto, dewiswch nod yn y rhestr uchod yn gyntaf cyn clicio 'iawn' - Nid yw'r nod bresennol yn cael ei ganiatáu o dan y nod ddewiswyd oherwydd ei fath - Ni all y nod bresennol gael ei symud i un o'i is-dudalennau - Ni all y nod bresennol fodoli ar y gwraidd - Nid yw'r gweithred wedi'i ganiatáu gan nad oes gennych ddigon o hawliau ar gyfer 1 neu fwy o ddogfennau blentyn. - Perthnasu eitemau wedi'u copïo at y rhai gwreiddiol - - - %0%]]> - Gosodiad hysbysiadau wedi cadw am - - - - Mae'r ieithoedd canlynol wedi'u haddasu %0% - - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Helo %0%, -

-

- Mae hyn yn ebost awtomatig i'ch hysbysu fod y dasg '%1%' wedi'i berfformio ar y dudalen '%2%' gan y defnyddiwr '%3%' -

- - - - - - -
- -
- GOLYGU
-
-

-

Crynodeb diweddariad:

- - %6% -
-

-

- Mwynhewch eich diwrnod!

- Hwyl fawr oddi wrth y robot Umbraco -

-
-
-


-
-
- - - ]]> -
- - Mae'r ieithoedd canlynol wedi'u haddasu:

- %0% - ]]> -
- [%0%] Hysbysiad am %1% wedi perfformio am %2% - Hysbysiadau - - - Gweithredoedd - Creu - Creu pecyn - - - Pori a darganfod y pecyn. Fel arfer, mae gen becynnau Umbraco estyniadau ".umb" neu ".zip". - ]]> - - Bydd hwn yn dileu'r pecyn - Gollwng i lanlwytho - Cynhwyswch yr holl nodau plentyn - neu cliciwch yma i ddewis ffeil pecyn - Lanlwytho pecyn - Gosod pecyn leol wrth ddewis o'ch peiriant. Dylwch ddim ond osod pecynnau o ffynonellau yr ydych yn adnabod a bod gennych hyder ynddynt - Lanlwytho pecyn arall - Canslo a lanlwytho pecyn arall - Trwydded - Rydw i'n derbyn - termau defnydd - Llwybr i'r ffeil - Llwybr llwyr i'r ffeil (ie: /bin/umbraco.bin) - Wedi'i osod - Gosod yn lleol - Gosod pecyn - Gorffen - Pecynnau wedi'u gosod - Nid oes gennych unrhyw becynnau wedi'u gosod - 'Pecynnau' yng nghornel dop, dde eich sgrîn]]> - Nid oes gan y pecyn hwn unrhyw olwg cyfluniad - Nid oes unrhyw becynnau wedi'u creu eto - URL y Awdur - Cynnwys y Pecyn - Ffeiliau y Pecyn - URL Eicon - Gosod pecyn - Trwydded - URL Trwydded - Priodweddau Pecyn - Chwilio am becynnau - Canlyniadau ar gyfer - Ni allwn ddarganfod unrhyw beth ar gyfer - Ceisiwch chwilio am becyn arall neu porwch drwy'r categorïau - Poblogaidd - Pecynnau newydd - yn cynnwys - pwyntiau karma - Gwybodaeth - Perchennog - Cyfranwyr - Creuwyd - Fersiwn bresennol - Fersiwn .NET - Lawrlwythiadau - Hoffi - Cydweddoldeb - Mae'r pecyn yma yn gydnaws â'r fersiynau canlynol o Umbraco, fel y mae aelodau'r gymued yn adrodd yn ôl. Ni all warantu cydweddoldeb cyflawn ar gyfer fersiynau sydd wedi'u hadrodd o dan 100% - Ffynonellau allanol - Awdur - Arddangosiad - Dogfennaeth - Meta ddata pecynnau - Enw pecyn - Does dim eitemau o fewn y pecyn - -
- Gallwch ddileu hyn yn ddiogel o'r system wrth glicio "dadosod pecyn" isod.]]> -
- Dim uwchraddiadau ar gael - Dewisiadau pecyn - Readme pecyn - Ystorfa pecyn - Cadarnhau dadosod pecyn - Pecyn wedi dadosod - Cafodd y pecyn ei ddadosod yn llwyddiannus - Dadosod pecyn - - - Rhybudd: bydd unrhyw ddogfennau, cyfrwng ayyb sy'n dibynnu ar yr eitemau yr ydych am ddileu yn torri, a gall arwain at system ansefydlog, - felly dadosodwch gyda gofal. Os oes unrhyw amheuaeth, cysylltwch ag awdur y pecyn.]]> - - Lawrlwytho diweddariad o'r ystorfa - Uwchraddio pecyn - Cyfarwyddiadau uwchraddio - Mae yna uwchraddiad ar gael ar gyfer y pecyn yma. Gallwch lawrlwytho'n uniongyrchol o'r ystorfa pecynnau Umbraco. - Fersiwn pecyn - Uwchraddio o ferswin - Hanes ferswin pecyn - Gweld gwefan pecyn - Pecyn wedi'i osod eisoes - Ni all y pecyn yma gael ei osod, mae angen fersiwn Umrbaco o leiaf - Dadosod... - Lawrlwytho... - Mewnforio... - Gosod... - Ailgychwyn, arhoswch... - Wedi cwblhau, bydd eich porwr yn adnewyddu, arhoswch... - Cliciwch 'Cwblhau' i orffen y gosodiad ac adnewyddu'r dudalen. - Lanlwytho pecyn... - - - Gludo gyda fformatio llawn (Heb ei argymell) - Mae'r testun yr ydych yn ceisio gludo yn cynnwys nodauneu fformatio arbennig. Gall hyn gael ei achosi gan ludo testun o Microsoft Word. Gall Umbraco ddileu nodau neu fformatio arbennig yn awtomatig, fel bod y cynnwys sy'n cael ei ludo yn fwy addas ar gyfer y we. - Gludo fel testun crai heb unrhyw fformatio - Gludo, ond dileu fformatio (Wedi'i hargymell) - - - Amddiffyniad yn seiliedig grŵp - Os ydych chi am ganiatáu mynediad i bob aelod o grwpiau aelodau penodol - Mae angen i chi greu grŵp aelod cyn y gallwch ddefnyddio dilysiad grŵp - Amddiffyn ar sail rôl - Os hoffwch reoli cyrchiad i'r dudalen wrth ddefnyddio dilysu ar sail rôl, gan ddefnyddio grwpiau aelodaeth Umbraco. - Mae angen i chi greu grŵp aeloadeth cyn i chi allu defnyddio dilysu ar sail rôl - Tudalen Wall - Wedi'i ddefnyddio pan mae defnyddwyr wedi mewngofnodi, ond nid oes ganddynt hawliau - Dewiswch sut i gyfyngu hawliau at y dudalen yma - %0% wedi amddiffyn rwan - Amddiffyniad wedi dileu o %0% - Tudalen Mewngofnodi - Dewiswch y dudalen sy'n cynnwys y ffurflen mewngofnodi - Dileu Amddiffyniad - Dewiswch y tudalennau sy'n cynnwys ffurflenni mewngofnodi a negeseuon gwall - Dewiswch y rolau sydd a hawliau i'r dudlaen yma - Gosodwch yr enw defnyddiwr a chyfrinair ar gyfer y dudalen yma - Amddiffyniad defnyddiwr unigol - Os hoffwch osod amddifyniad syml wrth ddefnyddio enw defnyddiwr a chyfrinair sengl - %0%?]]> - %0%]]> - %0%]]> - Amddiffyn aelodau penodol - Os ydych am ganiatáu mynediad i aelodau penodol - - - Caniatâd annigonol gan ddefnyddwyr i gyhoeddi'r holl ddogfennau disgynyddion - - - - - - - - - - - - - - - - - - - - Methodd y dilysiad ar gyfer yr iaith ofynnol '%0%'. Roedd yr iaith wedi cael ei arbed ond nid ei chyhoeddi. - Cynnwys is-dudalennau heb eu cyhoeddi - Cyhoeddi ar waith - arhoswch... - %0% allan o %1% o dudalennau wedi eu cyhoeddi... - %0% wedi ei gyhoeddi - %0% ac eu is-dudalennau wedi'u cyhoeddi - Cyhoeddi %0% ac ei holl is-dudalennau - - Cyhoeddi er mwyn cyhoeddi %0% a felly yn gwneud i'r cynnwys berthnasol fod ar gael i'r cyhoedd.

- Gallwch gyhoeddi'r dudalen yma ac ei holl is-dudalennau wrth dicio Cynnwys tudalennau heb eu cyhoeddi isod. - ]]> -
- - - Nid ydych chi wedi ffurfweddu unrhyw liwiau sydd wedi'u cymeradwyo - - - Gallwch ond ddewis eitemau o'r math(au): %0% - Rydych wedi dewis eitem gynnwys sydd naill ai wedi'i ddileu neu yn y bin ailgylchu - Rydych wedi dewis eitemau gynnwys sydd naill ai wedi'u dileu neu yn y bin ailgylchu - - - Rydych wedi dewis eitem gyfrwng sydd naill ai wedi'i ddileu neu yn y bin ailgylchu - Rydych wedi dewis eitemau gyfrwng sydd naill ai wedi'u dileu neu yn y bin ailgylchu - Eitem wedi'i ddileu - Yn sbwriel - - - Darparwch ddolen allanol - Dewiswch dudalen fewnol - Capsiwn - Dolen - Agor mewn ffenestr newydd - Darparwch y capsiwn arddangos - Darparwch y ddolen - - - Ailosod tocio - Achub tocio - Ychwanegu tocio newydd - Wedi gwneud - Dadwneud golygion - Diffiniad defnyddiwr - - - 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 - Rolio yn ôl at - Dewis fersiwn - Gwedd - - - Golygu ffeil sgript - - - Gwas - Cynnwys - Tywyswr - Datblygwr - Ffurflenni - Cymorth - Dewin Ffurfweddu Umbraco - Cyfrwng - Aelodau - Cylchlythyrau - Pecynnau - Gosodiadau - Ystadegau - Cyfieithiad - Defnyddwyr - Dadansoddeg - - - ewch i - Pynciau cymorth ar gyfer - Penodau fideo ar gyfer - Teithiau - Y fideos tiwtorial Umbraco gorau - Ymweld â our.umbraco.com - Ymweld â umbraco.tv - - - Templed diofyn - Allwedd Geiriadur - Er mwyn mewnforio math o ddogfen, darganfyddwch y ffeil ".udt" ar ecih cyfrifiadur wrth glicio ar y botwn "Pori" a cliciwch "Mewnforio" (byddwch yn cael eich gofyn i gadarnhau ar y sgrîn nesaf) - Teitl Tab Newydd - Math o nod - Math - Taflen arddull - Sgript - Priodwedd taflen arddull - Tab - Teitl Tab - Tabiau - Math o Gynnwys Meistr wedi'i alluogi - Mae'r Math o Gynnwys yma yn defnyddio - Dim priodweddau wedi'u diffinio ar y tab yma. Cliciwch ar y ddolen "ychwanegu priodwedd newydd" ar y topi greu priodwedd newydd. - Math o Ddogfen Feistr - Creu templedi cydweddol - Ychwanegu eicon - - - Trefn - Dyddiad creu - Trefnu wedi'i gwblhau. - Llusgwch yr eitemau gwahanol i fyny neu i lawr isod er mwyn gosod sut dylen nhw gael eu trefnu. Neu cliciwch ar beniadau'r golofnau i drefnu'r holl gasgliad o eitemau - - Nid oes gan y nod hwn nodau plentyn i trefnu - - - Dilysiad - Rhaid i wallau dilysu gael eu trwsio cyn gall yr eitem gael ei achub - Wedi methu - Wedi achub - Diffyg hawliau defnyddiwr, ni ellir cwblhau'r gweithred - Wedi canslo - Gweithred wedi'i ganslo gan ymestyniad 3-ydd parti - Cyhoeddi wedi'i ganslo gan ymestyniad 3-ydd parti - Math o briodwedd yn bodoli eisoes - Math o briodwedd wedi'i greu - Math o ddata: %1%]]> - math o briodwedd wedi'i ddileu - Math o Ddogfen wedi'u achub - Tab wedi'i greu - Tab wedi'i ddileu - Tab gyda id: %0% wedi'i ddileu - Taflen arddull heb ei achub - Taflen arddull wedi'i achub - Taflen arddull wedi'i achub heb unrhyw wallau - Math o ddata wedi'i achub - Eitem geiriadur wedi'i achub - Cyhoeddi wedi methu gan nad yw'r dudalen rhiant wedi'i gyhoeddi - Cynnwys wedi'i gyhoeddi - ac yn weladwy ar y wefan - %0% dogfennau wedi'i gyhoeddi ac yn gweledig ar y wefan - %0% gyhoeddi ac yn gweledig ar y wefan - %0% dogfennau wedi'i gyhoeddi am yr ieithoedd %1% ac yn gweledig ar y wefan - ac yn weladwy ar y wefan tan %0% at %1% - Cynnwys wedi'i achub - Cofiwch gyhoeddi er mwyn i'r newidiadau fod yn weladwy - Mae amserlen ar gyfer cyhoeddi wedi'i diweddaru - %0% wedi arbed - Bydd newidiadau yn cael ei gymerdwyo ar %0% at %1% - Wedi'i anfon am gymeradwyo - Newidiadau wedi'u hanfon am gymeradwyo - %0% newidiadau wedi'u hanfon am gymeradwyo - Cyfrwng wedi'i achub - Grŵp aeloadeth wedi'i achub - Cyfrwng wedi'i achub heb unrhyw wallau - Aelod wedi'i achub - Priodwedd taflen arddull wedi'i achub - Taflen arddull wedi'i achub - Templed wedi'i achub - Gwall yn achub y defnyddiwr (gwiriwch y log) - Defnyddiwr wedi'i achub - math o ddefnyddiwr wedi'i achub - Grŵp defnyddwyr wedi'i achub - Diwylliannau ac enwau gwesteia wedi'i achub - Gwall wrth achub diwylliannau ac enwau gwesteia - Ffeil heb ei achub - Ni ellir achub y ffeil. Gwiriwch hawliau'r ffeil - Ffeil wedi'i achub - Ffeil wedi'i achub heb unrhyw wallau - Iaith wedi'i achub - Math o Gyfrwng wedi'i achub - Math o Aelod wedi'i achub - Grŵp Aelod wedi'i achub - Sgript Python heb ei achub - Ni ellir achub y sgript Python oherwydd gwall - Sgript Python wedi'i achub - Dim gwallau yn y sgript Python - Templed heb ei achub - Sicrhewch nad oes gennych 2 dempled gyda'r un enw arall - Templed wedi'i achub - Templed wedi'i achub heb unrhyw wallau! - XSLT heb ei achub - XSLT yn cynnwys gwall - Ni ellir achub y ffeil XSLT, gwiriwch hawliau ffeil - XSLT wedi'i achub - Dim gwallau yn yr XSLT - Cynnwys wedi'i ddadgyhoeddi - amrywiad cynnwys %0% wedi'i dadgyhoeddi - Roedd yr iaith orfodol '%0%' wedi'i dadgyhoeddi. Mae'r holl ieithoedd ar gyfer yr eitem gynnwys hon bellach wedi'i dadgyhoeddi. - Rhan-wedd wedi'i achub - Rhan-wedd wedi'i achub heb unrhyw wallau! - Rhan-wedd heb ei achub - Bu gwall yn ystod achub y ffeil. - Hawliau wedi'u hachub ar gyfer - Gwedd sgript wedi'i achub - Gwedd sgript wedi'i achub heb unrhyw wallau! - Gwedd sgript heb ei achub - Bu gwall yn ystod achub y ffeil. - Bu gwall yn ystod achub y ffeil. - Wedi dileu %0% o rwpiau defnwyddwr - %0% wedi'i ddileu - %0% o ddefnyddwyr wedi'u galluogi - Bu gwall yn ystod galluogi'r defnyddwyr - Wedi analluogi %0% o ddefnyddwyr - Bu gwall yn ystod analluogi'r defnyddwyr - %0% yn awr wedi galluogi - Bu gwall yn ystod galluogi'r defnyddiwr - %0% yn awr wedi analluogi - Bu gwall yn ystod analluogi'r defnyddiwr - Grwpiau defnyddiwr wedi'u gosod - Wedi dileu %0% o rwpiau defnyddwyr - %0% wedi dileu - Wedi datgloi %0% o ddefnyddwyr - Bu gwall yn ystod datgloi'r defnyddwyr - %0% yn awr wedi datgloi - Bu gwall yn ystod datgloi'r defnyddiwr - Allforwyd yr aelod at ffeil - Bu gwall yn ystod allforio'r aelod - Defnyddiwr %0% wedi'i ddileu - Gawhodd defnyddiwr - Gwahoddiad wedi'i ail-anfon at %0% - Methu cyhoeddi'r ddogfen gan nad yw'r gofynnol '%0%' wedi cael ei gyhoeddi - Methodd dilysiad ar gyfer iaith '%0%' - Mae'r math dogfen wedi ei allforio i ffeil - Digwyddodd gwall wrth allforio'r math dogfen - Ni all y dyddiad rhyddhau fod yn y gorffennol - Ni all drefnu'r ddogfen i'w chyhoeddi gan nad yw'r gofynnol '%0%' wedi cael ei gyhoeddi - Ni all drefnu'r ddogfen i'w chyhoeddi oherwydd mae ganddo'r gofynnol '%0%' ddyddiad cyhoeddi yn hwyrach nag iaith nad yw'n orfodol - Ni all y dyddiad terfyn fod yn y gorffennol - Ni all y dyddiad terfyn fod cyn y dyddiad rhyddhau - - - Yn defnyddio cystrawen CSS e.e: h1, .coch, .glas - Ychwanegu ardull - Golygu ardull - Ardull golygydd testun cyfoethog - Diffiniwch yr arddulliau a ddylai fod ar gael yn y golygydd testun cyfoethog ar gyfer y daflen arddull hon - Golygu taflen arddull - Golygu priodwedd taflen arddull - Enw ar gyfer adnabod y priodwedd arddull yn y golygydd testun gyfoethog - Rhagolwg - Sut fydd y testun yn edrych yn y golygydd testun cyfoethog. - Dewisydd - Yn defnyddio cystrawen CSS e.e: h1, .coch, .glas - Arddulliau - Dyled y CSS ei gymhwyso yn y golygydd testun cyfoethog, e.g. "color:red;" - Côd - Golygydd - - - - Methwyd dileu templed efo'r ID %0% - Golygu templed - Adrannau - Mewnosod ardal cynnwys - Mewnosod dalfan ar gyfer ardal cynnwys - Mewnosod - Dewiswch beth i fewnosod i mewn i'ch templed - Eitem geiriadaur - Mae eitem geiriadur yn ddalfan ar gyfer darn o destun y gall gael ei gyfieithu, sy'n ei wneud yn hawdd i greu dyluniadau ar gyfer gwefannau aml-ieithog. - Macro - - Mae Macro yn gydran ffurfweddol sy'n wych ar gyfer - darnau o'ch dyluniad sy'n cael eu ail-ddefnyddio, ble mae angen y dewis i ddarparu paramedrau, - er enghraifft orielau, ffurflenni a rhestri. - - Gwerth - Yn dangos gwerth maes penodol o'r dudalen bresennol, gyda'r dewisiadau i newid y gwerth neu syrthio'n ôl at werthoedd eraill. - Rhan-wedd - - Mae rhan-wedd yn ffeil templed ar wahân y gall gael ei ddatganu o fewn templed arall, - mae'n wych ar gyfer ail-ddefnyddio côd neu ar gyfer gwahanu templedi cymhleth i mewn i ffeiliau gwahanol. - - Templed Meistr - Dim templed meistr - Dim meistr - - Datganu templed blentyn - - @RenderBody(). - ]]> - - Diffiniwch adran benodol - - @section { ... }. Gall hyn gael ei ddatganu mewn adran - benodol o rhiant y templed yma, wrth ddefnyddio @RenderSection. - ]]> - - Datganu adran benodol - - @RenderSection(name). - mae hyn yn datganu adran o dempled blentyn sydd wedi'i lapio mewn diffiniad berthnasol o @section [name]{ ... }. - ]]> - - Enw Adran - Mae Adran yn ofynnol - @section, fel arall bydd gwall yn cael ei ddangos. - ]]> - Adeiladwr ymholiad - Adeiladu ymholiad - o eitemau wedi dychwelyd, mewn - Copi i'r clipfwrdd - Rydw i eisiau - holl gynnwys - cynnwys o'r fath "%0%" - o - fy wefan - ble - ac - yn - ddim yn - cyn - cyn (gan gynnwys y dyddiad dewiswyd) - ar ôl - ar ôl (gan gynnwys y dyddiad dewiswyd) - yn gyfartal i - ddim yn gyfartal i - yn cynnwys - ddim yn cynnwys - yn fwy na - yn fwy na neu yn gyfartal i - llai na - llai na neu yn gyfartal i - Id - Enw - Dyddiad Creu - Dyddiad Diweddariad Ddiwethaf - trefnu wrth - esgynnol - disgynnol - Templed - - - Golygydd Testun Gyfoethog - Llun - Macro - Mewnosod - Pennawd - Dyfyniad - Dewis math o gynnwys - Dewis cynllun - Ychwanegu rhes - Ychwanegu cynnwys - Gollwng cynnwys - Gosodiadau wedi'u hymgeisio - Nid yw'r cynnwys yma wedi'i ganiatáu yma - Caniateir y cynnwys yma - Cliciwch i fewnblannu - Cliciwch i fewnosod llun - Capsiwn llun... - Ysgrifennwch yma... - Cynlluniau Grid - Cynlluniau yw'r holl ardal weithio gyfan ar gyfer y golygydd grid, fel arfer rydych ddim ond angen un neu ddau gynllun gwahanol - Ychwanegu Cynllun Grid - Golygu Cynllun Grid - Newid y cynllun wrth osod lledau colofnau ac ychwanegu adrannau ychwanegol - Ffurfweddau rhes - Mae rhesi yn gelloedd sydd wedi'u trefnu yn llorweddol - Ychwanegu Ffurfwedd rhes - Golygu Ffurfwedd rhes - Newidiwch y rhes wrth osod lledau colofn ac ychwanegu adrannau ychwanegol - Nid oes ffurfwedd pellach ar gael - Colofnau - Cyfanswm y nifer o golofnau yn y cynllun grid - Gosodiadau - Ffurfweddu pa osodiadau gall olygyddion eu newid - Ardduliau - Ffurfweddu pa arddulliau gall olygyddion eu newid - Bydd gosodiadau dim ond yn newid os mae'r ffurfwedd json yn ddilys - Caniatáu pob golygydd - Caniatáu holl ffurfweddi rhes - Uchafswm o eitemau - Gadewch yn wag neu gosod i 0 ar gyfer diderfyn - Gosod fel diofyn - Dewis ychwanegol - Dewis diofyn - wedi'u hychwanegu - Rhybudd - Rydych chi'n dileu'r ffurfwedd rhes - Bydd dileu enw ffurfwedd rhes yn arwain at golli data ar gyfer unrhyw gynnwys cynfodol sy'n seiliedig ar ffurfwedd hwn. - - - Cyfansoddiadau - Nid ydych wedi ychwanegu unrhyw dabiau - Ychwanegu tab newydd - Ychwanegu tab arall - Grŵp - Nid ydych wedi ychwanegu unrhyw grwpiau - Ychwanegu grŵp - Wedi etifeddu o - Ychwanegu priodwedd - Label gofynnol - Caniatáu gwedd rhestr - Ffurfweddi yr eitem gynnwys i ddangos rhestr trefnadwy a chwiladwy o'i phlant, ni fydd y plant yn cael eu dangos yn y goeden - Templedi Caniateir - Dewiswch pa olygoddion templedi sy'n cael defnyddio cynnwys o'r fath yma - Caniatáu fel gwraidd - Caniatáu golygyddion i greu cynnwys o'r fath yma yng ngwraidd y goeden gynnwys - Iawn - caniatáu cynnwys o'r fath yma yn y gwraidd - Mathau o nod blentyn caniateir - Caniatáu cynnwys o'r mathau benodol i gael eu creu o dan cynnwys o'r fath yma - Dewis nod blentyn - Etifeddu tabiau a phriodweddau o fath o ddogfen sy'n bodoli eisoes. Bydd tabiau newydd yn cael eu ychwanegu at y fath o ddogfen bresennol neu eu cyfuno os mae tab gyda enw yr union yr un fath yn bodoli eisoes. - Mae'r math o gynnwys yma wedi'i ddefnyddio mewn cyfansoddiad, felly ni ellir ei gyfansoddi ei hunan. - Nid oes unrhyw fathau o gynnwys ar gael i'w defnyddio fel cyfansoddiad. - Bydd dileu cyfansoddiad yn dileu'r holl ddata eiddo priodwedd gysylltiedig. Ar ôl i chi arbed y math o ddogfen, bydd ddim ffordd nôl. - Golygyddion ar gael - Ail-ddefnyddio - Gosodiadau golygydd - Ffurfweddau sydd ar gael - Creu ffurfwedd newydd - Ffurfwedd - Iawn, dileu - wedi symud islaw - wedi copïo islaw - Dewiswch y ffolder i symud - Dewiswch y ffolder i gopïo - i yn y strwythyr goeden isod - Holl Fathau o Ddogfennau - Holl Ddogfennau - Holl eitemau gyfrwng - sy'n defnyddio'r fath o ddogfen yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. - sy'n defnyddio'r fath o gyfrwng yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. - sy'n defnyddio'r fath o aelod yma fydd yn cael eu dileu yn barhaol, cadarnhewch os hoffwch ddileu'r rhain hefyd. - a phob dogfen sy'n defnyddio'r fath yma - a phob eitem gyfrwng sy'n defnyddio'r fath yma - a phob aelod sy'n defnyddio'r fath yma - sy'n defnyddio'r golygydd yma fydd yn cael eu diweddaru gyda'r gosodiadau newydd - Aeloed yn gallu golygu - Caniatáu i'r gwerth briodwedd yma gael ei olygu gan yr aelod ar eu tudalen broffil - Yn ddata sensitif - Cuddio'r priodwedd yma o'r golygyddion cynnwys sydd heb hawliau i weld gwybodaeth sensitif - Dangos ar broffil aelod - Caniatáu i'r gwerth briodwedd yma gael ei ddangos ar y dudalen broffil aelod - does dim rhif trefnu gan y tab - Ble mae'r cyfansoddiad yma'n cael ei ddefnyddio? - Mae'r cyfansoddiad yma yn cael ei ddefnyddio'n bresennol yng nghyfansoddiad o'r mathau o gynnwys ganlynol: - Caniatáu amrywiadau - Caniatáu amrywiad yn ôl ddiwylliant - Caniatáu segmentiad - Amrywio gan ddiwylliant - Amrywio gan segmentiad - Caniatáu i olygyddion greu cynnwys o'r math hwn mewn gwahanol ieithoedd - Caniatáu golygyddion i greu cynnwys o ieithoedd gwahanol - Caniatáu golygyddion i greu segmentiadau o'r cynnwys hwn - Caniatáu amrywio yn ôl diwylliant - Caniatáu segmentiad - Math o elfen - Yn fath Elfen - Mae math Elfen i fod i gael ei ddefnyddio er enghraifft mewn Cynnwys Nythu, ac nid yn y goeden - Ni ellir newid math o ddogfen i fath Elfen ar ôl mae'n cael ei defnyddio i greu un neu fwy o eitemau cynnwys. - Nid yw hyn yn berthnasol ar gyfer math Elfen - Rydych wedi gwneud newidiadau i'r eiddo hwn. Ydych chi'n siŵr eich bod chi am eu taflu? - - - Ychwanegu iaith - Iath gorfodol - Rhaid llenwi eiddo ar yr iaith hon cyn y gellir cyhoeddi'r nod. - Iaith diofyn - Gall wefan Umbraco ddim ond cael un iaith ddiofyn. - Gall newid iaith ddiofyn arwain at golli cynnwys diofyn. - Syrthio yn ôl i - Dim iaith cwympo yn ôl - Er mwyn caniatáu i gynnwys amlieithog ddisgyn yn ôl i iaith arall os nad yw'n bresennol yn yr iaith y gofynnwyd amdani, dewiswch hi yma. - Iaith cwympo yn ôl - dim - - - Ychwanegu paramedr - Golygu paramedr - Rhowch enw macro - Paramedrau - Diffiniwch y paramedrau a ddylai fod ar gael wrth ddefnyddio'r macro hwn. - Dewiswch ffeil macro golwg rhannol - - - Adeiladu modelau - gall hyn gymryd amser, peidiwch â phoeni - Modelau wedi'u generadu - Methwyd generadu modelau - Methwyd generadu modelau, gweler yr eithriadau yn y log Umbraco - - - Ychwanegu maes rolio yn ôl - Maes rolio yn ôl - Ychwanegu gwerth diofyn - Gwerth diofyn - Maes rolio yn ôl - Gwerth diofyn - Cyflwr - Amgodiad - Dewis maes - Trawsnewid torriadau llinellau - Iawn, trawsnewid torriadau llinellau - Cyfnewid torriadau llinellau gyda tag html 'br' - Meysydd bersonol - Dyddiad yn unig - Fformat ac amgodiad - Fformatio ar ffurf dyddiad - Fformatio'r gwerth ar ffurf dyddiad, neu dyddiad gyda amser, yn ôl y diwylliant gweithredol - Amgodi HTML - Bydd yn cyfnewid nodau arbennig gyda'u nodau HTML cyfatebol. - Bydd yn cael ei fewnosod ar ôl y gwerth maes - Bydd yn cael ei fewnosod cyn y gwerth maes - Llythrennau bach - Newid allbwn - Dim - Sampl allbwn - Mewnosod ar ôl maes - Mewnosod cyn maes - Ailadroddus - Iawn, gwnewch yn ailadroddus - Gwahanwr - Meysydd Safonol - Llythrennau bras - Amgodi URL - Bydd yn fformatio nodau arbennig o fewn URL - Bydd ddim ond yn cael ei ddefnyddio pan mae'r gwerthoedd maes uchod yn wag - Bydd y maes yma ddim ond yn cael ei ddefnyddio os mae'r maes gynradd yn wag - Dyddiad ac amser - - - Tasgau wedi'u neilltuo i chi - - wedi'u neilltuo i chi. Er mwyn gweld gwedd fanwl gan gynnwys sylwadau, cliciwch ar "Manylion" neu enw'r dudalen. - Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio'r ddolen "Lawrlwytho Xml".
- Er mwyn cau tasg cyfieithu, ewch at y wedd fanylion a cliciwch ar y botwm "Cau". - ]]> -
- cau tasg - Manylion cyfieithiad - Lawrlwytho pob tasg cyfieithu ar ffurf XML - Lawrlwytho XML - Lawrlwytho XML DTD - Meysydd - Cynnwys is-dudalennau - - - - [%0%] Tasg cyfieithu ar gyfer %1% - Dim defnyddwyr cyfieithu wedi'u darganfod. Creuwch ddefnyddiwr cyfieithu cyn i chi gychwyn anfon cynnwys am gyfieithiadau - Tasgau wedi'u creu gennych chi - - wedi'u creu gennych chi. Er mwyn gweld gwedd fanwl sy'n cynnwys sylwadau, - cliciwch ar "Manylion" neu enw'r dudalen. Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio ar y ddolen "Lawrlwytho Xml". - Er mwyn cau tasgau cyfieithu, ewch at y wedd fanylion a cliciwch y botwm "Cau". - ]]> - - Mae'r dudalen '%0%' wedi cael ei anfon am gyfieithiad - Dewiswch yr iaith y dylai'r cynnwys gael ei gyfieithu i - Anfon y dudalen '%0%' am gyfieithiad - Wedi'i neilltuo gan - Tasg wedi'i hagor - Cyfanswm o eiriau - Cyfieithu i - Cyfieithiad wedi'i gwblhau. - Gallwch ragolygu'r tudalennau yr ydych newydd gyfieithu gan glicio isod. Os mae'r dudalen gwreiddiol wedi'i ganfod, byddwch yn cael cymhariaeth o'r 2 dudalen. - Cyfieithiad wedi methu, mae'n bosib fod y ffeil XML wedi llygru - Dewisiadau cyfieithu - Cyfieithydd - Lanlwytho cyfieithiad XML - - - Cynnwys - Templedi Cynnwys - Cyfrwng - Porwr Storfa - Bin Ailgylchu - Pecynnau wedi'u creu - Mathau o Ddata - Geiriadur - Pecynnau wedi'u gosod - Gosod croen - Gosod cit gychwynol - Ieithoedd - Gosod pecyn leol - Macros - Mathau o Gyfrwng - Aelodau - Grwpiau Aelodau - Grwpiau Rolau - Mathau o Aelod - Mathau o Ddogfen - Math o Berthynas - Pecynnydd - Pecynnau - Rhan-weddi - Ffeiliau Rhan-wedd Macro - Ffeiliau Python - Gosod o ystorfa - Gosod Runway - Modylau Runway - Ffeiliau Sgriptio - Sgriptiau - Taflenni arddull - Templedi - Ffeiliau XSLT - Dadansoddeg - Gwyliwr Log - Defnyddwyr - Gosodiadau - Templedi - Trydydd parti - - - Diweddariad newydd yn barod - %0% yn barod, cliciwch yma i lawrlwytho - Dim cysylltiad at y gweinydd - Gwall yn chwilio am ddiweddariad. Ceisiwch wirio'r trywydd stac am fwy o wybodaeth - - - Mynediad - Ar sail y grwpiau aelodaeth ac y nodau cychwyn, mae gan y defnyddiwr hawliau at y nodau ganlynol - Neilltuo hawl - Gweinyddwr - Maes categori - Defnyddiwr wedi'i greu - Newidiwch Eich Cyfrinair - Newidiwch lun - Cyfrinair newydd - ddim wedi cloi allan - Nid yw'r cyfrinair wedi'i newid - Cadarnhau cyfrinair newydd - Gallwch newid eich cyfrinair i gyrchu Swyddfa Gefn Umbracogan lenwi allan y ffurflen isod a chlicio'r botwm 'Newid Cyfrinair' - Sianel Gynnwys - Creu defnyddiwr arall - Creu defnyddwyr newydd i roi hawliau iddynt gyrchu Umbraco. Pan mae defnyddiwr newydd yn cael ei greu, bydd cyfrinair yn cael ei generadu y gallwch chi rannu gyda'r defnyddiwr. - Maes disgrifiad - Analluogi Defnyddiwr - Math o Ddogfen - Golygydd - Maes dyfyniad - Nifer o fethiannau ceisio mewngofnodi - Ewch at broffil defnyddiwr - Ychwanegu grwpiau i neilltuo mynediad a hawliau - Gwahodd defnyddiwr arall - Gwahodd defnyddwyr newydd i roi hawliau iddynt gyrchu Umbraco. Bydd gwahoddiad ebost yn cael ei anfon at y defnyddiwr gyda gwybodaeth ar sut i fewngofnodi i Umbraco. Mae gwahoddiadau yn para am 72 awr. - Iaith - Gosod yr iaith fyddwch chi'n gweld yn y dewislenni a'r deialogau - Dyddiad cloi allan diweddaraf - Mewngofnodi diweddaraf - Cyfrinair wedi'i newid ddiwethaf - Enw defnyddiwr - Nod gychwynol gyfrwng - Cyfyngu'r llyfrgell gyfrwng at nod gychwynol benodol - Nodau gychwynol gyfrwng - Cyfyngu'r llyfrgell gyfrwng at nodau gychwynol benodol - Adrannau - Analluogi Mynediad Umbraco - ddim wedi mewngofnodi eto - Hen gyfrinair - Cyfrinair - Ailosod cyfrinair - Mae eich cyfrinair wedi'i newid! - Cyfrinair wedi'i newid - Cadarnhewch y cyfrinair newydd - Darparwch eich cyfrinair newydd - Ni all eich cyfrinair newydd fod yn wag! - Cyfrinair bresennol - Cyfrinair bresennol annilys - Roedd gwahaniaeth rhwng y cyfrinair newydd ac y cyfrinair i gadarnhau. Ceisiwch eto! - Nid yw'r cyfrinair cadarnhau yn cyfateb â'r cyfrinair newydd! - Cyfnewid hawliau nod blentyn - Rydych ar hyn o bryd yn newid hawliau ar gyfer y tudalennau: - Dewis tudalennau i newid eu hawliau - Dileu llun - Hawliau diofyn - Hawliau gronynnog - Gosod hawliau ar gyfer nodau penodol - Proffil - Chwilio holl blant - Ychwanegu adrannau i roi hawliau i ddefnyddwyr - Dewis grwpiau defnyddwir - Dim nod gychwynol wedi'i ddewis - Dim nodau cychwynol wedi'u dewis - Nod gynnwys gychwynol - Cyfyngu'r goeden gynnwys i nod gychwynol benodol - Nodau cynnwys gychwynol - Cyfyngu'r goeden gynnwys i nodau gychwynol benodol - Defnyddiwr wedi diweddaru ddiwethaf - wedi ei greu - Mae'r defnyddiwr newydd wedi'i greu. Er mwyn mewngofnodi i Umbraco defnyddiwch y cyfrinair isod. - Rheoli defnyddwyr - Enw - Hawliau defnyddiwr - Hawliau grwpiau defnyddiwr - Grŵp defnyddiwr - Grwpiau defnyddiwr - wedi'i wahodd - Mae gwahoddiad wedi cael ei anfon at y defnyddiwr newydd gyda manylion ar sut i fewngofnodi i Umbraco. - Helo a chroeso i Umbraco! Mewn 1 munud yn unig, byddech chi'n barod i fynd, rydym dim ond angen gosod cyfrinair a llun ar gyfer eich avatar. - Croeso i Umbraco! Yn anffodus, mae eich gwahoddiad wedi terfynu. Cysylltwch â'ch gweinyddwr a gofynnwch iddynt ail-anfon. - Lanlwythwch lun i wneud o'n haws i boble eich adnabod chi. - Ysgrifennydd - Cyfieithydd - Newid - Eich proffil - Eich hanes diweddar - Sesiwn yn terfynu mewn - Gwahodd defnyddiwr - Creu defnyddiwr - Anfon gwahoddiad - Yn ôl at ddefnyddwyr - Umbraco: Gwahoddiad - - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Helo %0%, -

-

- Rydych wedi cael eich gwahodd gan %1% i'r Swyddfa Gefn Umbraco. -

-

- Neges oddi wrth %1%: -
- %2% -

- - - - - - -
- - - - - - -
- - Cliciwch y ddolen yma i dderbyn y gwahoddiad - -
-
-

Os na allwch chi glicio ar y ddolen, copiwch a gludwch y URL i mewn i'ch porwr:

- - - - -
- - %3% - -
-

-
-
-


-
-
- - ]]> -
- Gwahoddiad - Yn ail-anfon y gwahoddiad... - Dileu Defnyddiwr - Ydych chi'n sicr eich bod eisiau dileu'r cyfrif defnyddiwr yma? - Pob - Gweithredol - Wedi analluogi - Wedi cloi allan - Wedi gwahodd - Anactif - Enw (A-Y) - Enw (Y-A) - Hynaf - Diweddaraf - Mewngofnodi diweddaraf - No user groups have been added - - - Dilysiad - Dim dilysiad - Dilysu fel cyfeiriad ebost - Dilysu fel rhif - Dilysu fel URL - ...neu darparwch ddilysiad bersonol - Maes yn ofynnol - Darparwch neges gwall dilysiad arferu (opsiynol) - Darparwch fynegiad rheoliadd - Darparwch neges gwall dilysiad arferu (opsiynol) - Mae angen i chi ychwanegu o leiaf - gallwch ddim ond gael - Adio lan i - o eitemau - url(s) - url(s) wedi'i ddewis - o eitemau wedi'u dewis - Dyddiad annilys - Ddim yn rif - Ebost annilys - Ni all y gwerth fod yn null - Ni all y gwerth fod yn gwag - Mae'r gwerth yn annilys, nid yw'n cyfateb i'r patrwm cywir - Dilysiad arferu - %1% mwy.]]> - %1% gormod.]]> - - - - Gwerth wedi'i osod at y gwerth argymhellwyd: '%0%'. - Gwerth wedi'i osod at '%1%' ar gyfer XPath '%2%' yn y ffeil ffurfweddu '%3%'. - Yn disgwyl y gwerth '%1%' ar gyfer '%2%' yn y ffeil ffurfweddu '%3%', ond darganfyddwyd '%0%'. - Darganfyddwyd gwerth annisgwyl '%0%' ar gyfer '%2%' yn y ffeil ffurfweddu '%3%'. - - Gwallau bersonol wedi gosod at '%0%'. - Gwallau bersonol wedi gosod at '%0%' yn bresennol. Argymhellwyd i osod hyn i '%1%' cyn mynd yn fyw. - Gwallau bersonol wedi gosod at '%0%' yn llwyddiannus. - - Gwallau Macro wedi gosod at '%0%'. - Gwallau Macro wedi gosod at '%0%' a fydd yn atal rhai neu holl dudalennau yn eich safle rhag llwytho'n gyfan gwbl os oes unrhyw wallau o fewn macros. Bydd cywiro hyn yn gosod y gwerth at '%1%'. - Gwallau Macro wedi gosod at '%0%' yn llwyddiannus. - - Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%' ac rydych yn defnyddio fersiwn IIS '%1%'. - Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%'. Argymhellwyd gosod hyn at '%1%' ar gyfer eich fersiwn IIS (%2%). - Ceisio sgipio Gwallau IIS Bersonol wedi'i osod at '%0%' yn llwyddiannus. - - Ffeil ddim yn bodoli: '%0%'. - '%0%' yn y ffeil ffurfweddu '%1%'.]]> - Bu gwall, gwiriwch y log ar gyfer y gwall cyflawn: %0%. - Aelodau - Cyfanswm XML: %0%, Cyfanswm: %1%, Cyfanswm annilys: %2% - Cyfrwng - Cyfanswm XML: %0%, Cyfanswm: %1%, Cyfanswm annilys: %2% - Cynnwys - Cyfanswm XML: %0%, Cyfanswm wedi cyhoeddi: %1%, Cyfanswm annilys: %2% - Cronfa ddata - Mae'r sgema gronfa ddata yn gywir ar gyfer y fersiwn yma o Umbraco - %0% o broblemau wedi'u canfod gyda'ch sgema gronfa ddata (Gwiriwch y log am fanylion) - Darganfyddwyd gwallau wrth ddilysu'r sgema gronfa ddata yn erbyn y fersiwn bresennol o Umbraco. - Mae tystysgrif eich gwefan yn ddilys. - Gwall dilysu tystysgrif: '%0%' - Mae tystysgrif SSL eich gwefan wedi terfynu. - Mae tystysgrif SSL eich gwefan am derfynu mewn %0% diwrnod. - Gwall yn pingio'r URL %0% - '%1%' - Rydych yn bresennol %0% yn gweld y wefan yn defnyddio'r cynllun HTTPS. - Mae'r appSetting 'umbracoUseSSL' wedi'i osod at 'false' yn eich ffeil web.config. Unwaith rydych yn ymweld â'r safle gan ddefnyddio'r cynllun HTTPS, dylai hynny gael ei osod i 'true'. - Mae'r appSetting 'umbracoUseSSL' wedi'i osod at '%0%' yn eich ffeil web.config, mae eich cwcis %1% marcio yn ddiogel. - Ni ellir diweddaru'r gosodiad 'umbracoUseSSL' yn eich ffeil web.config. Gwall: %0% - - Galluogi HTTPS - Yn gosod umbracoSSL i true yn yr appSettings yn y ffeil web.config. - Mae'r appSetting 'umbracoUseSSL' yn awr wedi'i osod at 'true' yn eich ffeil web.config, bydd eich cwcis wedi eu marcio yn ddiogel. - Trwsio - Ni ellir trwsio gwiriad gyda math chymhariaeth gwerth o 'ShouldNotEqual'. - Ni ellir trwsio gwiriad gyda math chymhariaeth gwerth o 'ShouldEqual' gyda gwerth a ddarparwyd. - Gwerth i drwrsio gwiriad heb ei ddarparu. - Modd casgliad dadfygio wedi'i analluogi. - Modd casgliad dadfygio wedi'i alluogi. Argymhellwyd analluogi'r gosodiad yma cyn mynd yn fyw. - Modd casgliad dadfygio wedi'i analluogi yn llwyddiannus. - Modd olrhain wedi'i analluogi. - Modd olrhain wedi'i alluogi. Argymhellwyd analluogi'r gosodiad yma cyn mynd yn fyw. - Modd olrhain wedi'i analluogi yn llwyddiannus. - Mae gan pob ffolder yr hawliau cywir wedi'u gosod. - - %0%.]]> - %0%. Os nad ydyn nhw'n cael eu ysgrifennu atynt, does dim angen unrhyw weithred.]]> - Mae gan pob ffeil yr hawliau cywir wedi'u gosod. - - %0%.]]> - %0%. Os nad ydyn nhw'n cael eu ysgrifennu atynt, does dim angen unrhyw weithred.]]> - X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> - X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> - Gosod Peniad o fewn Ffurfwedd - Ychwanegu gwerth at yr adran httpProtocol/customHeaders o'r ffeil web.config er mwyn atal y safle rhag cael ei ddangos o fewn IFRAME gan safleoedd eraill. - Gosodiad ar gyfer creu peniad sy'n atal y wefan rhag cael ei ddangos o fewn IFRAME ar safle arall wedi'i ychwanegu at eich ffeil web.config. - Ni ellir diweddaru'r ffeil web.config. Gwall: %0% - X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> - X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> - Ychwanegu gwerth at yr adran httpProtocol/customHeaders o'r ffeil web.config er mwyn amddiffyn yn erbyn gwendidau sniffio MIME. - Gosodiad ar gyfer creu peniad sy'n amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ychwanegu at eich ffeil web.config. - Strict-Transport-Security, hefyd wedi'i adnabod fel HSTS-header, wedi'i ganfod.]]> - Strict-Transport-Security wedi'i ganfod.]]> - Ychwanegu'r peniad 'Strict-Transport-Security' gyda'r gwerth 'max-age=10886400; preload' i'r adran httpProtocol/customHeaders o'r ffeil web.config. Defnyddiwch y trwsiad hyn dim ond os bydd gennych chi eich parthau yn rhedeg gyda https am yr 18 wythnos nesaf (o leiaf). - Mae'r peniad HSTS wedi'i ychwanegu at y ffeil web.config. - X-XSS-Protection wedi'i ganfod.]]> - X-XSS-Protection wedi'i ganfod.]]> - Ychwanegu'r peniad 'X-XSS-Protection' gyda'r gwerth '1; mode=block' at yr adran httpProtocol/customHeaders yn y ffeil web.config. - Mae'r peniad X-XSS-Protection wedi'i ychwanegu at y ffeil web.config. - - %0%.]]> - Dim peniadau sy'n datgelu gwynodaeth am dechnoleg eich gwefan wedi'u canfod. - Ni ellir darganfod system.net/mailsettings yn y ffeil Web.config. - Yn yr adran system.net/mailsettings o'r ffeil Web.config, nid yw'r "host" wedi ffurfweddu. - Gosodiadau SMTP wedi ffurfweddu'n gywir ac mae'r gwasanaeth yn gweithio fel y disgwylir. - Ni ellir cysylltu â gweinydd SMTP sydd wedi ffurfweddu gyda "host" '%0%' a phorth '%1%'. Gwiriwch fod y gosodiadau SMTP yn y ffeil Web.config, system.net/mailsettings yn gywir. - %0%.]]> - %0%.]]> -

Canlyniadau'r gwiriad Statws Iechyd Umbraco ar amserlen rhedwyd ar %0% am %1% fel y ganlyn:

%2%]]>
- Statws Iechyd Umbraco: %0% - Gwiriwch Pob Grŵp - Gwiriwch y grŵp - - Mae'r gwiriwr iechyd yn gwerthuso gwahanol rannau o'ch gwefan ar gyfer gosodiadau arfer gorau, cyfluniad, problemau posibl, ac ati. Gallwch chi drwsio problemau yn hawdd trwy wasgu botwm. - Gallwch chi ychwanegu eich gwiriadau iechyd eich hun, edrych ar y ddogfennaeth i gael mwy o wybodaeth am wiriadau iechyd arferu.

- ]]> -
- Eich wefan gallu defnyddio y protocol gwarchodaeth TLS 1.2 wrth wneud cysylltiadau allanol i endpoints HTTPS - Nid yw'ch gwefan wedi'i ffurfweddu i ganiatáu protocol diogelwch TLS 1.2 wrth wneud cysylltiadau allanol: efallai na fydd modd cyrchu rhai endpoints HTTPS gan ddefnyddio protocol llai diogel. - - - Analluogi olinydd URL - Galluogi olinydd URL - Diwylliant - URL gwreiddiol - Ailgyfeirwyd I - Gweinyddu Ailgyfeirio URLs - Mae'r URLs ganlynol yn ailgyfeirio at yr eitem gynnwys yma: - Dim ailgyfeiriadau wedi'u gwneud - Pan mae tudalen wedi'i gyhoeddi yn cael ei ailenwi neu symud bydd ailgyfeiriad yn cael ei greu yn awtomatig at y dudalen newydd. - Dileu - Ydych chi'n sicr eich bod eisiau dileu'r ailgyfeiriad o '%0%' at '%1%'? - URL ailgyfeirio wedi'i ddileu. - Gwall yn dileu'r URL. - Bydd hyn yn dileu'r ailgyfeiriad - Ydych chi'n sicr eich bod eisiau analluogi'r olinydd URL? - Mae'r olinydd URL wedi cael ei analluogi. - Gwall yn ystod analluogi'r olinydd URL, gall fwy o wybodaeth gael ei ddarganfod yn eich ffeil log. - Mae'r olinydd URL wedi cael ei alluogi. - Gwall yn ystod galluogi'r olinydd URL, gall fwy o wybodaeth gael ei ddarganfod yn eich ffeil log. - - - Dim eitemau Geiriadur i ddewis ohonynt - - - o nodau ar ôl - %1% gormod.]]> - - - Wedi chwalu cynnwys gyda Id: {0} yn berthnasol i gynnwys rhiant gwreiddiol gyda Id: {1} - Wedi chwalu cyfrwng gyda Id: {0} yn berthnasol i gyfrwng rhiant gwreiddiol gyda Id: {1} - Ni ellir adfer yr eitem yma yn awtomatig - Nid oes unrhyw leoliad lle gellir adfer yr eitem hon yn awtomatig. Gallwch chi symud yr eitem â llaw gan ddefnyddio'r goeden isod. - oedd adferwyd o dan - Does dim perthynas 'adfer' ar gael ar gyfer y nod yma. Defnyddiwch y ddewislen Symud i'w symud â llaw. - Mae'r eitem yr ydych eisiau adfer yr item oddi tan ('%0%') yn y bin ailgylchu. Defnyddiwch y ddewislen Symud i'w symud â llaw. - - - Cyfeiriad - Rhiant i plentyn - Deugyfeiriadol - Rhiant - Plentyn - Cyfrif - Cysylltiadau - Creu - Sylw - Enw - Dim cysylltiadau ar gyfer y math hwn o berthynas. - Math o Berthynas - Cysylltiadau - - - Dechrau Arni - Rheolaeth Ailgyfeirio URL - Cynnwys - Croeso - Rheolaeth Examine - Statws Cyhoeddedig - Adeiladwr Modelau - Gwiriad Iechyd - Proffilio - Dechrau Arni - Gosod Ffurflenni Umbraco - - - Mynd yn ôl - Cynllun gweithredol: - Neidio i - grŵp - pasio - rhybudd - methu - awgrym - Gwiriad wedi'i basio - Gwiriad wedi'i methu - Agor chwiliad swyddfa gefn - Agor/Cau cymorth swyddfa gefn - Agor/Cau eich opsiynau proffil - Sefydli Diwylliannau ac Enwau Gwesteia am %0% - Creu nod newydd o dan %0% - Sefydli Mynediad Cyhoeddus ar %0% - Sefydli Caniataid ar %0% - Newid y trefniad am %0% - Creu templed cynnwys yn seiliedig ar %0% - Agor dewislen cyd-destun ar gyfer - Iaith gyfredol - Newid iaith i - Creu ffolder newydd - Golwg Rhannol - Macro Golwg Rhannol - Aelod - Math o ddata - Chwilio'r dangosfwrdd ailgyfeirio - Chwilio'r adran grŵp defnyddwyr - Chwilio'r adran defnyddwyr - Creu eitem - Creu - Golygu - Enw - Ychwanegu rhes newydd - Gweld mwy o opsiynau - Wedi cyfieithu - Cyfieithiad ar goll - Eitemau geiriadur - - - Cyfeiriadau - This Data Type has no references. Nid oes gan y Math o Ddata hwn unrhyw gyferiadau. - Defnyddir mewn Mathau o Ddogfennau - Ddim cyfeiriadau i Fathau o Ddogfennau. - Defnyddir mewm Mathau o Gyfrwng - Ddim cyfeiriadau i Fathau o Gyfrwng. - Defnyddir mewn Mathau o Aelod - Ddim cyfeiriadau i Fathau o Aelod. - Defnyddir gan - A ddefnyddir yn Ddogfennau - A ddefnyddir yn Aelodau - A ddefnyddir yn Cyfryngau - - - Dileu Chwiliad Cadwedig - Lefelau Log - Chwiliadau Cadwedig - Arbed Chwiliad - Rhoi enw cyfeillgar am eich ymholiad chwilio - Hidlo Chwiliad - Cyfanswm o Eitemau - Stamp Amser - Lefel - Peiriant - Neges - Eithriad - Priodweddau - Chwilio efo Google - Chwiliwch y neges hon efo Google - Chwilio efo Bing - Chwiliwch y neges hon efo Bing - Chwilio Our Umbraco - Chwiliwch y neges hon arno Our Umbraco fforymau a dogfennau - Chwilio Our Umbraco efo Google - Chwilio Our Umbraco fforymau efo Google - Chwilio'r cod gwreiddiol Umbraco - Chwilio tu fewn y cod gwreiddiol Umbraco ar Github - Chwilio Problemau Umbraco - Chwilio Problemau Umbraco ar Github - Dileu chwiliad hon - Darganfod logiau efo ID y Cais - Darganfod logiau efo Namespace - Darganfod logiau efo Enw Peiriant - Agor - - - Copi %0% - %0% o %1% - Dileu pob eitem - Clirio y clipfwrdd - - - Agor Gweithredoedd Priodweddau - Cau Gweithredoedd Priodweddau - - - Aros - Adnewyddu statws - Cuddstôr Cof - - - - Ail-lwytho - Cuddstôr Cronfa Ddata - - Gall ailadeiladu fod yn ddrud. - Defnyddio fo pan mae ail-lwytho ddim yn ddigon, a ti'n feddwl mai'r stôr cronfa ddata heb gael ei - chynhyrchu'n iawn—a fyddai'n arwydd o broblem gritigol efo Umbraco. - ]]> - - Ailadeiladu - Mewnol - - nad oes angeni chi ei defnyddio. - ]]> - - Casglu - Statws Cuddstôr Cyhoeddedig - Cuddstorau - - - Proffilio perfformiad - - - Mae Umbraco yn rhedeg mewn modd dadfygio. Mae hyn yn golygu y gallwch chi ddefnyddio'r proffiliwr perfformiad adeiledig i asesu'r perfformiad wrth rendro tudalennau. -

-

- OS ti eisiau actifadu'r proffiliwr am rendro tudalen penodol, bydd angen ychwanegu umbDebug=true i'r ymholiad wrth geisio am y tudalen -

-

- Os ydych chi am i'r proffiliwr gael ei actifadu yn ddiofyn am bob rendrad tudalen, gallwch chi ddefnyddio'r togl isod. - Bydd e'n gosod cwci yn eich porwr, sydd wedyn yn actifadu'r proffiliwr yn awtomatig. - Mewn geiriau eraill, bydd y proffiliwr dim ond yn actif yn ddiofyn yn eich porwr chi - nid porwr pawb eraill. -

- ]]> -
- Actifadu y proffiliwr yn ddiofyn - Nodyn atgoffa cyfeillgar - - - Ni ddylech chi fyth adael i safle cynhyrchu redeg yn y modd dadfygio. Mae'r modd dadfygio yn gallu cael ei diffodd trwy ychwanegu'r gosodiad debug="false" ar yr elfen <grynhoi /> yn web.config. -

- ]]> -
- - - Mae Umbraco ddim yn rhedeg mewn modd dadfygio ar hyn o bryd, felly nid allwch chi ddefnyddio'r proffiliwer adeiledig. Dyma sut y dylai fod ar gyfer safle cynhyrchu. -

-

- Mae'r modd dadfygio yn gallu cael ei throi arno gan ychwanegu'r gosodiad debug="true" ar yr elfen <grynhoi /> yn web.config. -

- ]]> -
- - - Oriau o fideos hyfforddiant Umbraco ddim ond un clic i fwrdd - - Eisiau meistroli Umbraco? Treuliwch gwpl o funudau yn dysgu rhai o'r arferion gorau gan wylio un o'r fideos hyn am sut i ddefnyddio Umbraco. Ac ymweld â umbraco.tv am fwy o fideos am Umbraco

- ]]> -
- I roi cychwyn i chi - - - Dechrau yma - Mae'r adran hon yn cynnwys y blociau adeiladu am eich safle Umbraco. Dilyn y dolenni isod i ddarganfod fwy am weithio gyda'r eitemau yn yr adran Gosodiadau - Ddarganfod fwy - - fewn yr adran Dogfennaeth o Our Umbraco - ]]> - - - Fforwm Cymunedol - ]]> - - - fideos tiwtorial (mae rhai am ddim, ond bydd angen tanysgrifiad am rhai eraill) - ]]> - - - hoffer hybu cynhyrchiant a chefnogaeth fasnachol - ]]> - - - hyfforddi ac ardystio - ]]> - - - - Croeso i'r SRC cyfeillgar - Diolch am ddewis Umbraco - rydyn ni'n credu y gallai hyn fod dechreuad i rywbeth prydferth. Er y gallai deilo'n llethol ar y dechrau, rydym wedi gwneud llawer i wneud y gromlin ddysgu mor llyfn a chyflym a phosib. - - - Ffurflenni Umbraco - Creu ffurflenni gan ddefnyddio rhyngwyneb llusgo a gollwng sythweledol. O ffurflenni cyswllt syml sy'n anfon e-byst, i holiaduron mwy datblygedig sy'n integreiddio efo systemau CRM. Bydd eich cleientiaid wrth ei modd! - - - Creu bloc newydd - Atodwch adran gosodiadau - Dewis golygfa - Dewis taflen arddull - Dewis delwedd bawd - Creu newydd - Taflen arddull arferu - Ychwanegu taflen arddull - Ymddangosiad y golygydd - Modelau data - Ymddangosiad y catalog - Lliw cefndir - Lliw eicon - Model Cynnwys - Label - Golygfa arferu - Ddangos disgrifiad golygfa arferu - Trosysgrifo sut mae'r bloc hwn yn ymddangos yn yr UI y swyddfa gefn. Dewis ffeil .html sy'n cynnwys eich cyflwyniad. - Model gosodiadau - Maint y golygydd troshaen - Ychwanegu golygfa arferu - Ychwanegu gosodiadau - Trosysgrifo templed label - %0%.]]> - %0%.]]> - Bydd cynnwys y bloc hwn yn dal i fod yn bresennol, ni fydd golygu'r cynnwys hwn ar gael mwyach a bydd yn cael ei ddangos fel cynnwys heb gefnogaeth. - - Delwedd bawd - Ychwanegu delwedd bawd - Creu gwag - Clipfwrdd - Gosodiadau - Datblygedig - Gorfodi cuddio'r golygydd cynnwys - Rydych chi wedi gwneud newidiadau i'r cynnwys hwn. Wyt ti'n siŵr eich bod chi am eu taflu ei fwrdd? - Gwaredu cread? - - Priodwedd '%0%' yn defnyddio'r golygydd '%1%' sydd ddim yn cael ei gefnogi mewn blociau. - - - Beth yw Templedi Gynnwys - Mae Templedi Gynnwys yn gynnwys cyn-diffiniedig sydd yn gallu cael ei ddewis wrth greu nod cynnwys newydd. - Sut ydw i'n creu Templed Gynnwys? - - Mae yna ddwy ffordd i greu Templed Gynnwys:

-
    -
  • Gliciwch-de ar nod cynnwys a dewis "Creu Templed Gynnwys" i greu Templed Gynnwys newydd.
  • -
  • Gliciwch-de ar y goeden Templedi Gynnwys yn yr adran Gosodiadau a dewis y Math of Dogfen ti eisiau creu Templed Gynnwys am.
  • -
-

Unwaith y rhoddir enw, gall golygyddion ddechrau defnyddio'r Templed Gynnwys fel sylfaen am ei thudalen newydd.

- ]]> -
- Sut ydw i'n rheoli Templedi Gynnwys - Gallwch chi olygu a dileu Templedi Gynnwys o'r goeden "Templedi Gynnwys" yn yr adran Gosodiadau. Ehangwch y Math o Ddogfen mae'r Templed Gynnwys yn seiliedig arno a chlicio fo i'w golygu neu ddileu. - - diff --git a/src/Umbraco.Web.UI/Umbraco/js/UmbracoSpeechBubbleBackEnd.js b/src/Umbraco.Web.UI/Umbraco/js/UmbracoSpeechBubbleBackEnd.js deleted file mode 100644 index f738538f90..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/js/UmbracoSpeechBubbleBackEnd.js +++ /dev/null @@ -1,68 +0,0 @@ -// Umbraco SpeechBubble Javascript - -// Dependency Loader Constructor -function UmbracoSpeechBubble(id) { - this.id = id; - this.ie = document.all ? true : false; - - this.GenerateSpeechBubble(); -} - -UmbracoSpeechBubble.prototype.GenerateSpeechBubble = function () { - - var sbHtml = document.getElementById(this.id); - - sbHtml.innerHTML = '' + - '
' + - '
' + - '' + - ' Close' + - '

The header!

' + - '

Default Text Container!


' + - '
' + - '
'; -}; - -UmbracoSpeechBubble.prototype.ShowMessage = function (icon, header, message, dontAutoHide) { - var speechBubble = jQuery("#" + this.id); - jQuery("#" + this.id + "Header").html(header); - jQuery("#" + this.id + "Message").html(message); - jQuery("#" + this.id + "Icon").attr('src', 'images/speechBubble/' + icon + '.png'); - - if (!this.ie) { - if (!dontAutoHide) { - jQuery("#" + this.id).fadeIn("slow").animate({ opacity: 1.0 }, 5000).fadeOut("fast"); - } else { - jQuery(".speechClose").show(); - jQuery("#" + this.id).fadeIn("slow"); - } - } else { - // this is special for IE as it handles fades with pngs very ugly - jQuery("#" + this.id).show(); - if (!dontAutoHide) { - setTimeout('UmbSpeechBubble.Hide();', 5000); - } else { - jQuery(".speechClose").show(); - } - } -}; - -UmbracoSpeechBubble.prototype.Hide = function () { - if (!this.ie) { - jQuery("#" + this.id).fadeOut("slow"); - } else { - jQuery("#" + this.id).hide(); - } -}; - -// Initialize -var UmbSpeechBubble = null -function InitUmbracoSpeechBubble() { - if (UmbSpeechBubble == null) - UmbSpeechBubble = new UmbracoSpeechBubble("defaultSpeechbubble"); -} - -jQuery(document).ready(function() { - InitUmbracoSpeechBubble(); -}); diff --git a/src/Umbraco.Web.UI/Umbraco/js/dualSelectBox.js b/src/Umbraco.Web.UI/Umbraco/js/dualSelectBox.js deleted file mode 100644 index 5409e662e2..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/js/dualSelectBox.js +++ /dev/null @@ -1,48 +0,0 @@ - -function dualSelectBoxShift(id) { - var posVal = document.getElementById(id + "_posVals"); - var selVal = document.getElementById(id + "_selVals"); - - // First check the possible items - for (var i=0;i