From a9147f14730c1a835f4b7949d9654cc698bb5423 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 20 Mar 2018 18:21:37 +0100 Subject: [PATCH] Port v7@2aa0dfb2c5 - WIP --- src/Umbraco.Core/Composing/TypeFinder.cs | 52 ++++++++- src/Umbraco.Core/Composing/TypeLoader.cs | 106 +++++++++++------- .../ClientDependencyConfiguration.cs | 68 +++++++++++ .../Configuration/ContentXmlStorage.cs | 9 -- .../Configuration/GlobalSettings.cs | 26 ++--- .../Configuration/LocalTempStorage.cs | 9 ++ .../UmbracoSettings/BackOfficeElement.cs | 18 +++ .../UmbracoSettings/IBackOfficeSection.cs | 7 ++ .../UmbracoSettings/ITourSection.cs | 7 ++ .../IUmbracoSettingsSection.cs | 2 + .../UmbracoSettings/TourConfigElement.cs | 17 +++ .../UmbracoSettings/UmbracoSettingsSection.cs | 11 ++ src/Umbraco.Core/Constants-Applications.cs | 5 + src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + src/Umbraco.Core/Constants-Security.cs | 1 + src/Umbraco.Core/Constants-System.cs | 36 ++++++ src/Umbraco.Core/DateTimeExtensions.cs | 4 +- src/Umbraco.Core/DisposableObject.cs | 3 + src/Umbraco.Core/DisposableObjectSlim.cs | 53 +++++++++ src/Umbraco.Core/DisposableTimer.cs | 2 +- src/Umbraco.Core/HashGenerator.cs | 2 +- src/Umbraco.Core/HttpContextExtensions.cs | 4 +- src/Umbraco.Core/IO/IOHelper.cs | 12 ++ src/Umbraco.Core/IO/PhysicalFileSystem.cs | 40 ++++++- src/Umbraco.Core/IO/SystemFiles.cs | 10 +- src/Umbraco.Core/Manifest/ManifestWatcher.cs | 2 +- src/Umbraco.Core/Scoping/ScopeContext.cs | 24 +++- .../Strings/DefaultShortStringHelper.cs | 18 ++- src/Umbraco.Core/UdiEntityType.cs | 9 +- src/Umbraco.Core/Umbraco.Core.csproj | 7 +- src/Umbraco.Core/UriExtensions.cs | 17 ++- 31 files changed, 479 insertions(+), 107 deletions(-) delete mode 100644 src/Umbraco.Core/Configuration/ContentXmlStorage.cs create mode 100644 src/Umbraco.Core/Configuration/LocalTempStorage.cs create mode 100644 src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs create mode 100644 src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs create mode 100644 src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs create mode 100644 src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs create mode 100644 src/Umbraco.Core/DisposableObjectSlim.cs diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index ca7c642453..f244d1d1ce 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.IO; using System.Linq; using System.Reflection; @@ -21,6 +22,38 @@ namespace Umbraco.Core.Composing { private static volatile HashSet _localFilteredAssemblyCache; private static readonly object LocalFilteredAssemblyCacheLocker = new object(); + private static readonly List NotifiedLoadExceptionAssemblies = new List(); + private static string[] _assembliesAcceptingLoadExceptions; + + private static string[] AssembliesAcceptingLoadExceptions + { + get + { + if (_assembliesAcceptingLoadExceptions != null) + return _assembliesAcceptingLoadExceptions; + + var s = ConfigurationManager.AppSettings["Umbraco.AssembliesAcceptingLoadExceptions"]; + return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) + ? Array.Empty() + : s.Split(',').Select(x => x.Trim()).ToArray(); + } + } + + private static bool AcceptsLoadExceptions(Assembly a) + { + if (AssembliesAcceptingLoadExceptions.Length == 0) + return false; + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") + return true; + 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 + return name.StartsWith(pattern); // and name must start with pattern + }); + } /// /// lazily load a reference to all assemblies and only local assemblies. @@ -45,7 +78,7 @@ namespace Umbraco.Core.Composing HashSet assemblies = null; try { - var isHosted = HttpContext.Current != null || HostingEnvironment.IsHosted; + var isHosted = IOHelper.IsHosted; try { @@ -529,8 +562,21 @@ namespace Umbraco.Core.Composing foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) AppendLoaderException(sb, loaderException); - // rethrow with new message - throw new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + + // rethrow with new message, unless accepted + if (AcceptsLoadExceptions(a) == false) throw ex; + + // log a warning, and return what we can + lock (NotifiedLoadExceptionAssemblies) + { + if (NotifiedLoadExceptionAssemblies.Contains(a.FullName) == false) + { + NotifiedLoadExceptionAssemblies.Add(a.FullName); + Current.Logger.Warn(typeof (TypeFinder), ex, $"Could not load all types from {a.GetName().Name}."); + } + } + return rex.Types.WhereNotNull().ToArray(); } } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 02c69ceec8..1e62c68c4b 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using System.Web; using System.Web.Compilation; using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using File = System.IO.File; @@ -28,11 +30,13 @@ namespace Umbraco.Core.Composing private readonly IRuntimeCacheProvider _runtimeCache; private readonly ProfilingLogger _logger; - private readonly string _tempFolder; private readonly object _typesLock = new object(); private readonly Dictionary _types = new Dictionary(); + private readonly Lazy _typesListFilePath = new Lazy(GetTypesListFilePath); + private readonly Lazy _typesHashFilePath = new Lazy(GetTypesHashFilePath); + private string _cachedAssembliesHash; private string _currentAssembliesHash; private IEnumerable _assemblies; @@ -49,13 +53,6 @@ namespace Umbraco.Core.Composing _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - // the temp folder where the cache file lives - _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); - if (Directory.Exists(_tempFolder) == false) - Directory.CreateDirectory(_tempFolder); - - var typesListFile = GeTypesListFilePath(); - if (detectChanges) { //first check if the cached hash is string.Empty, if it is then we need @@ -67,7 +64,8 @@ namespace Umbraco.Core.Composing // 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 - File.Delete(typesListFile); + if (File.Exists(TypesListFilePath)) + File.Delete(TypesListFilePath); WriteCacheTypesHash(); } @@ -77,13 +75,24 @@ namespace Umbraco.Core.Composing // 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 - File.Delete(typesListFile); + if (File.Exists(TypesListFilePath)) + File.Delete(TypesListFilePath); // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; } } + /// + /// Gets the plugin list file path. + /// + private string TypesListFilePath => _typesListFilePath.Value; + + /// + /// Gets the plugin hash file path. + /// + private string TypesHashFilePath => _typesHashFilePath.Value; + /// /// Gets or sets the set of assemblies to scan. /// @@ -135,10 +144,9 @@ namespace Umbraco.Core.Composing if (_cachedAssembliesHash != null) return _cachedAssembliesHash; - var filePath = GetTypesHashFilePath(); - if (File.Exists(filePath) == false) return string.Empty; + if (!File.Exists(TypesHashFilePath)) return string.Empty; - var hash = File.ReadAllText(filePath, Encoding.UTF8); + var hash = File.ReadAllText(TypesHashFilePath, Encoding.UTF8); _cachedAssembliesHash = hash; return _cachedAssembliesHash; @@ -177,8 +185,7 @@ namespace Umbraco.Core.Composing /// private void WriteCacheTypesHash() { - var filePath = GetTypesHashFilePath(); - File.WriteAllText(filePath, CurrentAssembliesHash, Encoding.UTF8); + File.WriteAllText(TypesHashFilePath, CurrentAssembliesHash, Encoding.UTF8); } /// @@ -295,8 +302,7 @@ namespace Umbraco.Core.Composing { try { - var filePath = GeTypesListFilePath(); - File.Delete(filePath); + File.Delete(TypesListFilePath); } catch { @@ -312,11 +318,10 @@ namespace Umbraco.Core.Composing { var cache = new Dictionary, IEnumerable>(); - var filePath = GeTypesListFilePath(); - if (File.Exists(filePath) == false) + if (File.Exists(TypesListFilePath) == false) return cache; - using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) + using (var stream = GetFileStream(TypesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) { while (true) @@ -354,28 +359,47 @@ namespace Umbraco.Core.Composing } // internal for tests - internal string GeTypesListFilePath() - { - var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".list"; - return Path.Combine(_tempFolder, filename); - } + internal static string GetTypesListFilePath() => GetFilePath("list"); - private string GetTypesHashFilePath() + private static string GetTypesHashFilePath() => GetFilePath("hash"); + + private static string GetFilePath(string extension) { - var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".hash"; - return Path.Combine(_tempFolder, filename); + string path; + switch (GlobalSettings.LocalTempStorageLocation) + { + case LocalTempStorage.AspNetTemp: + path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types." + extension); + break; + case LocalTempStorage.EnvironmentTemp: + // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash); + path = Path.Combine(cachePath, "umbraco-types." + extension); + break; + case LocalTempStorage.Default: + default: + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); + path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName + "." + extension); + break; + } + + // ensure that the folder exists + var directory = Path.GetDirectoryName(path); + if (directory == null) + throw new InvalidOperationException($"Could not determine folder for file \"{path}\"."); + if (Directory.Exists(directory) == false) + Directory.CreateDirectory(directory); + + return path; } // internal for tests internal void WriteCache() { - // be absolutely sure - if (Directory.Exists(_tempFolder) == false) - Directory.CreateDirectory(_tempFolder); - - var filePath = GeTypesListFilePath(); - - using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) + using (var stream = GetFileStream(TypesListFilePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) using (var writer = new StreamWriter(stream)) { foreach (var typeList in _types.Values) @@ -405,13 +429,11 @@ namespace Umbraco.Core.Composing /// Generally only used for resetting cache, for example during the install process. public void ClearTypesCache() { - var path = GeTypesListFilePath(); - if (File.Exists(path)) - File.Delete(path); + if (File.Exists(TypesListFilePath)) + File.Delete(TypesListFilePath); - path = GetTypesHashFilePath(); - if (File.Exists(path)) - File.Delete(path); + if (File.Exists(TypesHashFilePath)) + File.Delete(TypesHashFilePath); _runtimeCache.ClearCacheItem(CacheKey); } @@ -589,7 +611,7 @@ namespace Umbraco.Core.Composing // else proceed, typeList = new TypeList(baseType, attributeType); - var scan = RequiresRescanning || File.Exists(GeTypesListFilePath()) == false; + var scan = RequiresRescanning || File.Exists(TypesListFilePath) == false; if (scan) { diff --git a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs index c85df0be73..8693f2e6e8 100644 --- a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; using System.Web; using System.Xml.Linq; using ClientDependency.Core.CompositeFiles.Providers; using ClientDependency.Core.Config; +using Semver; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -24,10 +28,74 @@ namespace Umbraco.Core.Configuration _logger = logger; _fileName = IOHelper.MapPath(string.Format("{0}/ClientDependency.config", SystemDirectories.Config)); } + + /// + /// Changes the version number in ClientDependency.config to a hashed value for the version and the DateTime.Day + /// + /// The version of Umbraco we're upgrading to + /// A date value to use in the hash to prevent this method from updating the version on each startup + /// Allows the developer to specify the date precision for the hash (i.e. "yyyyMMdd" would be a precision for the day) + /// Boolean to indicate succesful update of the ClientDependency.config file + public bool UpdateVersionNumber(SemVersion version, DateTime date, string dateFormat) + { + var byteContents = Encoding.Unicode.GetBytes(version + date.ToString(dateFormat)); + + //This is a way to convert a string to a long + //see https://www.codeproject.com/Articles/34309/Convert-String-to-bit-Integer + //We could much more easily use MD5 which would create us an INT but since that is not compliant with + //hashing standards we have to use SHA + int intHash; + using (var hash = SHA256.Create()) + { + var bytes = hash.ComputeHash(byteContents); + + var longResult = new[] { 0, 8, 16, 24 } + .Select(i => BitConverter.ToInt64(bytes, i)) + .Aggregate((x, y) => x ^ y); + + //CDF requires an INT, and although this isn't fail safe, it will work for our purposes. We are not hashing for crypto purposes + //so there could be some collisions with this conversion but it's not a problem for our purposes + //It's also important to note that the long.GetHashCode() implementation in .NET is this: return (int) this ^ (int) (this >> 32); + //which means that this value will not change per appdomain like some GetHashCode implementations. + intHash = longResult.GetHashCode(); + } + + try + { + var clientDependencyConfigXml = XDocument.Load(_fileName, LoadOptions.PreserveWhitespace); + if (clientDependencyConfigXml.Root != null) + { + + var versionAttribute = clientDependencyConfigXml.Root.Attribute("version"); + + //Set the new version to the hashcode of now + var oldVersion = versionAttribute.Value; + var newVersion = Math.Abs(intHash).ToString(); + + //don't update if it's the same version + if (oldVersion == newVersion) + return false; + + versionAttribute.SetValue(newVersion); + clientDependencyConfigXml.Save(_fileName, SaveOptions.DisableFormatting); + + _logger.Info(string.Format("Updated version number from {0} to {1}", oldVersion, newVersion)); + return true; + } + } + catch (Exception ex) + { + _logger.Error("Couldn't update ClientDependency version number", ex); + } + + return false; + } /// /// Changes the version number in ClientDependency.config to a random value to avoid stale caches /// + /// + [Obsolete("Use the UpdateVersionNumber method specifying the version, date and dateFormat instead")] public bool IncreaseVersionNumber() { try diff --git a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs deleted file mode 100644 index f81ca8f8cc..0000000000 --- a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Core.Configuration -{ - internal enum ContentXmlStorage - { - Default, - AspNetTemp, - EnvironmentTemp - } -} diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 1ef1b1645b..0ed7289b30 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -470,24 +470,24 @@ namespace Umbraco.Core.Configuration internal static bool ContentCacheXmlStoredInCodeGen { - get { return ContentCacheXmlStorageLocation == ContentXmlStorage.AspNetTemp; } + get { return LocalTempStorageLocation == LocalTempStorage.AspNetTemp; } } - internal static ContentXmlStorage ContentCacheXmlStorageLocation + /// + /// This is the location type to store temporary files such as cache files or other localized files for a given machine + /// + /// + /// Currently used for the xml cache file and the plugin cache files + /// + internal static LocalTempStorage LocalTempStorageLocation { get { - if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLStorage")) - { - return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]); - } - if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp")) - { - return bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]) - ? ContentXmlStorage.AspNetTemp - : ContentXmlStorage.Default; - } - return ContentXmlStorage.Default; + var setting = ConfigurationManager.AppSettings.ContainsKey("umbracoLocalTempStorage"); + if (!string.IsNullOrWhiteSpace(setting)) + return Enum.Parse(setting); + + return LocalTempStorage.Default; } } diff --git a/src/Umbraco.Core/Configuration/LocalTempStorage.cs b/src/Umbraco.Core/Configuration/LocalTempStorage.cs new file mode 100644 index 0000000000..d41f7d1925 --- /dev/null +++ b/src/Umbraco.Core/Configuration/LocalTempStorage.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Configuration +{ + internal enum LocalTempStorage + { + Default, + AspNetTemp, + EnvironmentTemp + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs new file mode 100644 index 0000000000..d1d2a26a96 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs @@ -0,0 +1,18 @@ +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + internal class BackOfficeElement : UmbracoConfigurationElement, IBackOfficeSection + { + [ConfigurationProperty("tours")] + internal TourConfigElement Tours + { + get { return (TourConfigElement)this["tours"]; } + } + + ITourSection IBackOfficeSection.Tours + { + get { return Tours; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs new file mode 100644 index 0000000000..36dd6a22ed --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + public interface IBackOfficeSection + { + ITourSection Tours { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs new file mode 100644 index 0000000000..938642521e --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + public interface ITourSection + { + bool EnableTours { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs index 0801c9933f..085a826626 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs @@ -5,6 +5,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { public interface IUmbracoSettingsSection : IUmbracoConfigurationSection { + IBackOfficeSection BackOffice { get; } + IContentSection Content { get; } ISecuritySection Security { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs new file mode 100644 index 0000000000..ebb649ca3b --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs @@ -0,0 +1,17 @@ +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + internal class TourConfigElement : UmbracoConfigurationElement, ITourSection + { + //disabled by default so that upgraders don't get it enabled by default + //TODO: we probably just want to disable the initial one from automatically loading ? + [ConfigurationProperty("enable", DefaultValue = false)] + public bool EnableTours + { + get { return (bool)this["enable"]; } + } + + //TODO: We could have additional filters, etc... defined here + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs index 49a791144b..0cf97b2560 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs @@ -8,6 +8,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings public class UmbracoSettingsSection : ConfigurationSection, IUmbracoSettingsSection { + [ConfigurationProperty("backOffice")] + internal BackOfficeElement BackOffice + { + get { return (BackOfficeElement)this["backOffice"]; } + } + [ConfigurationProperty("content")] internal ContentElement Content { @@ -132,6 +138,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return Templates; } } + IBackOfficeSection IUmbracoSettingsSection.BackOffice + { + get { return BackOffice; } + } + IDeveloperSection IUmbracoSettingsSection.Developer { get { return Developer; } diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index 014c4af450..d401eaee88 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -73,6 +73,11 @@ /// public const string Media = "media"; + /// + /// alias for the macro tree. + /// + public const string Macros = "macros"; + /// /// alias for the datatype tree. /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index f6ce98901d..43bd381571 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -63,6 +63,11 @@ namespace Umbraco.Core /// DropDown List Multiple, Publish Keys. /// public const string DropdownlistMultiplePublishKeys = "Umbraco.DropdownlistMultiplePublishKeys"; + + /// + /// DropDown List. + /// + public const string DropDownListFlexible = "Umbraco.DropDown.Flexible"; /// /// Folder Browser. diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 2e9de0d8a4..4c24febb22 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -13,6 +13,7 @@ namespace Umbraco.Core public const int SuperId = -1; public const string AdminGroupAlias = "admin"; + public const string SensitiveDataGroupAlias = "sensitiveData"; public const string TranslatorGroupAlias = "translator"; public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 8ab2f74f4c..9fad62c347 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -12,16 +12,52 @@ /// public const int Root = -1; + /// + /// The string identifier for global system root node. + /// + /// Use this instead of re-creating the string everywhere. + public const string RootString = "-1"; + /// /// The integer identifier for content's recycle bin. /// public const int RecycleBinContent = -20; + /// + /// The string identifier for content's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinContentString = "-20"; + + /// + /// The string path prefix of the content's recycle bin. + /// + /// + /// Everything that is in the content recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinContentPathPrefix = "-1,-20,"; + /// /// The integer identifier for media's recycle bin. /// public const int RecycleBinMedia = -21; + /// + /// The string identifier for media's recycle bin. + /// + /// Use this instead of re-creating the string everywhere. + public const string RecycleBinMediaString = "-21"; + + /// + /// The string path prefix of the media's recycle bin. + /// + /// + /// Everything that is in the media recycle bin, has a path that starts with the prefix. + /// Use this instead of re-creating the string everywhere. + /// + public const string RecycleBinMediaPathPrefix = "-1,-21,"; + public const string UmbracoConnectionName = "umbracoDbDSN"; public const string UmbracoUpgradePlanName = "Umbraco.Core"; } diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index d82ec99c6a..b3babd2d07 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -21,9 +21,9 @@ namespace Umbraco.Core public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) { if (truncateTo == DateTruncate.Year) - return new DateTime(dt.Year, 0, 0); + return new DateTime(dt.Year, 1, 1); if (truncateTo == DateTruncate.Month) - return new DateTime(dt.Year, dt.Month, 0); + return new DateTime(dt.Year, dt.Month, 1); if (truncateTo == DateTruncate.Day) return new DateTime(dt.Year, dt.Month, dt.Day); if (truncateTo == DateTruncate.Hour) diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index 956804e347..ecdc149f6e 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -6,6 +6,9 @@ namespace Umbraco.Core /// Abstract implementation of IDisposable. /// /// + /// This is for objects that DO have unmanaged resources. Use + /// for objects that do NOT have unmanaged resources, and avoid creating a finalizer. + /// /// Can also be used as a pattern for when inheriting is not possible. /// /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs new file mode 100644 index 0000000000..4992f8bc0f --- /dev/null +++ b/src/Umbraco.Core/DisposableObjectSlim.cs @@ -0,0 +1,53 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Abstract implementation of managed IDisposable. + /// + /// + /// This is for objects that do NOT have unmanaged resources. Use + /// for objects that DO have unmanaged resources and need to deal with them when disposing. + /// + /// Can also be used as a pattern for when inheriting is not possible. + /// + /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx + /// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ + /// + /// Note: if an object's ctor throws, it will never be disposed, and so if that ctor + /// has allocated disposable objects, it should take care of disposing them. + /// + public abstract class DisposableObjectSlim : IDisposable + { + private readonly object _locko = new object(); + + // gets a value indicating whether this instance is disposed. + // for internal tests only (not thread safe) + public bool Disposed { get; private set; } + + // implements IDisposable + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + // can happen if the object construction failed + if (_locko == null) + return; + + lock (_locko) + { + if (Disposed) return; + Disposed = true; + } + + if (disposing) + DisposeResources(); + } + + protected virtual void DisposeResources() { } + } +} diff --git a/src/Umbraco.Core/DisposableTimer.cs b/src/Umbraco.Core/DisposableTimer.cs index 819e86f8e1..6ded588be6 100644 --- a/src/Umbraco.Core/DisposableTimer.cs +++ b/src/Umbraco.Core/DisposableTimer.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core /// /// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a using (C#) statement. /// - public class DisposableTimer : DisposableObject + public class DisposableTimer : DisposableObjectSlim { private readonly ILogger _logger; private readonly LogType? _logType; diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 0f03e22a9b..f8e45c362d 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core /// This will use the crypto libs to generate the hash and will try to ensure that /// strings, etc... are not re-allocated so it's not consuming much memory. /// - internal class HashGenerator : DisposableObject + internal class HashGenerator : DisposableObjectSlim { public HashGenerator() { diff --git a/src/Umbraco.Core/HttpContextExtensions.cs b/src/Umbraco.Core/HttpContextExtensions.cs index df134ad7ca..e370b055a4 100644 --- a/src/Umbraco.Core/HttpContextExtensions.cs +++ b/src/Umbraco.Core/HttpContextExtensions.cs @@ -40,13 +40,13 @@ namespace Umbraco.Core var ipAddress = httpContext.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; if (string.IsNullOrEmpty(ipAddress)) - return httpContext.Request.ServerVariables["REMOTE_ADDR"]; + return httpContext.Request.UserHostAddress; var addresses = ipAddress.Split(','); if (addresses.Length != 0) return addresses[0]; - return httpContext.Request.ServerVariables["REMOTE_ADDR"]; + return httpContext.Request.UserHostAddress; } catch (System.Exception ex) { diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 94d2ccfe7c..5a2928f795 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -13,11 +13,22 @@ namespace Umbraco.Core.IO { public static class IOHelper { + /// + /// Gets or sets a value forcing Umbraco to consider it is non-hosted. + /// + /// This should always be false, unless unit testing. + public static bool ForceNotHosted { get; set; } + private static string _rootDir = ""; // static compiled regex for faster performance //private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + /// + /// Gets a value indicating whether Umbraco is hosted. + /// + public static bool IsHosted => !ForceNotHosted && (HttpContext.Current != null || HostingEnvironment.IsHosted); + public static char DirSepChar => Path.DirectorySeparatorChar; internal static void UnZip(string zipFilePath, string unPackDirectory, bool deleteZipFile) @@ -80,6 +91,7 @@ namespace Umbraco.Core.IO public static string MapPath(string path, bool useHttpContext) { if (path == null) throw new ArgumentNullException("path"); + useHttpContext = useHttpContext && IsHosted; // Check if the path is already mapped if ((path.Length >= 2 && path[1] == Path.VolumeSeparatorChar) diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 3b9cac42a7..9a2c6eb1de 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; +using System.Threading; using Umbraco.Core.Logging; namespace Umbraco.Core.IO @@ -104,7 +105,7 @@ namespace Umbraco.Core.IO try { - Directory.Delete(fullPath, recursive); + WithRetry(() => Directory.Delete(fullPath, recursive)); } catch (DirectoryNotFoundException ex) { @@ -221,7 +222,7 @@ namespace Umbraco.Core.IO try { - File.Delete(fullPath); + WithRetry(() => File.Delete(fullPath)); } catch (FileNotFoundException ex) { @@ -371,7 +372,7 @@ namespace Umbraco.Core.IO { if (overrideIfExists == false) throw new InvalidOperationException($"A file at path '{path}' already exists"); - File.Delete(fullPath); + WithRetry(() => File.Delete(fullPath)); } var directory = Path.GetDirectoryName(fullPath); @@ -379,9 +380,9 @@ namespace Umbraco.Core.IO Directory.CreateDirectory(directory); // ensure it exists if (copy) - File.Copy(physicalPath, fullPath); + WithRetry(() => File.Copy(physicalPath, fullPath)); else - File.Move(physicalPath, fullPath); + WithRetry(() => File.Move(physicalPath, fullPath)); } #region Helper Methods @@ -410,6 +411,35 @@ namespace Umbraco.Core.IO return path; } + protected void WithRetry(Action action) + { + // 10 times 100ms is 1s + const int count = 10; + const int pausems = 100; + + for (var i = 0;; i++) + { + try + { + action(); + break; // done + } + catch (IOException e) + { + // if it's not *exactly* IOException then it could be + // some inherited exception such as FileNotFoundException, + // and then we don't want to retry + if (e.GetType() != typeof(IOException)) throw; + + // if we have tried enough, throw, else swallow + // the exception and retry after a pause + if (i == count) throw; + } + + Thread.Sleep(pausems); + } + } + #endregion } } diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs index fa22a9a447..20b7bf6a3e 100644 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ b/src/Umbraco.Core/IO/SystemFiles.cs @@ -22,19 +22,19 @@ namespace Umbraco.Core.IO { get { - switch (GlobalSettings.ContentCacheXmlStorageLocation) + switch (GlobalSettings.LocalTempStorageLocation) { - case ContentXmlStorage.AspNetTemp: + case LocalTempStorage.AspNetTemp: return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); - case ContentXmlStorage.EnvironmentTemp: + case LocalTempStorage.EnvironmentTemp: var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); - var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoXml", + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not // utilizing an old path appDomainHash); return Path.Combine(cachePath, "umbraco.config"); - case ContentXmlStorage.Default: + case LocalTempStorage.Default: return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); default: throw new ArgumentOutOfRangeException(); diff --git a/src/Umbraco.Core/Manifest/ManifestWatcher.cs b/src/Umbraco.Core/Manifest/ManifestWatcher.cs index 014721eb88..3bc70e2d78 100644 --- a/src/Umbraco.Core/Manifest/ManifestWatcher.cs +++ b/src/Umbraco.Core/Manifest/ManifestWatcher.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.Manifest { - internal class ManifestWatcher : DisposableObject + internal class ManifestWatcher : DisposableObjectSlim { private static readonly object Locker = new object(); private static volatile bool _isRestarting; diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs index 97b655ed63..dd26fda85e 100644 --- a/src/Umbraco.Core/Scoping/ScopeContext.cs +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -79,16 +79,30 @@ namespace Umbraco.Core.Scoping var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary()); - if (enlistedObjects.TryGetValue(key, out IEnlistedObject enlisted)) + if (enlistedObjects.TryGetValue(key, out var enlisted)) { - var enlistedAs = enlisted as EnlistedObject; - if (enlistedAs == null) throw new InvalidOperationException("An item with the key already exists, but with a different type."); - if (enlistedAs.Priority != priority) throw new InvalidOperationException("An item with the key already exits, but with a different priority."); + if (!(enlisted is EnlistedObject enlistedAs)) + throw new InvalidOperationException("An item with the key already exists, but with a different type."); + if (enlistedAs.Priority != priority) + throw new InvalidOperationException("An item with the key already exits, but with a different priority."); return enlistedAs.Item; } - var enlistedOfT = new EnlistedObject(creator == null ? default(T) : creator(), action, priority); + var enlistedOfT = new EnlistedObject(creator == null ? default : creator(), action, priority); Enlisted[key] = enlistedOfT; return enlistedOfT.Item; } + + public T GetEnlisted(string key) + { + var enlistedObjects = _enlisted; + if (enlistedObjects == null) return default; + + if (enlistedObjects.TryGetValue(key, out var enlisted) == false) + return default; + + if (!(enlisted is EnlistedObject enlistedAs)) + throw new InvalidOperationException("An item with the key exists, but with a different type."); + return enlistedAs.Item; + } } } diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index 5ff4052a45..42a2d3b5d4 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -680,19 +680,14 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// The filtered string. public virtual string ReplaceMany(string text, IDictionary replacements) { - // be safe if (text == null) throw new ArgumentNullException(nameof(text)); if (replacements == null) throw new ArgumentNullException(nameof(replacements)); - // Have done various tests, implementing my own "super fast" state machine to handle - // replacement of many items, or via regexes, but on short strings and not too - // many replacements (which prob. is going to be our case) nothing can beat this... - // (at least with safe and checked code -- we don't want unsafe/unchecked here) - // Note that it will do chained-replacements ie replaced items can be replaced - // in turn by another replacement (ie the order of replacements is important) + foreach (KeyValuePair item in replacements) + text = text.Replace(item.Key, item.Value); - return replacements.Aggregate(text, (current, kvp) => current.Replace(kvp.Key, kvp.Value)); + return text; } /// @@ -704,13 +699,14 @@ function validateSafeAlias(input, value, immediate, callback) {{ /// The filtered string. public virtual string ReplaceMany(string text, char[] chars, char replacement) { - // be safe if (text == null) throw new ArgumentNullException(nameof(text)); if (chars == null) throw new ArgumentNullException(nameof(chars)); - // see note above - return chars.Aggregate(text, (current, c) => current.Replace(c, replacement)); + for (int i = 0; i < chars.Length; i++) + text = text.Replace(chars[i], replacement); + + return text; } #endregion diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index 047bb3198f..34bd26b537 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core { AnyGuid, UdiType.GuidUdi }, { Document, UdiType.GuidUdi }, - { DocumentBluePrint, UdiType.GuidUdi }, + { DocumentBlueprint, UdiType.GuidUdi }, { Media, UdiType.GuidUdi }, { Member, UdiType.GuidUdi }, { DictionaryItem, UdiType.GuidUdi }, @@ -67,7 +67,7 @@ namespace Umbraco.Core public const string Document = "document"; - public const string DocumentBluePrint = "document-blueprint"; + public const string DocumentBlueprint = "document-blueprint"; public const string Media = "media"; public const string Member = "member"; @@ -79,6 +79,7 @@ namespace Umbraco.Core public const string DocumentType = "document-type"; public const string DocumentTypeContainer = "document-type-container"; + //TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type public const string DocumentTypeBluePrints = "document-type-blueprints"; public const string MediaType = "media-type"; public const string MediaTypeContainer = "media-type-container"; @@ -115,6 +116,8 @@ namespace Umbraco.Core { case UmbracoObjectTypes.Document: return Document; + case UmbracoObjectTypes.DocumentBlueprint: + return DocumentBlueprint; case UmbracoObjectTypes.Media: return Media; case UmbracoObjectTypes.Member: @@ -159,6 +162,8 @@ namespace Umbraco.Core { case Document: return UmbracoObjectTypes.Document; + case DocumentBlueprint: + return UmbracoObjectTypes.DocumentBlueprint; case Media: return UmbracoObjectTypes.Media; case Member: diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 00f1a85d79..434f25638e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -182,7 +182,6 @@ - @@ -230,6 +229,7 @@ + @@ -237,6 +237,7 @@ + @@ -251,6 +252,7 @@ + @@ -272,6 +274,7 @@ + @@ -290,6 +293,7 @@ + @@ -312,6 +316,7 @@ + diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 6c37a43623..d58814590d 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core { @@ -142,10 +143,18 @@ namespace Umbraco.Core /// internal static bool IsClientSideRequest(this Uri url) { - var ext = Path.GetExtension(url.LocalPath); - if (ext.IsNullOrWhiteSpace()) return false; - var toInclude = new[] { ".aspx", ".ashx", ".asmx", ".axd", ".svc" }; - return toInclude.Any(ext.InvariantEquals) == false; + try + { + var ext = Path.GetExtension(url.LocalPath); + if (ext.IsNullOrWhiteSpace()) return false; + var toInclude = new[] {".aspx", ".ashx", ".asmx", ".axd", ".svc"}; + return toInclude.Any(ext.InvariantEquals) == false; + } + catch (ArgumentException ex) + { + LogHelper.Error(typeof(UriExtensions), "Failed to determine if request was client side", ex); + return false; + } } ///