diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 546c9259f6..8742ab3e78 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -28,11 +28,11 @@ - + - + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 9199867efa..c3a575dc51 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.6 \ No newline at end of file +7.2.7 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 0e3d292c0f..3c73b4d055 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.2.6")] -[assembly: AssemblyInformationalVersion("7.2.6")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.2.7")] +[assembly: AssemblyInformationalVersion("7.2.7")] \ No newline at end of file diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 382c3f29c4..93d502c8d3 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -1,15 +1,12 @@ using System; using System.Configuration; using System.Threading; -using System.Web; -using System.Web.Caching; -using Umbraco.Core.Cache; +using System.Threading.Tasks; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Services; - +using Umbraco.Core.Sync; namespace Umbraco.Core { @@ -36,6 +33,8 @@ namespace Umbraco.Core _databaseContext = dbContext; _services = serviceContext; ApplicationCache = cache; + + Init(); } /// @@ -45,6 +44,8 @@ namespace Umbraco.Core public ApplicationContext(CacheHelper cache) { ApplicationCache = cache; + + Init(); } /// @@ -60,13 +61,13 @@ namespace Umbraco.Core /// public static ApplicationContext EnsureContext(ApplicationContext appContext, bool replaceContext) { - if (ApplicationContext.Current != null) + if (Current != null) { if (!replaceContext) - return ApplicationContext.Current; + return Current; } - ApplicationContext.Current = appContext; - return ApplicationContext.Current; + Current = appContext; + return Current; } /// @@ -85,14 +86,14 @@ namespace Umbraco.Core /// public static ApplicationContext EnsureContext(DatabaseContext dbContext, ServiceContext serviceContext, CacheHelper cache, bool replaceContext) { - if (ApplicationContext.Current != null) + if (Current != null) { if (!replaceContext) - return ApplicationContext.Current; + return Current; } var ctx = new ApplicationContext(dbContext, serviceContext, cache); - ApplicationContext.Current = ctx; - return ApplicationContext.Current; + Current = ctx; + return Current; } /// @@ -141,61 +142,75 @@ namespace Umbraco.Core // GlobalSettings.CurrentVersion returns the hard-coded "current version" // the system is configured if they match // if they don't, install runs, updates web.config (presumably) and updates GlobalSettings.ConfiguredStatus - // - // then there is Application["umbracoNeedConfiguration"] which makes no sense... getting rid of it... SD: I have actually remove that now! - // + public bool IsConfigured { - // todo - we should not do this - ok for now - get - { - return Configured; - } + get { return _configured.Value; } } /// - /// The original/first url that the web application executes + /// The application url. /// /// - /// we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must - /// exist on a global context because the keep alive service doesn't run in a web context. - /// we are NOT going to put a lock on this because locking will slow down the application and we don't really care - /// if two threads write to this at the exact same time during first page hit. - /// see: http://issues.umbraco.org/issue/U4-2059 + /// The application url is the url that should be used by services to talk to the application, + /// eg keep alive or scheduled publishing services. It must exist on a global context because + /// some of these services may not run within a web context. + /// The format of the application url is: + /// - has a scheme (http or https) + /// - has the SystemDirectories.Umbraco path + /// - does not end with a slash + /// It is initialized on the first request made to the server, by UmbracoModule.EnsureApplicationUrl: + /// - if umbracoSettings:settings/web.routing/@umbracoApplicationUrl is set, use the value (new setting) + /// - if umbracoSettings:settings/scheduledTasks/@baseUrl is set, use the value (backward compatibility) + /// - otherwise, use the url of the (first) request. + /// Not locking, does not matter if several threads write to this. + /// See also issues: + /// - http://issues.umbraco.org/issue/U4-2059 + /// - http://issues.umbraco.org/issue/U4-6788 + /// - http://issues.umbraco.org/issue/U4-5728 + /// - http://issues.umbraco.org/issue/U4-5391 /// - internal string OriginalRequestUrl { get; set; } + internal string UmbracoApplicationUrl + { + get + { + // if initialized, return + if (_umbracoApplicationUrl != null) return _umbracoApplicationUrl; - private bool _versionsDifferenceReported; + // try settings + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(this, UmbracoConfig.For.UmbracoSettings()); - /// - /// Checks if the version configured matches the assembly version - /// - private bool Configured - { - get - { - try - { - var configStatus = ConfigurationStatus; - var currentVersion = UmbracoVersion.Current.ToString(3); - var ok = configStatus == currentVersion; + // and return what we have, may be null + return _umbracoApplicationUrl; + } + set + { + _umbracoApplicationUrl = value; + } + } - if (ok == false && _versionsDifferenceReported == false) - { - // remember it's been reported so we don't flood the log - // no thread-safety so there may be a few log entries, doesn't matter - _versionsDifferenceReported = true; - LogHelper.Info("CurrentVersion different from configStatus: '" + currentVersion + "','" + configStatus + "'"); - } - - return ok; - } - catch - { - return false; - } - } - } + internal string _umbracoApplicationUrl; // internal for tests + + private Lazy _configured; + internal MainDom MainDom { get; private set; } + + private void Init() + { + MainDom = new MainDom(); + MainDom.Acquire(); + //Create the lazy value to resolve whether or not the application is 'configured' + _configured = new Lazy(() => + { + var configStatus = ConfigurationStatus; + var currentVersion = UmbracoVersion.Current.ToString(3); + var ok = configStatus == currentVersion; + if (ok == false) + { + LogHelper.Debug("CurrentVersion different from configStatus: '" + currentVersion + "','" + configStatus + "'"); + } + return ok; + }); + } private string ConfigurationStatus { @@ -212,12 +227,6 @@ namespace Umbraco.Core } } - private void AssertIsReady() - { - if (!this.IsReady) - throw new Exception("ApplicationContext is not ready yet."); - } - private void AssertIsNotReady() { if (this.IsReady) diff --git a/src/Umbraco.Core/ApplicationEventHandler.cs b/src/Umbraco.Core/ApplicationEventHandler.cs index a725a08a8e..8d97baac95 100644 --- a/src/Umbraco.Core/ApplicationEventHandler.cs +++ b/src/Umbraco.Core/ApplicationEventHandler.cs @@ -74,7 +74,7 @@ namespace Umbraco.Core /// private bool ShouldExecute(ApplicationContext applicationContext) { - if (applicationContext.IsConfigured && applicationContext.DatabaseContext.CanConnect) + if (applicationContext.IsConfigured && applicationContext.DatabaseContext.IsDatabaseConfigured) { return true; } @@ -84,7 +84,7 @@ namespace Umbraco.Core return true; } - if (!applicationContext.DatabaseContext.CanConnect && ExecuteWhenDatabaseNotConfigured) + if (!applicationContext.DatabaseContext.IsDatabaseConfigured && ExecuteWhenDatabaseNotConfigured) { return true; } diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index 0a9c79a80e..b4e8e64312 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -63,14 +63,14 @@ namespace Umbraco.Core // for anonymous semaphore, use the unique releaser, else create a new one return _semaphore != null ? _releaser // (IDisposable)new SemaphoreSlimReleaser(_semaphore) - : (IDisposable)new NamedSemaphoreReleaser(_semaphore2); + : new NamedSemaphoreReleaser(_semaphore2); } public Task LockAsync() { var wait = _semaphore != null - ? _semaphore.WaitAsync() - : WaitOneAsync(_semaphore2); + ? _semaphore.WaitAsync() + : _semaphore2.WaitOneAsync(); return wait.IsCompleted ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named @@ -79,6 +79,19 @@ namespace Umbraco.Core TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } + public Task LockAsync(int millisecondsTimeout) + { + var wait = _semaphore != null + ? _semaphore.WaitAsync(millisecondsTimeout) + : _semaphore2.WaitOneAsync(millisecondsTimeout); + + return wait.IsCompleted + ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), + this, CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + public IDisposable Lock() { if (_semaphore != null) @@ -168,40 +181,5 @@ namespace Umbraco.Core Dispose(false); } } - - // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 - // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html - // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... - // version below should be OK - - private static Task WaitOneAsync(WaitHandle handle) - { - var tcs = new TaskCompletionSource(); - var callbackHandleInitLock = new object(); - lock (callbackHandleInitLock) - { - RegisteredWaitHandle callbackHandle = null; - // ReSharper disable once RedundantAssignment - callbackHandle = ThreadPool.RegisterWaitForSingleObject( - handle, - (state, timedOut) => - { - tcs.SetResult(null); - - // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. - lock (callbackHandleInitLock) - { - // ReSharper disable once PossibleNullReferenceException - // ReSharper disable once AccessToModifiedClosure - callbackHandle.Unregister(null); - } - }, - /*state:*/ null, - /*millisecondsTimeOutInterval:*/ Timeout.Infinite, - /*executeOnlyOnce:*/ true); - } - - return tcs.Task; - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs index a2bcaee9c9..a6275eca7a 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs @@ -90,7 +90,7 @@ namespace Umbraco.Core.Cache { foreach (var entry in GetDictionaryEntries() .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -105,6 +105,9 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(string typeName) { + var type = TypeFinder.GetTypeByName(typeName); + if (type == null) return; + var isInterface = type.IsInterface; using (WriteLock) { foreach (var entry in GetDictionaryEntries() @@ -114,16 +117,20 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType().ToString().InvariantEquals(typeName); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); }) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } public virtual void ClearCacheObjectTypes() { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; using (WriteLock) { foreach (var entry in GetDictionaryEntries() @@ -134,16 +141,20 @@ namespace Umbraco.Core.Cache // compare on exact type, don't use "is" // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType() == typeOfT; + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); }) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } public virtual void ClearCacheObjectTypes(Func predicate) { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; var plen = CacheItemPrefix.Length + 1; using (WriteLock) { @@ -156,11 +167,14 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - return value.GetType() == typeOfT + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? (value is T) : (value.GetType() == typeOfT)) // run predicate on the 'public key' part only, ie without prefix - && predicate(((string)x.Key).Substring(plen), (T)value); + && predicate(((string)x.Key).Substring(plen), (T)value); })) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -172,7 +186,7 @@ namespace Umbraco.Core.Cache foreach (var entry in GetDictionaryEntries() .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } @@ -184,7 +198,7 @@ namespace Umbraco.Core.Cache foreach (var entry in GetDictionaryEntries() .Where(x => Regex.IsMatch(((string)x.Key).Substring(plen), regexString)) .ToArray()) - RemoveEntry((string) entry.Key); + RemoveEntry((string)entry.Key); } } diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index a4a6938fc0..ca1f4e85a0 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -102,7 +102,7 @@ namespace Umbraco.Core.Cache // cannot create value within the lock, so if result.IsValueCreated is false, just // do nothing here - means that if creation throws, a race condition could cause // more than one thread to reach the return statement below and throw - accepted. - + if (result == null || GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null { result = GetSafeLazy(getCacheItem); @@ -122,7 +122,7 @@ namespace Umbraco.Core.Cache if (eh != null) throw eh.Exception; // throw once! return value; } - + #endregion #region Insert diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index 748912b9f1..e7f5d17b83 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -20,16 +20,22 @@ namespace Umbraco.Core.Cache private readonly System.Web.Caching.Cache _cache; + /// + /// Used for debugging + /// + internal Guid InstanceId { get; private set; } + public HttpRuntimeCacheProvider(System.Web.Caching.Cache cache) { _cache = cache; + InstanceId = Guid.NewGuid(); } protected override IEnumerable GetDictionaryEntries() { const string prefix = CacheItemPrefix + "-"; return _cache.Cast() - .Where(x => x.Key is string && ((string) x.Key).StartsWith(prefix)); + .Where(x => x.Key is string && ((string)x.Key).StartsWith(prefix)); } protected override void RemoveEntry(string key) @@ -134,6 +140,7 @@ namespace Umbraco.Core.Cache var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); lck.UpgradeToWriteLock(); + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } } @@ -184,13 +191,14 @@ namespace Umbraco.Core.Cache var value = result.Value; // force evaluation now - this may throw if cacheItem throws, and then nothing goes into cache if (value == null) return; // do not store null values (backward compat) - cacheKey = GetCacheKey(cacheKey); + cacheKey = GetCacheKey(cacheKey); var absolute = isSliding ? System.Web.Caching.Cache.NoAbsoluteExpiration : (timeout == null ? System.Web.Caching.Cache.NoAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); using (new WriteLock(_locker)) { + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } } diff --git a/src/Umbraco.Core/Cache/ICacheProvider.cs b/src/Umbraco.Core/Cache/ICacheProvider.cs index 6cec619be8..0d2bb1bdb6 100644 --- a/src/Umbraco.Core/Cache/ICacheProvider.cs +++ b/src/Umbraco.Core/Cache/ICacheProvider.cs @@ -8,13 +8,51 @@ namespace Umbraco.Core.Cache /// public interface ICacheProvider { + /// + /// Removes all items from the cache. + /// void ClearAllCache(); + + /// + /// Removes an item from the cache, identified by its key. + /// + /// The key of the item. void ClearCacheItem(string key); + + /// + /// Removes items from the cache, of a specified type. + /// + /// The name of the type to remove. + /// + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). + /// Performs a case-sensitive search. + /// void ClearCacheObjectTypes(string typeName); + + /// + /// Removes items from the cache, of a specified type. + /// + /// The type of the items to remove. + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). void ClearCacheObjectTypes(); + + /// + /// Removes items from the cache, of a specified type, satisfying a predicate. + /// + /// The type of the items to remove. + /// The predicate to satisfy. + /// If the type is an interface, then all items of a type implementing that interface are + /// removed. Otherwise, only items of that exact type are removed (items of type inheriting from + /// the specified type are not removed). void ClearCacheObjectTypes(Func predicate); + void ClearCacheByKeySearch(string keyStartsWith); void ClearCacheByKeyExpression(string regexString); + IEnumerable GetCacheItemsByKeySearch(string keyStartsWith); IEnumerable GetCacheItemsByKeyExpression(string regexString); diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index d4f2c33ed1..045bc4ff4e 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Runtime.Caching; using System.Text.RegularExpressions; using System.Threading; @@ -16,12 +17,19 @@ namespace Umbraco.Core.Cache /// internal class ObjectCacheRuntimeCacheProvider : IRuntimeCacheProvider { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); internal ObjectCache MemoryCache; + /// + /// Used for debugging + /// + internal Guid InstanceId { get; private set; } + public ObjectCacheRuntimeCacheProvider() { MemoryCache = new MemoryCache("in-memory"); + InstanceId = Guid.NewGuid(); } #region Clear @@ -41,11 +49,14 @@ namespace Umbraco.Core.Cache { if (MemoryCache[key] == null) return; MemoryCache.Remove(key); - } + } } public virtual void ClearCacheObjectTypes(string typeName) { + var type = TypeFinder.GetTypeByName(typeName); + if (type == null) return; + var isInterface = type.IsInterface; using (new WriteLock(_locker)) { foreach (var key in MemoryCache @@ -55,7 +66,10 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType().ToString().InvariantEquals(typeName); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type)); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove @@ -67,7 +81,8 @@ namespace Umbraco.Core.Cache { using (new WriteLock(_locker)) { - var typeOfT = typeof (T); + var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; foreach (var key in MemoryCache .Where(x => { @@ -75,7 +90,11 @@ namespace Umbraco.Core.Cache // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); - return value == null || value.GetType() == typeOfT; + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT)); + }) .Select(x => x.Key) .ToArray()) // ToArray required to remove @@ -88,6 +107,7 @@ namespace Umbraco.Core.Cache using (new WriteLock(_locker)) { var typeOfT = typeof(T); + var isInterface = typeOfT.IsInterface; foreach (var key in MemoryCache .Where(x => { @@ -96,8 +116,11 @@ namespace Umbraco.Core.Cache // get non-created as NonCreatedValue & exceptions as null var value = DictionaryCacheProviderBase.GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; - return value.GetType() == typeOfT - && predicate(x.Key, (T) value); + + // if T is an interface remove anything that implements that interface + // otherwise remove exact types (not inherited types) + return (isInterface ? (value is T) : (value.GetType() == typeOfT)) + && predicate(x.Key, (T)value); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove @@ -114,7 +137,7 @@ namespace Umbraco.Core.Cache .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); - } + } } public virtual void ClearCacheByKeyExpression(string regexString) @@ -126,7 +149,7 @@ namespace Umbraco.Core.Cache .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); - } + } } #endregion @@ -178,7 +201,7 @@ namespace Umbraco.Core.Cache return GetCacheItem(cacheKey, getCacheItem, null); } - public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal,CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) + public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // see notes in HttpRuntimeCacheProvider @@ -193,6 +216,7 @@ namespace Umbraco.Core.Cache var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); lck.UpgradeToWriteLock(); + //NOTE: This does an add or update MemoryCache.Set(cacheKey, result, policy); } } @@ -219,6 +243,7 @@ namespace Umbraco.Core.Cache if (value == null) return; // do not store null values (backward compat) var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); + //NOTE: This does an add or update MemoryCache.Set(cacheKey, result, policy); } @@ -239,7 +264,7 @@ namespace Umbraco.Core.Cache { policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); } - + if (removedCallback != null) { policy.RemovedCallback = arguments => @@ -270,6 +295,6 @@ namespace Umbraco.Core.Cache } return policy; } - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs index f3d42b6904..2998fc2f78 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs @@ -11,6 +11,8 @@ bool DisableFindContentByIdPath { get; } string UrlProviderMode { get; } + + string UmbracoApplicationUrl { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs index f5b71eb2c7..1ed9bc034c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs @@ -30,8 +30,13 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("urlProviderMode", DefaultValue = "AutoLegacy")] public string UrlProviderMode { - get { return (string)base["urlProviderMode"]; } + get { return (string) base["urlProviderMode"]; } } + [ConfigurationProperty("umbracoApplicationUrl", DefaultValue = null)] + public string UmbracoApplicationUrl + { + get { return (string)base["umbracoApplicationUrl"]; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index a548f759c7..3f8305589e 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.2.6"); + private static readonly Version Version = new Version("7.2.7"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 6e00d3d682..c5d3e2a4c7 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Web; using AutoMapper; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Mapping; @@ -66,7 +68,9 @@ namespace Umbraco.Core InitializeProfilerResolver(); - _timer = DisposableTimer.TraceDuration("Umbraco application starting", "Umbraco application startup complete"); + _timer = DisposableTimer.TraceDuration( + string.Format("Umbraco {0} application starting on {1}", UmbracoVersion.Current, NetworkHelper.MachineName), + "Umbraco application startup complete"); CreateApplicationCache(); @@ -95,9 +99,25 @@ namespace Umbraco.Core InitializeModelMappers(); - //now we need to call the initialize methods - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationInitialized(UmbracoApplication, ApplicationContext)); + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationInitialized", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationInitialized")) + { + //now we need to call the initialize methods + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationInitialized for handler " + x.GetType(), ex); + throw; + } + }); + } _isInitialized = true; @@ -201,11 +221,27 @@ namespace Umbraco.Core if (_isStarted) throw new InvalidOperationException("The boot manager has already been initialized"); - //call OnApplicationStarting of each application events handler - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarting(UmbracoApplication, ApplicationContext)); + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationStarting", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationStarting")) + { + //call OnApplicationStarting of each application events handler + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationStarting for handler " + x.GetType(), ex); + throw; + } + }); + } - if (afterStartup != null) + if (afterStartup != null) { afterStartup(ApplicationContext.Current); } @@ -226,10 +262,29 @@ namespace Umbraco.Core throw new InvalidOperationException("The boot manager has already been completed"); FreezeResolution(); - - //call OnApplicationStarting of each application events handler - ApplicationEventsResolver.Current.ApplicationEventHandlers - .ForEach(x => x.OnApplicationStarted(UmbracoApplication, ApplicationContext)); + + //Here we need to make sure the db can be connected to + EnsureDatabaseConnection(); + + using (DisposableTimer.DebugDuration( + () => string.Format("Executing {0} IApplicationEventHandler.OnApplicationStarted", ApplicationEventsResolver.Current.ApplicationEventHandlers.Count()), + () => "Finished executing IApplicationEventHandler.OnApplicationStarted")) + { + //call OnApplicationStarting of each application events handler + ApplicationEventsResolver.Current.ApplicationEventHandlers + .ForEach(x => + { + try + { + x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred running OnApplicationStarted for handler " + x.GetType(), ex); + throw; + } + }); + } //Now, startup all of our legacy startup handler ApplicationEventsResolver.Current.InstantiateLegacyStartupHandlers(); @@ -249,6 +304,32 @@ namespace Umbraco.Core return this; } + /// + /// We cannot continue if the db cannot be connected to + /// + private void EnsureDatabaseConnection() + { + if (ApplicationContext.IsConfigured == false) return; + if (ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) return; + + var currentTry = 0; + while (currentTry < 5) + { + if (ApplicationContext.DatabaseContext.CanConnect) + break; + + //wait and retry + Thread.Sleep(1000); + currentTry++; + } + + if (currentTry == 5) + { + throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but the Umbraco cannot connect to the database."); + } + + } + /// /// Freeze resolution to not allow Resolvers to be modified /// diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index fad96b849d..dfaebb0315 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -26,8 +26,6 @@ namespace Umbraco.Core { private readonly IDatabaseFactory _factory; private bool _configured; - private bool _canConnect; - private volatile bool _connectCheck = false; private readonly object _locker = new object(); private string _connectionString; private string _providerName; @@ -67,21 +65,9 @@ namespace Umbraco.Core get { if (IsDatabaseConfigured == false) return false; - - //double check lock so that it is only checked once and is fast - if (_connectCheck == false) - { - lock (_locker) - { - if (_canConnect == false) - { - _canConnect = DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DatabaseProvider); - _connectCheck = true; - } - } - } - - return _canConnect; + var canConnect = DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DatabaseProvider); + LogHelper.Info("CanConnect = " + canConnect); + return canConnect; } } diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index f054b53c98..516a9712e5 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -4,69 +4,58 @@ using System.Threading; namespace Umbraco.Core { /// - /// Abstract implementation of logic commonly required to safely handle disposable unmanaged resources. + /// Abstract implementation of IDisposable. /// + /// + /// 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 DisposableObject : IDisposable { private bool _disposed; - private readonly ReaderWriterLockSlim _disposalLocker = new ReaderWriterLockSlim(); + private readonly object _locko = new object(); - /// - /// Gets a value indicating whether this instance is disposed. - /// - /// - /// true if this instance is disposed; otherwise, false. - /// - public bool IsDisposed - { - get { return _disposed; } - } + // gets a value indicating whether this instance is disposed. + // for internal tests only (not thread safe) + //TODO make this internal + rename "Disposed" when we can break compatibility + public bool IsDisposed { get { return _disposed; } } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// 2 + // implements IDisposable public void Dispose() { Dispose(true); - - // Use SupressFinalize in case a subclass of this type implements a finalizer. GC.SuppressFinalize(this); } + // finalizer ~DisposableObject() { - // Run dispose but let the class know it was due to the finalizer running. Dispose(false); } - protected virtual void Dispose(bool disposing) + //TODO make this private, non-virtual when we can break compatibility + protected virtual void Dispose(bool disposing) { - // Only operate if we haven't already disposed - if (IsDisposed || !disposing) return; + lock (_locko) + { + if (_disposed) return; + _disposed = true; + } - using (new WriteLock(_disposalLocker)) - { - // Check again now we're inside the lock - if (IsDisposed) return; + DisposeUnmanagedResources(); - // Call to actually release resources. This method is only - // kept separate so that the entire disposal logic can be used as a VS snippet - DisposeResources(); - - // Indicate that the instance has been disposed. - _disposed = true; - } + if (disposing) + DisposeResources(); } - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// protected abstract void DisposeResources(); protected virtual void DisposeUnmanagedResources() - { - - } + { } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs b/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs new file mode 100644 index 0000000000..d27d38de9a --- /dev/null +++ b/src/Umbraco.Core/Exceptions/UmbracoStartupFailedException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + /// + /// An exception that is thrown if the umbraco application cannnot boot + /// + public class UmbracoStartupFailedException : Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UmbracoStartupFailedException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/LogHelper.cs b/src/Umbraco.Core/Logging/LogHelper.cs index 14aab56f0b..05b9e7ed65 100644 --- a/src/Umbraco.Core/Logging/LogHelper.cs +++ b/src/Umbraco.Core/Logging/LogHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Web; @@ -40,8 +41,11 @@ namespace Umbraco.Core.Logging /// private static string PrefixThreadId(string generateMessageFormat) { - return "[T" + Thread.CurrentThread.ManagedThreadId + "/D" + AppDomain.CurrentDomain.Id + "] " + generateMessageFormat; - } + return (_prefixThreadId ?? (_prefixThreadId = "[P" + Process.GetCurrentProcess().Id + "/T" + Thread.CurrentThread.ManagedThreadId + "/D" + AppDomain.CurrentDomain.Id + "] ")) + + generateMessageFormat; + } + + private static string _prefixThreadId = null; #region Error /// diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs new file mode 100644 index 0000000000..9eec3d4da6 --- /dev/null +++ b/src/Umbraco.Core/MainDom.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Hosting; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Core +{ + // represents the main domain + class MainDom : IRegisteredObject + { + #region Vars + + // our own lock for local consistency + private readonly object _locko = new object(); + + // async lock representing the main domain lock + private readonly AsyncLock _asyncLock; + private IDisposable _asyncLocker; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + + // indicates whether... + private volatile bool _isMainDom; // we are the main domain + private volatile bool _signaled; // we have been signaled + + // actions to run before releasing the main domain + private readonly SortedList _callbacks = new SortedList(); + + private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds + + #endregion + + #region Ctor + + // initializes a new instance of MainDom + public MainDom() + { + var appId = string.Empty; + // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail + if (HostingEnvironment.ApplicationID != null) + appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty); + + var lockName = "UMBRACO-" + appId + "-MAINDOM-LCK"; + _asyncLock = new AsyncLock(lockName); + + var eventName = "UMBRACO-" + appId + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + } + + #endregion + + // register a main domain consumer + public bool Register(Action release, int weight = 100) + { + return Register(null, release, weight); + } + + // register a main domain consumer + public bool Register(Action install, Action release, int weight = 100) + { + lock (_locko) + { + if (_signaled) return false; + if (install != null) + install(); + if (release != null) + _callbacks.Add(weight, release); + return true; + } + } + + // handles the signal requesting that the main domain is released + private void OnSignal(string source) + { + // once signaled, we stop waiting, but then there is the hosting environment + // so we have to make sure that we only enter that method once + + lock (_locko) + { + LogHelper.Debug("Signaled" + (_signaled ? " (again)" : "") + " (" + source + ")."); + if (_signaled) return; + if (_isMainDom == false) return; // probably not needed + _signaled = true; + } + + try + { + LogHelper.Debug("Stopping..."); + foreach (var callback in _callbacks.Values) + { + try + { + callback(); // no timeout on callbacks + } + catch (Exception e) + { + LogHelper.Error("Error while running callback, remaining callbacks will not run.", e); + throw; + } + + } + LogHelper.Debug("Stopped."); + } + finally + { + // in any case... + _isMainDom = false; + _asyncLocker.Dispose(); + LogHelper.Debug("Released MainDom."); + } + } + + // acquires the main domain + public bool Acquire() + { + lock (_locko) // we don't want the hosting environment to interfere by signaling + { + // if signaled, too late to acquire, give up + // the handler is not installed so that would be the hosting environment + if (_signaled) + { + LogHelper.Debug("Cannot acquire MainDom (signaled)."); + return false; + } + + LogHelper.Debug("Acquiring MainDom..."); + + // signal other instances that we want the lock, then wait one the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // resetted (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + _asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds); + _isMainDom = true; + + // we need to reset the event, because otherwise we would end up + // signaling ourselves and commiting suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + + _signal.Reset(); + _signal.WaitOneAsync() + .ContinueWith(_ => OnSignal("signal")); + + HostingEnvironment.RegisterObject(this); + + LogHelper.Debug("Acquired MainDom."); + return true; + } + } + + // gets a value indicating whether we are the main domain + public bool IsMainDom + { + get { return _isMainDom; } + } + + // IRegisteredObject + public void Stop(bool immediate) + { + try + { + OnSignal("environment"); // will run once + } + finally + { + HostingEnvironment.UnregisterObject(this); + } + } + } +} diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 8ba30665d0..1dc882674d 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -137,9 +137,23 @@ namespace Umbraco.Core.Models new DelegateEqualityComparer( (o, o1) => { - //Custom comparer for enumerable if it is enumerable if (o == null && o1 == null) return true; + + //custom comparer for strings. + if (o is string || o1 is string) + { + //if one is null and another is empty then they are the same + if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + { + return true; + } + if (o == null || o1 == null) return false; + return o.Equals(o1); + } + if (o == null || o1 == null) return false; + + //Custom comparer for enumerable if it is enumerable var enum1 = o as IEnumerable; var enum2 = o1 as IEnumerable; if (enum1 != null && enum2 != null) diff --git a/src/Umbraco.Core/ObjectResolution/Resolution.cs b/src/Umbraco.Core/ObjectResolution/Resolution.cs index 87eb06e295..0b478d54cf 100644 --- a/src/Umbraco.Core/ObjectResolution/Resolution.cs +++ b/src/Umbraco.Core/ObjectResolution/Resolution.cs @@ -112,7 +112,7 @@ namespace Umbraco.Core.ObjectResolution /// resolution is already frozen. public static void Freeze() { - LogHelper.Debug(typeof(Resolution), "Freezing resolution"); + LogHelper.Debug(typeof (Resolution), "Freezing resolution"); using (new WriteLock(ConfigurationLock)) { @@ -121,9 +121,20 @@ namespace Umbraco.Core.ObjectResolution _isFrozen = true; } - - if (Frozen != null) - Frozen(null, null); + + LogHelper.Debug(typeof(Resolution), "Resolution is frozen"); + + if (Frozen == null) return; + + try + { + Frozen(null, null); + } + catch (Exception e) + { + LogHelper.Error(typeof (Resolution), "Exception in Frozen event handler.", e); + throw; + } } /// diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs index 31cfeb7997..0ad3d8e5b9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs @@ -34,15 +34,20 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven if (Context.CurrentDatabaseProvider == DatabaseProviders.MySql) { Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("nodeId").ToTable("umbracoNode").PrimaryColumn("id"); + //check for another strange really old one that might have existed + if (constraints.Any(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "tagId")) + { + Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("tagId").ToTable("cmsTags").PrimaryColumn("id"); + } } else { //Before we try to delete this constraint, we'll see if it exists first, some older schemas never had it and some older schema's had this named // differently than the default. - var constraint = constraints - .SingleOrDefault(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); - if (constraint != null) + var constraintMatches = constraints.Where(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); + + foreach (var constraint in constraintMatches) { Delete.ForeignKey(constraint.Item3).OnTable("cmsTagRelationship"); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs index 198e8307eb..dc775cb426 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixTwoZero/AdditionalIndexesAndKeys.cs @@ -42,11 +42,33 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero Create.Index("IX_cmsDocument_newest").OnTable("cmsDocument").OnColumn("newest").Ascending().WithOptions().NonClustered(); } - //TODO: We need to fix this for SQL Azure since it does not let you drop any clustered indexes - // Issue: http://issues.umbraco.org/issue/U4-5673 - // Some work around notes: - // http://stackoverflow.com/questions/15872347/alter-clustered-index-column - // https://social.msdn.microsoft.com/Forums/azure/en-US/5cc4b302-fa42-4c62-956a-bbf79dbbd040/changing-clustered-index-in-azure?forum=ssdsgetstarted + //We need to do this for SQL Azure V2 since it does not let you drop any clustered indexes + // Issue: http://issues.umbraco.org/issue/U4-5673 + if (Context.CurrentDatabaseProvider == DatabaseProviders.SqlServer || Context.CurrentDatabaseProvider == DatabaseProviders.SqlAzure) + { + var version = Context.Database.ExecuteScalar("SELECT @@@@VERSION"); + if (version.Contains("Microsoft SQL Azure")) + { + var parts = version.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + if (parts.Length > 1) + { + if (parts[1].StartsWith("11.")) + { + + //we want to drop the umbracoUserLogins_Index index since it is named incorrectly and then re-create it so + // it follows the standard naming convention + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("umbracoUserLogins_Index"))) + { + //It's the old version that doesn't support dropping a clustered index on a table, so we need to do some manual work. + ExecuteSqlAzureSqlForChangingIndex(); + } + + return; + } + } + } + } + //we want to drop the umbracoUserLogins_Index index since it is named incorrectly and then re-create it so // it follows the standard naming convention @@ -67,5 +89,28 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixTwoZero Delete.Index("IX_cmsDocument_published").OnTable("cmsDocument"); Delete.Index("IX_cmsDocument_newest").OnTable("cmsDocument"); } + + private void ExecuteSqlAzureSqlForChangingIndex() + { + Context.Database.Execute(@"CREATE TABLE ""umbracoUserLogins_temp"" +( + contextID uniqueidentifier NOT NULL, + userID int NOT NULL, + [timeout] bigint NOT NULL +); +CREATE CLUSTERED INDEX ""IX_umbracoUserLogins_Index"" ON ""umbracoUserLogins_temp"" (""contextID""); +INSERT INTO ""umbracoUserLogins_temp"" SELECT * FROM ""umbracoUserLogins"" +DROP TABLE ""umbracoUserLogins"" +CREATE TABLE ""umbracoUserLogins"" +( + contextID uniqueidentifier NOT NULL, + userID int NOT NULL, + [timeout] bigint NOT NULL +); +CREATE CLUSTERED INDEX ""IX_umbracoUserLogins_Index"" ON ""umbracoUserLogins"" (""contextID""); +INSERT INTO ""umbracoUserLogins"" SELECT * FROM ""umbracoUserLogins_temp"" +DROP TABLE ""umbracoUserLogins_temp"""); + + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 2a58879025..b606830c62 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -493,15 +493,10 @@ AND umbracoNode.id <> @id", throw new InvalidOperationException("Cannot insert a pre value for a data type that has no identity"); } - //Cannot add a duplicate alias - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataTypePreValues -WHERE alias = @alias -AND datatypeNodeId = @dtdid", - new { alias = entity.Alias, dtdid = entity.DataType.Id }); - if (exists > 0) - { - throw new DuplicateNameException("A pre value with the alias " + entity.Alias + " already exists for this data type"); - } + //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT + // in reality there is no need to check the uniqueness of this alias because the only way that this code executes is + // based on an IDictionary dictionary being passed to this repository and a dictionary + // must have unique aliases by definition, so there is no need for this additional check var dto = new DataTypePreValueDto { @@ -519,17 +514,12 @@ AND datatypeNodeId = @dtdid", { throw new InvalidOperationException("Cannot update a pre value for a data type that has no identity"); } - - //Cannot change to a duplicate alias - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataTypePreValues -WHERE alias = @alias -AND datatypeNodeId = @dtdid -AND id <> @id", - new { id = entity.Id, alias = entity.Alias, dtdid = entity.DataType.Id }); - if (exists > 0) - { - throw new DuplicateNameException("A pre value with the alias " + entity.Alias + " already exists for this data type"); - } + + //NOTE: We used to check that the Alias was unique for the given DataTypeNodeId prevalues list, BUT + // this causes issues when sorting the pre-values (http://issues.umbraco.org/issue/U4-5670) but in reality + // there is no need to check the uniqueness of this alias because the only way that this code executes is + // based on an IDictionary dictionary being passed to this repository and a dictionary + // must have unique aliases by definition, so there is no need for this additional check var dto = new DataTypePreValueDto { diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index d03edc0fc6..c63d8cdfde 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -606,14 +606,10 @@ namespace Umbraco.Core /// internal IEnumerable CreateInstances(IEnumerable types, bool throwException = false) { - //Have removed logging because it doesn't really need to be logged since the time taken is generally 0ms. - //we want to know if it fails ever, not how long it took if it is only 0. - var typesAsArray = types.ToArray(); - //using (DisposableTimer.DebugDuration( - // String.Format("Starting instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName), - // String.Format("Completed instantiation of {0} objects of type {1}", typesAsArray.Length, typeof(T).FullName))) - //{ + + LogHelper.Debug(string.Format("Instantiating {0} objects of type {1}", typesAsArray.Length, typeof (T).FullName)); + var instances = new List(); foreach (var t in typesAsArray) { @@ -634,7 +630,7 @@ namespace Umbraco.Core } } return instances; - //} + } /// diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs index 1d5ee7855a..f3b241a5e0 100644 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -1,10 +1,9 @@ -using System; using System.Linq; using System.Web; -using System.Xml; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core.Sync { @@ -13,58 +12,73 @@ namespace Umbraco.Core.Sync /// internal static class ServerEnvironmentHelper { - /// - /// Returns the current umbraco base url for the current server depending on it's environment - /// status. This will attempt to determine the internal umbraco base url that can be used by the current - /// server to send a request to itself if it is in a load balanced environment. - /// - /// The full base url including schema (i.e. http://myserver:80/umbraco ) - or null if the url - /// cannot be determined at the moment (usually because the first request has not properly completed yet). - public static string GetCurrentServerUmbracoBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) + public static void TrySetApplicationUrlFromSettings(ApplicationContext appContext, IUmbracoSettingsSection settings) { + // try umbracoSettings:settings/web.routing/@umbracoApplicationUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - contain a scheme + // - end or not with a slash, it will be taken care of + // eg "http://www.mysite.com/umbraco" + var url = settings.WebRouting.UmbracoApplicationUrl; + if (url.IsNullOrWhiteSpace() == false) + { + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using web.routing/@umbracoApplicationUrl)"); + return; + } + + // try umbracoSettings:settings/scheduledTasks/@baseUrl + // which is assumed to: + // - end with SystemDirectories.Umbraco + // - NOT contain any scheme (because, legacy) + // - end or not with a slash, it will be taken care of + // eg "mysite.com/umbraco" + url = settings.ScheduledTasks.BaseUrl; + if (url.IsNullOrWhiteSpace() == false) + { + var ssl = GlobalSettings.UseSSL ? "s" : ""; + url = "http" + ssl + "://" + url; + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using scheduledTasks/@baseUrl)"); + return; + } + + // try servers var status = GetStatus(settings); - if (status == CurrentServerEnvironmentStatus.Single) - { - // single install, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); - } + return; + // no server, nothing we can do var servers = settings.DistributedCall.Servers.ToArray(); + if (servers.Length == 0) + return; - if (servers.Any() == false) - { - // cannot be determined, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); - } - + // we have servers, look for this server foreach (var server in servers) { var appId = server.AppId; var serverName = server.ServerName; + // skip if no data if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) - { continue; - } + // if this server, build and return the url if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) { - //match by appId or computer name! return the url configured - return string.Format("{0}://{1}:{2}/{3}", + // match by appId or computer name, return the url configured + url = string.Format("{0}://{1}:{2}/{3}", server.ForceProtocol.IsNullOrWhiteSpace() ? "http" : server.ForceProtocol, server.ServerAddress, server.ForcePortnumber.IsNullOrWhiteSpace() ? "80" : server.ForcePortnumber, IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); + + appContext.UmbracoApplicationUrl = url.TrimEnd('/'); + LogHelper.Info(typeof(ServerEnvironmentHelper), "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (using distributedCall/servers)"); } } - - // cannot be determined, return null if no config/original url, else use config/original url as base - // use http or https as appropriate - return GetBaseUrl(appContext, settings); } /// @@ -113,21 +127,5 @@ namespace Umbraco.Core.Sync return CurrentServerEnvironmentStatus.Slave; } - - private static string GetBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) - { - return ( - // is config empty? - settings.ScheduledTasks.BaseUrl.IsNullOrWhiteSpace() - // is the orig req empty? - ? appContext.OriginalRequestUrl.IsNullOrWhiteSpace() - // we've got nothing - ? null - //the orig req url is not null, use that - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl) - // the config has been specified, use that - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", settings.ScheduledTasks.BaseUrl)) - .EnsureEndsWith('/'); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index 6b257fecfe..abaf5ae5e0 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -137,7 +137,7 @@ namespace Umbraco.Core } return _allAssemblies; - } + } } /// @@ -226,7 +226,7 @@ namespace Umbraco.Core } return LocalFilteredAssemblyCache; - } + } } /// @@ -451,7 +451,7 @@ namespace Umbraco.Core var allTypes = GetTypesWithFormattedException(a) .ToArray(); - var attributedTypes = new Type[] {}; + var attributedTypes = new Type[] { }; try { //now filter the types based on the onlyConcreteClasses flag, not interfaces, not static classes but have @@ -480,7 +480,8 @@ namespace Umbraco.Core //now we need to include types that may be inheriting from sub classes of the attribute type being searched for //so we will search in assemblies that reference those types too. - foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)){ + foreach (var subTypesInAssembly in allAttributeTypes.GroupBy(x => x.Assembly)) + { //So that we are not scanning too much, we need to group the sub types: // * if there is more than 1 sub type in the same assembly then we should only search on the 'lowest base' type. @@ -610,7 +611,7 @@ namespace Umbraco.Core catch (TypeLoadException ex) { LogHelper.Error(typeof(TypeFinder), string.Format("Could not query types on {0} assembly, this is most likely due to this assembly not being compatible with the current Umbraco version", a), ex); - continue; + continue; } //add the types to our list to return @@ -618,7 +619,7 @@ namespace Umbraco.Core { foundAssignableTypes.Add(t); } - + //now we need to include types that may be inheriting from sub classes of the type being searched for //so we will search in assemblies that reference those types too. foreach (var subTypesInAssembly in allSubTypes.GroupBy(x => x.Assembly)) @@ -699,7 +700,18 @@ namespace Umbraco.Core #endregion - + //TODO: This isn't very elegant, and will have issues since the AppDomain.CurrentDomain + // doesn't actualy load in all assemblies, only the types that have been referenced so far. + // However, in a web context, the BuildManager will have executed which will force all assemblies + // to be loaded so it's fine for now. + internal static Type GetTypeByName(string typeName) + { + var type = Type.GetType(typeName); + if (type != null) return type; + return AppDomain.CurrentDomain.GetAssemblies() + .Select(x => x.GetType(typeName)) + .FirstOrDefault(x => x != null); + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 67ce55b8f1..cf5b46776f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -312,10 +312,12 @@ + + @@ -1214,6 +1216,7 @@ + diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index c861eca6cd..550228e3a8 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -36,6 +36,19 @@ namespace Umbraco.Core //don't output the MVC version header (security) MvcHandler.DisableMvcResponseHeader = true; + //take care of unhandled exceptions - there is nothing we can do to + // prevent the entire w3wp process to go down but at least we can try + // and log the exception + AppDomain.CurrentDomain.UnhandledException += (_, args) => + { + var exception = (Exception) args.ExceptionObject; + var isTerminating = args.IsTerminating; // always true? + + var msg = "Unhandled exception in AppDomain"; + if (isTerminating) msg += " (terminating)"; + LogHelper.Error(msg, exception); + }; + //boot up the application GetBootManager() .Initialize() @@ -73,7 +86,18 @@ namespace Umbraco.Core protected virtual void OnApplicationStarting(object sender, EventArgs e) { if (ApplicationStarting != null) - ApplicationStarting(sender, e); + { + try + { + ApplicationStarting(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationStarting event handler", ex); + throw; + } + } + } /// @@ -84,7 +108,17 @@ namespace Umbraco.Core protected virtual void OnApplicationStarted(object sender, EventArgs e) { if (ApplicationStarted != null) - ApplicationStarted(sender, e); + { + try + { + ApplicationStarted(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationStarted event handler", ex); + throw; + } + } } /// @@ -95,7 +129,17 @@ namespace Umbraco.Core private void OnApplicationInit(object sender, EventArgs e) { if (ApplicationInit != null) - ApplicationInit(sender, e); + { + try + { + ApplicationInit(sender, e); + } + catch (Exception ex) + { + LogHelper.Error("An error occurred in an ApplicationInit event handler", ex); + throw; + } + } } /// diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 53441d7ea1..48e9b83b20 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -49,7 +49,7 @@ namespace Umbraco.Core var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); //check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/')); + var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); //if not, then def not back office if (isUmbracoPath == false) return false; diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs new file mode 100644 index 0000000000..0d840a2496 --- /dev/null +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core +{ + internal static class WaitHandleExtensions + { + + // http://stackoverflow.com/questions/25382583/waiting-on-a-named-semaphore-with-waitone100-vs-waitone0-task-delay100 + // http://blog.nerdbank.net/2011/07/c-await-for-waithandle.html + // F# has a AwaitWaitHandle method that accepts a time out... and seems pretty complex... + // version below should be OK + + public static Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout = Timeout.Infinite) + { + var tcs = new TaskCompletionSource(); + var callbackHandleInitLock = new object(); + lock (callbackHandleInitLock) + { + RegisteredWaitHandle callbackHandle = null; + // ReSharper disable once RedundantAssignment + callbackHandle = ThreadPool.RegisterWaitForSingleObject( + handle, + (state, timedOut) => + { + tcs.SetResult(null); + + // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. + lock (callbackHandleInitLock) + { + // ReSharper disable once PossibleNullReferenceException + // ReSharper disable once AccessToModifiedClosure + callbackHandle.Unregister(null); + } + }, + /*state:*/ null, + /*millisecondsTimeOutInterval:*/ millisecondsTimeout, + /*executeOnlyOnce:*/ true); + } + + return tcs.Task; + } + } +} diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 52962879d4..258c5adef9 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -581,15 +581,11 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } }; var task = new MyRecurringTask(runner, 200, 500); @@ -604,7 +600,10 @@ namespace Umbraco.Tests.Scheduling Assert.GreaterOrEqual(runCount, 4); // stops recurring - runner.Shutdown(false, false); + runner.Shutdown(false, true); + + // check that task has been disposed (timer has been killed, etc) + Assert.IsTrue(task.IsDisposed); } } @@ -651,15 +650,12 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } + }; var task = new MyDelayedRecurringTask(runner, 2000, 1000); @@ -783,37 +779,31 @@ namespace Umbraco.Tests.Scheduling } } - private class MyDelayedRecurringTask : DelayedRecurringTaskBase + private class MyDelayedRecurringTask : RecurringTaskBase { public bool HasRun { get; private set; } - public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) : base(runner, delayMilliseconds, periodMilliseconds) { } - private MyDelayedRecurringTask(MyDelayedRecurringTask source) - : base(source) - { } - public override bool IsAsync { get { return false; } } - public override void PerformRun() + public override bool PerformRun() { HasRun = true; + return true; // repeat } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } - protected override MyDelayedRecurringTask GetRecurring() - { - return new MyDelayedRecurringTask(this); - } + public override bool RunsOnShutdown { get { return true; } } } private class MyDelayedTask : ILatchedBackgroundTask @@ -867,29 +857,23 @@ namespace Umbraco.Tests.Scheduling { } } - private class MyRecurringTask : RecurringTaskBase + private class MyRecurringTask : RecurringTaskBase { private readonly int _runMilliseconds; - - public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) + public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) + : base(runner, 0, periodMilliseconds) { _runMilliseconds = runMilliseconds; } - private MyRecurringTask(MyRecurringTask source, int runMilliseconds) - : base(source) - { - _runMilliseconds = runMilliseconds; - } - - public override void PerformRun() + public override bool PerformRun() { Thread.Sleep(_runMilliseconds); + return true; // repeat } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } @@ -899,10 +883,7 @@ namespace Umbraco.Tests.Scheduling get { return false; } } - protected override MyRecurringTask GetRecurring() - { - return new MyRecurringTask(this, _runMilliseconds); - } + public override bool RunsOnShutdown { get { return false; } } } private class MyTask : BaseTask diff --git a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs index 237c6cb17d..39f8b71eb3 100644 --- a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs +++ b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs @@ -1,92 +1,98 @@ using System.Configuration; +using System.IO; using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Profiling; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests { [TestFixture] public class ServerEnvironmentHelperTests { + + // note: in tests, read appContext._umbracoApplicationUrl and not the property, + // because reading the property does run some code, as long as the field is null. + [Test] - public void Get_Base_Url_Single_Server_Orig_Request_Url_No_SSL() + public void SetApplicationUrlWhenNoSettings() { var appContext = new ApplicationContext(null) { - OriginalRequestUrl = "test.com" + UmbracoApplicationUrl = null // NOT set }; - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, - Mock.Of( - section => - section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.ScheduledTasks == Mock.Of())); - - - Assert.AreEqual("http://test.com/", result); - } - - [Test] - public void Get_Base_Url_Single_Server_Orig_Request_Url_With_SSL() - { - var appContext = new ApplicationContext(null) - { - OriginalRequestUrl = "test.com" - }; - - ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); - - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) && section.ScheduledTasks == Mock.Of())); - Assert.AreEqual("https://test.com/", result); + // still NOT set + Assert.IsNull(appContext._umbracoApplicationUrl); } [Test] - public void Get_Base_Url_Single_Server_Via_Config_Url_No_SSL() + public void SetApplicationUrlFromDcSettingsNoSsl() { var appContext = new ApplicationContext(null); ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) - && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world/"))); - Assert.AreEqual("http://mycoolhost.com/hello/world/", result); + Assert.AreEqual("http://mycoolhost.com/hello/world", appContext._umbracoApplicationUrl); } [Test] - public void Get_Base_Url_Single_Server_Via_Config_Url_With_SSL() + public void SetApplicationUrlFromDcSettingsSsl() { var appContext = new ApplicationContext(null); ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); - var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, Mock.Of( section => section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == (string) null) && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); - Assert.AreEqual("https://mycoolhost.com/hello/world/", result); + Assert.AreEqual("https://mycoolhost.com/hello/world", appContext._umbracoApplicationUrl); + } + + [Test] + public void SetApplicationUrlFromWrSettingsSsl() + { + var appContext = new ApplicationContext(null); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); // does not make a diff here + + ServerEnvironmentHelper.TrySetApplicationUrlFromSettings(appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.WebRouting == Mock.Of(wrSection => wrSection.UmbracoApplicationUrl == "httpx://whatever.com/hello/world/") + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + + + Assert.AreEqual("httpx://whatever.com/hello/world", appContext._umbracoApplicationUrl); } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 18b9b8d006..10bbdaadc2 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -58,9 +58,9 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - + False - ..\packages\Examine.0.1.63.0\lib\Examine.dll + ..\packages\Examine.0.1.65.0\lib\Examine.dll False diff --git a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs index 18343a96f5..d01b298bb4 100644 --- a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs @@ -14,7 +14,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Events_Ignoring_Node() { //change the parent id so that they are all ignored - var existingCriteria = ((IndexCriteria)_indexer.IndexerData); + var existingCriteria = _indexer.IndexerData; _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, 999); //change to 999 diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 77a9fd2dba..a5418649fe 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -53,7 +53,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Index_Move_Media_From_Non_Indexable_To_Indexable_ParentID() { //change parent id to 1116 - var existingCriteria = ((IndexCriteria)_indexer.IndexerData); + var existingCriteria = _indexer.IndexerData; _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, 1116); @@ -110,7 +110,7 @@ namespace Umbraco.Tests.UmbracoExamine _indexer.ReIndexNode(node, IndexTypes.Media); //change the parent node id to be the one it used to exist under - var existingCriteria = ((IndexCriteria)_indexer.IndexerData); + var existingCriteria = _indexer.IndexerData; _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, 2222); diff --git a/src/Umbraco.Tests/UriExtensionsTests.cs b/src/Umbraco.Tests/UriExtensionsTests.cs index 1b0ae96621..4ea8e8875a 100644 --- a/src/Umbraco.Tests/UriExtensionsTests.cs +++ b/src/Umbraco.Tests/UriExtensionsTests.cs @@ -4,12 +4,19 @@ using System.Linq; using System.Text; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.IO; namespace Umbraco.Tests { [TestFixture] public class UriExtensionsTests { + [TearDown] + public void TearDown() + { + SystemDirectories.Root = ""; + } + [TestCase("http://www.domain.com/umbraco", "", true)] [TestCase("http://www.domain.com/Umbraco/", "", true)] [TestCase("http://www.domain.com/umbraco/default.aspx", "", true)] @@ -35,6 +42,8 @@ namespace Umbraco.Tests [TestCase("http://www.domain.com/umbraco/test/legacyAjaxCalls.ashx?some=query&blah=js", "", true)] public void Is_Back_Office_Request(string input, string virtualPath, bool expected) { + SystemDirectories.Root = virtualPath; + var source = new Uri(input); Assert.AreEqual(expected, source.IsBackOfficeRequest(virtualPath)); } diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index 58d314c3b1..a919949f56 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -2,25 +2,25 @@ - - + + - - - + + + - + - - + + - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..d6682b0435 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -22,9 +22,13 @@ "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", - "angular-dynamic-locale": "~0.1.27" + "angular-dynamic-locale": "~0.1.27", + "bootstrap-tabdrop": "~1.0.0" }, "exportsOverride": { + "bootstrap-tabdrop": { + "": "build/js/bootstrap-tabdrop.min.js" + }, "rgrove-lazyload": { "": "lazyload.js" }, @@ -38,7 +42,7 @@ "": "tmhDynamicLocale.min.{js,js.map}" }, "jquery": { - "": "jquery.min.{js,map}" + "": "jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" @@ -53,9 +57,9 @@ "blueimp-tmpl": { "ignore": "*.ignore" }, - + "blueimp-canvas-to-blob": { "ignore": "*.ignore" } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index ef636d2ada..a5961f126c 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -418,7 +418,7 @@ module.exports = function (grunt) { //this is the same as 'byComponent', however we will not allow // folders with '.' in them since the grunt copy task does not like that var componentWithoutPeriod = component.replace(".", "-"); - return path.join(componentWithoutPeriod, type); + return path.join(componentWithoutPeriod, type); } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js index b1cca01d44..15a1a09c05 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js @@ -40,6 +40,8 @@ angular.module("umbraco.directives") } }); + + $('.nav-pills, .nav-tabs').tabdrop(); } scope.showTabs = iAttrs.tabs ? true : false; diff --git a/src/Umbraco.Web.UI.Client/src/less/grid.less b/src/Umbraco.Web.UI.Client/src/less/grid.less index 22ea5d9836..8e1557fea1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/grid.less @@ -176,3 +176,70 @@ body { .emptySection #contentwrapper {left: 80px;} .emptySection #speechbubble {left: 0;} .emptySection #navigation {display: none} + +@media (max-width: 767px) { + + // make leftcolumn smaller on tablets + #leftcolumn { + width: 60px; + } + #applications ul.sections { + width: 60px; + } + ul.sections.sections-tray { + width: 60px; + } + #applications-tray { + left: 60px; + } + #navigation { + left: 60px; + } + #contentwrapper, #contentcolumn { + left: 30px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 60px; + } +} + + + +@media (max-width: 500px) { + + // make leftcolumn smaller on mobiles + #leftcolumn { + width: 40px; + } + #applications ul.sections { + width: 40px; + } + ul.sections li [class^="icon-"]:before { + font-size: 25px!important; + } + #applications ul.sections li.avatar a img { + width: 25px; + } + ul.sections a span { + display:none !important; + } + #applications ul.sections-tray { + width: 40px; + } + ul.sections.sections-tray { + width: 40px; + } + #applications-tray { + left: 40px; + } + #navigation { + left: 40px; + } + #contentwrapper, #contentcolumn { + left: 20px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 40px; + width: 85%!important; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 2a6abef6d7..ca61d872d3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -1,19 +1,27 @@ // Panel // ------------------------- -.umb-panel{ - background: white; - position: absolute; - top: 0px; bottom: 0px; left: 0px; right: 0px;} +.umb-panel { + background: white; + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; +} + +.umb-panel-nobody { + padding-top: 100px; + overflow: auto; +} -.umb-panel-nobody{padding-top: 100px; overflow: auto;} .umb-panel-header { - background: @grayLighter; - border-bottom: 1px solid @grayLight; - position: absolute; - height: 99px; - top: 0px; - right: 0px; - left: 0px; + background: @grayLighter; + border-bottom: 1px solid @grayLight; + position: absolute; + height: 99px; + top: 0px; + right: 0px; + left: 0px; } .umb-panel-body { @@ -25,6 +33,7 @@ clear: both; overflow: auto; } + .umb-panel-body.no-header { top: 20px; } @@ -38,71 +47,83 @@ } .umb-panel-header .umb-headline, .umb-panel-header h1 { - font-size: 16px; - border: none; - background: none; - margin: 15px 0 0 20px; - padding: 3px 5px; - line-height: 1.4; - height: auto; - width: 100%; - border: 1px solid @grayLighter; + font-size: 16px; + border: none; + background: none; + margin: 15px 0 0 20px; + padding: 3px 5px; + line-height: 1.4; + height: auto; + width: 100%; + border: 1px solid @grayLighter; } -.umb-panel-header .umb-headline:focus,.umb-panel-header .umb-headline:active { - border: 1px solid @grayLight; - background-color: @white; +.umb-panel-header .umb-headline:focus, .umb-panel-header .umb-headline:active { + border: 1px solid @grayLight; + background-color: @white; } -.umb-headline-editor-wrapper{ - position: relative; +.umb-headline-editor-wrapper { + position: relative; } -.umb-headline-editor-wrapper .help-inline{ - right: 0px; - top: 25px; - position: absolute; - font-size: 10px; - color: @red; - } +.umb-headline-editor-wrapper .help-inline { + right: 0px; + top: 25px; + position: absolute; + font-size: 10px; + color: @red; +} -.umb-panel-header .umb-nav-tabs{ - bottom: -1px; - position: absolute; - padding: 0px 0px 0px 20px; +.umb-panel-header .umb-nav-tabs { + bottom: -1px; + position: absolute; + padding: 0px 0px 0px 20px; } .umb-panel-header p { - margin:0px 20px; + margin: 0px 20px; } -.umb-btn-toolbar .dimmed, .umb-dimmed{ - opacity: 0.6; +.umb-btn-toolbar .dimmed, .umb-dimmed { + opacity: 0.6; } -.umb-headline-editor-wrapper input { - background: none; - border: none; - margin: -6px 0 0 0; - padding: 0 0 2px 0; - border-radius: 0; - line-height: normal; - border: 1px solid transparent; - color: @black; - letter-spacing: -0.01em -} -.umb-headline-editor-wrapper input.ng-invalid { - color: @red; +.umb-headline-editor-wrapper input { + background: none; + border: none; + margin: -6px 0 0 0; + padding: 0 0 2px 0; + border-radius: 0; + line-height: normal; + border: 1px solid transparent; + color: @black; + letter-spacing: -0.01em; } -.umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder {color: @red; line-height: 22px;} -.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder {color: @red; line-height: 22px;} -.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder {color: @red; line-height: 22px;} +.umb-headline-editor-wrapper input.ng-invalid { + color: @red; +} + +.umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder { + color: @red; + line-height: 22px; +} + +.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder { + color: @red; + line-height: 22px; +} + +.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder { + color: @red; + line-height: 22px; +} .umb-panel-header i { - font-size: 13px; - vertical-align: middle; + font-size: 13px; + vertical-align: middle; } .umb-panel-header-meta { @@ -115,65 +136,76 @@ } .umb-panel-footer { - margin: 0; - padding: 20px; - z-index: 999; - position: absolute; - bottom: 0px; - left: 0px; - right: 0px; + margin: 0; + padding: 20px; + z-index: 999; + position: absolute; + bottom: 0px; + left: 0px; + right: 0px; } /* Publish */ .umb-btn-toolbar .dropdown-menu { - right: 0; - left: auto; - border-radius: @tabsBorderRadius; - box-shadow: none; - padding: 0 - } + right: 0; + left: auto; + border-radius: @tabsBorderRadius; + box-shadow: none; + padding: 0; +} - .umb-btn-toolbar .dropdown-menu small { - background: #fef9db; - display: block; - padding: 10px 20px; - } +.umb-btn-toolbar .dropdown-menu small { + background: #fef9db; + display: block; + padding: 10px 20px; +} - .umb-btn-toolbar .dropdown-menu .btn { +.umb-btn-toolbar .dropdown-menu .btn  { margin: 20px 29px; - width: 80px - } + width: 80px; +} /* tab buttons */ -.umb-bottom-bar{ +.umb-bottom-bar { background: white; -webkit-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); - -moz-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); - box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); + -moz-box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); + box-shadow: 0px -18px 20px rgba(255, 255, 255, 1); border-top: 1px solid @grayLighter; padding: 10px 0 10px 0; position: fixed; - bottom: 0px; + bottom: 0; left: 100px; + right: 40px; z-index: 6010; -} -.umb-tab-buttons{ - padding-left: 0px; -} - -@media (min-width: 1101px) { - .umb-bottom-bar { + @media (min-width: 1101px) { left: 460px; - } + } } -.umb-tab-pane{padding-bottom: 90px} +.umb-tab-buttons { + padding-left: 0; -.tab-content{overflow: visible; } + > .btn-group:not([style*="display:none"]):not([style*="display: none"]) { + margin-left: 0; + } -.umb-panel-footer-nav{ + @media (min-width: 768px) { + padding-left: 180px; + } +} + +.umb-tab-pane { + padding-bottom: 90px; +} + +.tab-content { + overflow: visible; +} + +.umb-panel-footer-nav { position: absolute; bottom: 0px; height: 30px; @@ -187,93 +219,117 @@ } .umb-panel-footer-nav li a { - border-radius: 0; - display: block; - float: left; - height: 30px; - background: @grayLighter; - text-align: center; - padding: 8px 0px 8px 30px; - position: relative; - margin: 0 1px 0 0; - text-decoration: none; - color: @gray; - font-size: 11px; + border-radius: 0; + display: block; + float: left; + height: 30px; + background: @grayLighter; + text-align: center; + padding: 8px 0px 8px 30px; + position: relative; + margin: 0 1px 0 0; + text-decoration: none; + color: @gray; + font-size: 11px; } .umb-panel-footer-nav li a:after { - content: ""; - border-top: 16px solid transparent; - border-bottom: 16px solid transparent; - border-left: 16px solid @grayLighter; - position: absolute; right: -16px; top: 0; - z-index: 1; -} - -.umb-panel-footer-nav li a:before { - content: ""; - border-top: 16px solid transparent; - border-bottom: 16px solid transparent; - border-left: 16px solid @grayLight; - position: absolute; left: 0; top: 0; + content: ""; + border-top: 16px solid transparent; + border-bottom: 16px solid transparent; + border-left: 16px solid @grayLighter; + position: absolute; + right: -16px; + top: 0; + z-index: 1; } -.umb-panel-footer-nav li:first-child a{ - padding-left: 20px; +.umb-panel-footer-nav li a:before { + content: ""; + border-top: 16px solid transparent; + border-bottom: 16px solid transparent; + border-left: 16px solid @grayLight; + position: absolute; + left: 0; + top: 0; +} + +.umb-panel-footer-nav li:first-child a { + padding-left: 20px; } .umb-panel-footer-nav li:first-child a:before { - display: none; + display: none; } .umb-panel-footer-nav li:last-child a:after { - display: none; + display: none; } - // Utility classes +// Utility classes - //SD: Had to add these because if we want to use the bootstrap text colors - // in a panel/editor they'll all show up as white on white - so we need to use the - // form styles +//SD: Had to add these because if we want to use the bootstrap text colors +// in a panel/editor they'll all show up as white on white - so we need to use the +// form styles .umb-dialog .muted, -.umb-panel .muted { color: @grayLight; } +.umb-panel .muted { + color: @grayLight; +} .umb-dialog a.muted:hover, .umb-dialog a.muted:focus, .umb-panel a.muted:hover, -.umb-panel a.muted:focus { color: darken(@grayLight, 10%); } +.umb-panel a.muted:focus { + color: darken(@grayLight, 10%); +} .umb-dialog .text-warning, -.umb-panel .text-warning { color: @formWarningText; } +.umb-panel .text-warning { + color: @formWarningText; +} .umb-dialog a.text-warning:hover, .umb-dialog a.text-warning:focus, .umb-panel a.text-warning:hover, -.umb-panel a.text-warning:focus { color: darken(@formWarningText, 10%); } +.umb-panel a.text-warning:focus { + color: darken(@formWarningText, 10%); +} .umb-dialog .text-error, -.umb-panel .text-error { color: @formErrorText; } +.umb-panel .text-error { + color: @formErrorText; +} .umb-dialog a.text-error:hover, .umb-dialog a.text-error:focus, .umb-panel a.text-error:hover, -.umb-panel a.text-error:focus { color: darken(@formErrorText, 10%); } +.umb-panel a.text-error:focus { + color: darken(@formErrorText, 10%); +} .umb-dialog .text-info, -.umb-panel .text-info { color: @formInfoText; } +.umb-panel .text-info { + color: @formInfoText; +} .umb-dialog a.text-info:hover, .umb-dialog a.text-info:focus, .umb-panel a.text-info:hover, -.umb-panel a.text-info:focus { color: darken(@formInfoText, 10%); } +.umb-panel a.text-info:focus { + color: darken(@formInfoText, 10%); +} .umb-dialog .text-success, -.umb-panel .text-success { color: @formSuccessText; } +.umb-panel .text-success { + color: @formSuccessText; +} .umb-dialog a.text-success:hover, .umb-dialog a.text-success:focus, .umb-panel a.text-success:hover, -.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } \ No newline at end of file +.umb-panel a.text-success:focus { + color: darken(@formSuccessText, 10%); +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index f34d45be63..e06a45771b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -522,3 +522,5 @@ ul.color-picker li a { .bootstrap-datetimepicker-widget .btn{padding: 0;} .bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} .umb-datepicker .input-append .add-on{cursor: pointer;} +.umb-datepicker p {margin-top:10px;} +.umb-datepicker p a{color: @gray;} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/loader.js b/src/Umbraco.Web.UI.Client/src/loader.js index 32def61618..179de16de8 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.js +++ b/src/Umbraco.Web.UI.Client/src/loader.js @@ -1,28 +1,32 @@ LazyLoad.js( [ 'lib/jquery/jquery.min.js', - 'lib/jquery-ui/jquery-ui.min.js', - - /* 1.1.5 */ 'lib/angular/1.1.5/angular.min.js', + 'lib/underscore/underscore-min.js', + + 'lib/jquery-ui/jquery-ui.min.js', + 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-mobile.js', 'lib/angular/1.1.5/angular-sanitize.min.js', 'lib/angular/angular-ui-sortable.js', 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', - /* App-wide file-upload helper */ + 'lib/blueimp-load-image/load-image.all.min.js', 'lib/jquery-file-upload/jquery.fileupload.js', 'lib/jquery-file-upload/jquery.fileupload-process.js', + 'lib/jquery-file-upload/jquery.fileupload-image.js', 'lib/jquery-file-upload/jquery.fileupload-angular.js', - + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/underscore/underscore-min.js', - 'lib/umbraco/Extensions.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', + 'lib/umbraco/Extensions.js', + 'lib/umbraco/NamespaceManager.js', + 'lib/umbraco/LegacyUmbClientMgr.js', + 'lib/umbraco/LegacySpeechBubble.js', 'js/umbraco.servervariables.js', 'js/app.dev.js', diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index eb49bc3b39..64a22d4c54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -18,7 +18,8 @@ function dateTimePickerController($scope, notificationsService, assetsService, a //map the user config $scope.model.config = angular.extend(config, $scope.model.config); - $scope.datetimePickerValue = $scope.model.value; + $scope.hasDatetimePickerValue = $scope.model.value ? true : false; + $scope.datetimePickerValue = null; //hide picker if clicking on the document $scope.hidePicker = function () { @@ -35,14 +36,20 @@ function dateTimePickerController($scope, notificationsService, assetsService, a function applyDate(e) { angularHelper.safeApply($scope, function() { // when a date is changed, update the model - if (e.date) { - if ($scope.model.config.pickTime) { - $scope.model.value = e.date.format("YYYY-MM-DD HH:mm:ss"); + if (e.date && e.date.isValid()) { + $scope.datePickerForm.datepicker.$setValidity("pickerError", true); + $scope.hasDatetimePickerValue = true; + if (!$scope.model.config.format) { + $scope.datetimePickerValue = e.date; } else { - $scope.model.value = e.date.format("YYYY-MM-DD"); + $scope.datetimePickerValue = e.date.format($scope.model.config.format); } } + else { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + } if (!$scope.model.config.pickTime) { $element.find("div:first").datetimepicker("hide", 0); @@ -50,6 +57,15 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); } + var picker = null; + + $scope.clearDate = function() { + $scope.hasDatetimePickerValue = false; + $scope.datetimePickerValue = null; + $scope.model.value = null; + $scope.datePickerForm.datepicker.$setValidity("pickerError", true); + } + //get the current user to see if we can localize this picker userService.getCurrentUser().then(function (user) { @@ -69,25 +85,39 @@ function dateTimePickerController($scope, notificationsService, assetsService, a // Get the id of the datepicker button that was clicked var pickerId = $scope.model.alias; - // Open the datepicker and add a changeDate eventlistener - $element.find("div:first") - .datetimepicker($scope.model.config) - .on("dp.change", applyDate); + var element = $element.find("div:first"); - //manually assign the date to the plugin - if (!$scope.model.config.format) { - $element.find("div:first").datetimepicker("setValue", $scope.model.value ? $scope.model.value : null); - } - else { - $element.find("div:first").datetimepicker("setValue", $scope.model.value ? new Date($scope.model.value) : null); - if ($scope.model.value && $scope.model.config.format) { - $scope.datetimePickerValue = moment($scope.model.value).format($scope.model.config.format); - } - } + // Open the datepicker and add a changeDate eventlistener + element + .datetimepicker(angular.extend({ useCurrent: true }, $scope.model.config)) + .on("dp.change", applyDate) + .on("dp.error", function(a, b, c) { + $scope.hasDatetimePickerValue = false; + $scope.datePickerForm.datepicker.$setValidity("pickerError", false); + }); + + if ($scope.hasDatetimePickerValue) { + + //assign value to plugin/picker + element.datetimepicker("setValue", $scope.model.value ? new Date($scope.model.value) : moment()); + + if (!$scope.model.config.format) { + $scope.datetimePickerValue = moment($scope.model.value); + } + else { + $scope.datetimePickerValue = moment($scope.model.value).format($scope.model.config.format); + } + } + + element.find("input").bind("blur", function() { + //we need to force an apply here + $scope.$apply(); + }); //Ensure to remove the event handler when this instance is destroyted - $scope.$on('$destroy', function () { - $element.find("div:first").datetimepicker("destroy"); + $scope.$on('$destroy', function () { + element.find("input").unbind("blur"); + element.datetimepicker("destroy"); }); }); }); @@ -95,9 +125,24 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + if ($scope.hasDatetimePickerValue) { + if ($scope.model.config.pickTime) { + $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD HH:mm:ss"); + } + else { + $scope.model.value = $element.find("div:first").data().DateTimePicker.getDate().format("YYYY-MM-DD"); + } + } + else { + $scope.model.value = null; + } + }); + //unbind doc click event! $scope.$on('$destroy', function () { $(document).unbind("click", $scope.hidePicker); + unsubscribe(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index b3e503eb4a..898afe88a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -1,14 +1,26 @@
-
- - - - -
+ +
+ + - Required - {{propertyForm.datepicker.errorMsg}} + + + + +
+ + Required + {{datePickerForm.datepicker.errorMsg}} + Invalid date + +

+ Clear date +

+ +
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index b84764cd0d..a24bca7b17 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -47,6 +47,7 @@ ..\ true true + bin\ @@ -114,9 +115,9 @@ False ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll
- - False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + True False @@ -126,9 +127,9 @@ False ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.63.0\lib\Examine.dll - True + + False + ..\packages\Examine.0.1.65.0\lib\Examine.dll False @@ -2540,9 +2541,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7260 + 7270 / - http://localhost:7260 + http://localhost:7270 False False diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 1c16a6e029..0f6c04a6b7 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + + internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false" disableFindContentByIdPath="false" + umbracoApplicationUrl=""> diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 255dce6542..546ecf42fe 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -276,10 +276,15 @@ By default you can call any content Id in the url and show the content with that id, for example: http://mysite.com/1092 or http://mysite.com/1092.aspx would render the content with id 1092. Setting this setting to true stops that behavior + @umbracoApplicationUrl + The url of the Umbraco application. By default, Umbraco will figure it out from the first request. + Configure it here if you need anything specific. Needs to be a complete url with scheme and umbraco + path, eg http://mysite.com/umbraco. NOT just "mysite.com" or "mysite.com/umbraco" or "http://mysite.com". --> - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 4a754f6758..c6d0752e12 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -1,28 +1,28 @@  - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 8655acd9a7..a3be346758 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -14,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.member; using System.Linq; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Logging; using Umbraco.Core.Publishing; using Content = Umbraco.Core.Models.Content; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; @@ -30,7 +31,9 @@ namespace Umbraco.Web.Cache public class CacheRefresherEventHandler : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { + { + LogHelper.Info("Initializing Umbraco internal event handlers for cache refreshing"); + //bind to application tree events ApplicationTreeService.Deleted += ApplicationTreeDeleted; ApplicationTreeService.Updated += ApplicationTreeUpdated; diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 013f40fc0e..b5bc908fde 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -98,7 +98,7 @@ namespace Umbraco.Web.Install // that and we might get lock issues. try { - var xmlFile = content.Instance.UmbracoXmlDiskCacheFileName + ".tmp"; + var xmlFile = content.GetUmbracoXmlDiskFileName() + ".tmp"; SaveAndDeleteFile(xmlFile); return true; } diff --git a/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs index cbd2e0e519..dc8aa15f6a 100644 --- a/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/AdminTokenAuthorizeAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Web; @@ -36,22 +37,33 @@ namespace Umbraco.Web.Mvc return _applicationContext ?? ApplicationContext.Current; } + public const string AuthorizationType = "AToken"; + /// - /// Used to return the value that needs to go in the Authorization header + /// Used to return the full value that needs to go in the Authorization header /// /// /// public static string GetAuthHeaderTokenVal(ApplicationContext appContext) { - var admin = appContext.Services.UserService.GetUserById(0); + return string.Format("{0} {1}", AuthorizationType, GetAuthHeaderVal(appContext)); + } + public static AuthenticationHeaderValue GetAuthenticationHeaderValue(ApplicationContext appContext) + { + return new AuthenticationHeaderValue(AuthorizationType, GetAuthHeaderVal(appContext)); + } + + private static string GetAuthHeaderVal(ApplicationContext appContext) + { + var admin = appContext.Services.UserService.GetUserById(0); var token = string.Format("{0}u____u{1}u____u{2}", admin.Email, admin.Username, admin.RawPasswordValue); var encrypted = token.EncryptWithMachineKey(); var bytes = Encoding.UTF8.GetBytes(encrypted); var base64 = Convert.ToBase64String(bytes); - return "AToken val=\"" + base64 + "\""; + return string.Format("val=\"{0}\"", base64); } /// diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index accdc660d7..870d65c4e1 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -17,11 +17,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : ILatchedBackgroundTask + internal class XmlCacheFilePersister : LatchedBackgroundTaskBase { private readonly IBackgroundTaskRunner _runner; private readonly content _content; - private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); private readonly object _locko = new object(); private bool _released; private Timer _timer; @@ -38,7 +37,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down - public bool RunsOnShutdown { get { return true; } } + public override bool RunsOnShutdown { get { return _timer != null; } } // initialize the first instance, which is inactive (not touched yet) public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content) @@ -141,28 +140,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache lock (_locko) { LogHelper.Debug("Timer: release."); - if (_timer != null) - _timer.Dispose(); - _timer = null; _released = true; - // if running (because of shutdown) this will have no effect - // else it tells the runner it is time to run the task - _latch.Set(); + Release(); } } - public WaitHandle Latch - { - get { return _latch.WaitHandle; } - } - - public bool IsLatched - { - get { return true; } - } - - public async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { lock (_locko) { @@ -181,15 +165,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public bool IsAsync + public override bool IsAsync { get { return true; } } - public void Dispose() - { } - - public void Run() + public override void Run() { lock (_locko) { @@ -203,5 +184,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache _content.SaveXmlToFile(); } } + + protected override void DisposeResources() + { + base.DisposeResources(); + + // stop the timer + if (_timer == null) return; + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 575845efba..9612ede0b9 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -90,7 +90,8 @@ namespace Umbraco.Web.Scheduling _options = options; _logPrefix = "[" + name + "] "; - HostingEnvironment.RegisterObject(this); + if (options.Hosted) + HostingEnvironment.RegisterObject(this); if (options.AutoStart) StartUp(); @@ -385,6 +386,7 @@ namespace Umbraco.Web.Scheduling // still latched & not running on shutdown = stop here if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) { + dbgTask.Dispose(); // will not run TaskSourceCompleted(taskSource, token); return; } @@ -442,7 +444,7 @@ namespace Umbraco.Web.Scheduling try { - using (bgTask) // ensure it's disposed + try { if (bgTask.IsAsync) //configure await = false since we don't care about the context, we're on a background thread. @@ -450,6 +452,12 @@ namespace Umbraco.Web.Scheduling else bgTask.Run(); } + finally // ensure we disposed - unless latched (again) + { + var lbgTask = bgTask as ILatchedBackgroundTask; + if (lbgTask == null || lbgTask.IsLatched == false) + bgTask.Dispose(); + } } catch (Exception e) { diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs index 4688ff37d6..55df42d3b7 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -15,6 +15,8 @@ namespace Umbraco.Web.Scheduling LongRunning = false; KeepAlive = false; AutoStart = false; + PreserveRunningTask = false; + Hosted = true; } /// @@ -36,9 +38,16 @@ namespace Umbraco.Web.Scheduling public bool AutoStart { get; set; } /// - /// Gets or setes a value indicating whether the running task should be preserved + /// Gets or sets a value indicating whether the running task should be preserved /// once completed, or reset to null. For unit tests. /// public bool PreserveRunningTask { get; set; } + + /// + /// Gets or sets a value indicating whether the runner should register with (and be + /// stopped by) the hosting. Otherwise, something else should take care of stopping + /// the runner. True by default. + /// + public bool Hosted { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs deleted file mode 100644 index af3dedbe70..0000000000 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Provides a base class for recurring background tasks. - /// - /// The type of the managed tasks. - internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, ILatchedBackgroundTask - where T : class, IBackgroundTask - { - private readonly ManualResetEventSlim _latch; - private Timer _timer; - - protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) - { - if (delayMilliseconds > 0) - { - _latch = new ManualResetEventSlim(false); - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _latch.Set(); - }); - _timer.Change(delayMilliseconds, 0); - } - } - - protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) - : base(source) - { - // no latch on recurring instances - _latch = null; - } - - public override void Run() - { - if (_latch != null) - _latch.Dispose(); - base.Run(); - } - - public override async Task RunAsync(CancellationToken token) - { - if (_latch != null) - _latch.Dispose(); - await base.RunAsync(token); - } - - public WaitHandle Latch - { - get - { - if (_latch == null) - throw new InvalidOperationException("The task is not latched."); - return _latch.WaitHandle; - } - } - - public bool IsLatched - { - get { return _latch != null && _latch.IsSet == false; } - } - - public virtual bool RunsOnShutdown - { - get { return true; } - } - } -} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index c1b43b57c6..d11bf2af35 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -1,47 +1,83 @@ using System; using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { - internal class KeepAlive + internal class KeepAlive : RecurringTaskBase { - public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings) + private readonly ApplicationContext _appContext; + + public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext) + : base(runner, delayMilliseconds, periodMilliseconds) { + _appContext = appContext; + } + + public override bool PerformRun() + { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) - { - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( - appContext, - settings); + { + string umbracoAppUrl = null; - if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) + try { - LogHelper.Warn("No url for service (yet), skip."); - } - else - { - var url = string.Format("{0}ping.aspx", umbracoBaseUrl.EnsureEndsWith('/')); - - try + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) { - using (var wc = new WebClient()) + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + + var url = umbracoAppUrl + "/ping.aspx"; + using (var wc = new HttpClient()) + { + var request = new HttpRequestMessage() { - wc.DownloadString(url); - } - } - catch (Exception ee) - { - LogHelper.Error( - string.Format("Error in ping. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoBaseUrl) - , ee); + RequestUri = new Uri(url), + Method = HttpMethod.Get + }; + + var result = await wc.SendAsync(request, token); } } + catch (Exception e) + { + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + } } - + + return true; // repeat + } + + public override bool IsAsync + { + get { return true; } + } + + public override bool RunsOnShutdown + { + get { return false; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs new file mode 100644 index 0000000000..3315fa7c34 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core; + +namespace Umbraco.Web.Scheduling +{ + internal abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask + { + private readonly ManualResetEventSlim _latch; + + protected LatchedBackgroundTaskBase() + { + _latch = new ManualResetEventSlim(false); + } + + /// + /// Implements IBackgroundTask.Run(). + /// + public abstract void Run(); + + /// + /// Implements IBackgroundTask.RunAsync(). + /// + public abstract Task RunAsync(CancellationToken token); + + /// + /// Indicates whether the background task can run asynchronously. + /// + public abstract bool IsAsync { get; } + + public WaitHandle Latch + { + get { return _latch.WaitHandle; } + } + + public bool IsLatched + { + get { return _latch.IsSet == false; } + } + + protected void Release() + { + _latch.Set(); + } + + protected void Reset() + { + _latch.Reset(); + } + + public abstract bool RunsOnShutdown { get; } + + // the task is going to be disposed after execution, + // unless it is latched again, thus indicating it wants to + // remain active + + protected override void DisposeResources() + { + _latch.Dispose(); + } + } +} diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index a9f70a612e..14a7ea3211 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Caching; @@ -6,15 +7,16 @@ using umbraco.BusinessLogic; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { - internal class LogScrubber : DelayedRecurringTaskBase + internal class LogScrubber : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -22,19 +24,7 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public LogScrubber(LogScrubber source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override LogScrubber GetRecurring() - { - return new LogScrubber(this); - } - - private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) + private static int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { int maximumAge = 24 * 60 * 60; try @@ -44,7 +34,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 hours.", e); } return maximumAge; @@ -65,15 +55,32 @@ namespace Umbraco.Web.Scheduling return interval; } - public override void PerformRun() + public override bool PerformRun() { - using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) + if (_appContext == null) return true; // repeat... + + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return false; // do NOT repeat, server status comes from config and will NOT change + } + + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + + using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); - } + } + + return true; // repeat } - public override Task PerformRunAsync() + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index d710a70e03..567f85f1f5 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -6,57 +6,51 @@ namespace Umbraco.Web.Scheduling /// /// Provides a base class for recurring background tasks. /// - /// The type of the managed tasks. - internal abstract class RecurringTaskBase : IBackgroundTask - where T : class, IBackgroundTask + internal abstract class RecurringTaskBase : LatchedBackgroundTaskBase { - private readonly IBackgroundTaskRunner _runner; + private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; - private Timer _timer; - private T _recurrent; + private readonly Timer _timer; + private bool _disposed; /// - /// Initializes a new instance of the class with a tasks runner and a period. + /// Initializes a new instance of the class. /// /// The task runner. + /// The delay. /// The period. /// The task will repeat itself periodically. Use this constructor to create a new task. - protected RecurringTaskBase(IBackgroundTaskRunner runner, int periodMilliseconds) + protected RecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) { _runner = runner; _periodMilliseconds = periodMilliseconds; - } - /// - /// Initializes a new instance of the class with a source task. - /// - /// The source task. - /// Use this constructor to create a new task from a source task in GetRecurring. - protected RecurringTaskBase(RecurringTaskBase source) - { - _runner = source._runner; - _timer = source._timer; - _periodMilliseconds = source._periodMilliseconds; + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + + _timer = new Timer(_ => Release()); + _timer.Change(delayMilliseconds, 0); } /// /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual void Run() + public override void Run() { - PerformRun(); - Repeat(); + var shouldRepeat = PerformRun(); + if (shouldRepeat) Repeat(); } /// /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { - await PerformRunAsync(); - Repeat(); + var shouldRepeat = await PerformRunAsync(token); + if (shouldRepeat) Repeat(); } private void Repeat() @@ -64,52 +58,39 @@ namespace Umbraco.Web.Scheduling // again? if (_runner.IsCompleted) return; // fail fast - if (_periodMilliseconds == 0) return; + if (_periodMilliseconds == 0) return; // safe - _recurrent = GetRecurring(); - if (_recurrent == null) - { - _timer.Dispose(); - _timer = null; - return; // done - } + Reset(); // re-latch - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent)); - _timer.Change(_periodMilliseconds, 0); + // try to add again (may fail if runner has completed) + // if added, re-start the timer, else kill it + if (_runner.TryAdd(this)) + _timer.Change(_periodMilliseconds, 0); + else + Dispose(true); } - /// - /// Indicates whether the background task can run asynchronously. - /// - public abstract bool IsAsync { get; } - /// /// Runs the background task. /// - public abstract void PerformRun(); + /// A value indicating whether to repeat the task. + public abstract bool PerformRun(); /// /// Runs the task asynchronously. /// - /// A instance representing the execution of the background task. - public abstract Task PerformRunAsync(); + /// A cancellation token. + /// A instance representing the execution of the background task, + /// and returning a value indicating whether to repeat the task. + public abstract Task PerformRunAsync(CancellationToken token); - /// - /// Gets a new occurence of the recurring task. - /// - /// A new task instance to be queued, or null to terminate the recurring task. - /// The new task instance must be created via the RecurringTaskBase(RecurringTaskBase{T} source) constructor, - /// where source is the current task, eg: return new MyTask(this); - protected abstract T GetRecurring(); + protected override void DisposeResources() + { + base.DisposeResources(); - /// - /// Dispose the task. - /// - public virtual void Dispose() - { } + // stop the timer + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 9db21fba8a..11cffb3648 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -1,25 +1,21 @@ using System; -using System.Diagnostics; -using System.Net; -using System.Text; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Publishing; using Umbraco.Core.Sync; using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing : DelayedRecurringTaskBase + internal class ScheduledPublishing : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private static bool _isPublishingRunning; - - public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -27,75 +23,68 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - private ScheduledPublishing(ScheduledPublishing source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledPublishing GetRecurring() - { - return new ScheduledPublishing(this); - } - - public override void PerformRun() - { - if (_appContext == null) return; - if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) - { - LogHelper.Debug("Does not run on slave servers."); - return; - } - - using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) - { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(_appContext, _settings); - - try - { - - if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) - { - LogHelper.Warn("No url for service (yet), skip."); - } - else - { - var url = string.Format("{0}RestServices/ScheduledPublish/Index", umbracoBaseUrl.EnsureEndsWith('/')); - using (var wc = new WebClient()) - { - //pass custom the authorization header - wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(_appContext)); - - var result = wc.UploadString(url, ""); - } - } - } - catch (Exception ee) - { - LogHelper.Error( - string.Format("An error occurred with the scheduled publishing. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoBaseUrl) - , ee); - } - finally - { - _isPublishingRunning = false; - } - } - } - - public override Task PerformRunAsync() + public override bool PerformRun() { throw new NotImplementedException(); } + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return false; // do NOT repeat, server status comes from config and will NOT change + } + + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down + } + + using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) + { + string umbracoAppUrl = null; + + try + { + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + + var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; + using (var wc = new HttpClient()) + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(url), + Method = HttpMethod.Post, + Content = new StringContent(string.Empty) + }; + //pass custom the authorization header + request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + + var result = await wc.SendAsync(request, token); + } + } + catch (Exception e) + { + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + } + } + + return true; // repeat + } + public override bool IsAsync { - get { return false; } + get { return true; } } public override bool RunsOnShutdown diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index cba3cb4fc8..def7b606b5 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -1,15 +1,13 @@ using System; using System.Collections; -using System.Linq; using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using System.Xml; -using Umbraco.Core.Configuration; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Publishing; using Umbraco.Core.Sync; -using Umbraco.Core; namespace Umbraco.Web.Scheduling { @@ -17,14 +15,13 @@ namespace Umbraco.Web.Scheduling // would need to be a publicly available task (URL) which isn't really very good :( // We should really be using the AdminTokenAuthorizeAttribute for this stuff - internal class ScheduledTasks : DelayedRecurringTaskBase + internal class ScheduledTasks : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); - private static bool _isPublishingRunning = false; - public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -32,30 +29,19 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public ScheduledTasks(ScheduledTasks source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledTasks GetRecurring() - { - return new ScheduledTasks(this); - } - - private void ProcessTasks() + private async Task ProcessTasksAsync(CancellationToken token) { var scheduledTasks = _settings.ScheduledTasks.Tasks; foreach (var t in scheduledTasks) { var runTask = false; - if (!ScheduledTaskTimes.ContainsKey(t.Alias)) + if (ScheduledTaskTimes.ContainsKey(t.Alias) == false) { runTask = true; ScheduledTaskTimes.Add(t.Alias, DateTime.Now); } - /// Add 1 second to timespan to compensate for differencies in timer + + // Add 1 second to timespan to compensate for differencies in timer else if ( new TimeSpan( DateTime.Now.Ticks - ((DateTime)ScheduledTaskTimes[t.Alias]).Ticks).TotalSeconds + 1 >= t.Interval) @@ -66,69 +52,80 @@ namespace Umbraco.Web.Scheduling if (runTask) { - bool taskResult = GetTaskByHttp(t.Url); + var taskResult = await GetTaskByHttpAync(t.Url, token); if (t.Log) LogHelper.Info(string.Format("{0} has been called with response: {1}", t.Alias, taskResult)); } } } - private bool GetTaskByHttp(string url) + private async Task GetTaskByHttpAync(string url, CancellationToken token) { - var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); - - try + using (var wc = new HttpClient()) { - using (var response = (HttpWebResponse)myHttpWebRequest.GetResponse()) + var request = new HttpRequestMessage() { - return response.StatusCode == HttpStatusCode.OK; - } - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling web task for url: " + url, ex); - } + RequestUri = new Uri(url), + Method = HttpMethod.Get, + Content = new StringContent(string.Empty) + }; - return false; + //TODO: pass custom the authorization header, currently these aren't really secured! + //request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); + + try + { + var result = await wc.SendAsync(request, token); + return result.StatusCode == HttpStatusCode.OK; + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); + } + return false; + } } - public override void PerformRun() + public override bool PerformRun() { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { LogHelper.Debug("Does not run on slave servers."); - return; + return false; // do NOT repeat, server status comes from config and will NOT change + } + + // ensure we do not run if not main domain, but do NOT lock it + if (_appContext.MainDom.IsMainDom == false) + { + LogHelper.Debug("Does not run if not MainDom."); + return false; // do NOT repeat, going down } using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - try { - ProcessTasks(); + await ProcessTasksAsync(token); } catch (Exception ee) { LogHelper.Error("Error executing scheduled task", ee); } - finally - { - _isPublishingRunning = false; - } } - } - public override Task PerformRunAsync() - { - throw new NotImplementedException(); + return true; // repeat } public override bool IsAsync { - get { return false; } + get { return true; } } public override bool RunsOnShutdown diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 409a67028f..8831a81838 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,11 +1,7 @@ -using System; -using System.Threading; -using System.Web; +using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -18,7 +14,7 @@ namespace Umbraco.Web.Scheduling /// internal sealed class Scheduler : ApplicationEventHandler { - private static Timer _pingTimer; + private static BackgroundTaskRunner _keepAliveRunner; private static BackgroundTaskRunner _publishingRunner; private static BackgroundTaskRunner _tasksRunner; private static BackgroundTaskRunner _scrubberRunner; @@ -48,30 +44,24 @@ namespace Umbraco.Web.Scheduling LogHelper.Debug(() => "Initializing the scheduler"); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _keepAliveRunner = new BackgroundTaskRunner("KeepAlive"); _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing"); _tasksRunner = new BackgroundTaskRunner("ScheduledTasks"); _scrubberRunner = new BackgroundTaskRunner("LogScrubber"); var settings = UmbracoConfig.For.UmbracoSettings(); - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // also make the timer static to ensure further GC safety - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - // ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies - _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _pingTimer.Change(60000, 300000); + // ping/keepalive + // on all servers + _keepAliveRunner.Add(new KeepAlive(_keepAliveRunner, 60000, 300000, applicationContext)); // scheduled publishing/unpublishing // install on all, will only run on non-slaves servers - // both are delayed recurring tasks _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); // log scrubbing - // install & run on all servers - // LogScrubber is a delayed recurring task + // install on all, will only run on non-slaves servers _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); } } diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index f066476948..918a2c1542 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -105,9 +105,7 @@ namespace Umbraco.Web.Security .Where(p => member.ContentType.PropertyTypeExists(p.Alias)) .Where(property => member.Properties.Contains(property.Alias)) //needs to be editable - .Where(p => member.ContentType.MemberCanEditProperty(p.Alias)) - //needs to have a value - .Where(p => p.Value != null)) + .Where(p => member.ContentType.MemberCanEditProperty(p.Alias))) { member.Properties[property.Alias].Value = property.Value; } diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 22d6b5b54b..d7d331d887 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -81,9 +81,9 @@ namespace Umbraco.Web.Templates //set the doc that was found by id contentRequest.PublishedContent = doc; //set the template, either based on the AltTemplate found or the standard template of the doc - contentRequest.TemplateModel = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates || AltTemplate.HasValue == false - ? ApplicationContext.Current.Services.FileService.GetTemplate(doc.TemplateId) - : ApplicationContext.Current.Services.FileService.GetTemplate(AltTemplate.Value); + contentRequest.TemplateModel = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates || AltTemplate.HasValue == false + ? _umbracoContext.Application.Services.FileService.GetTemplate(doc.TemplateId) + : _umbracoContext.Application.Services.FileService.GetTemplate(AltTemplate.Value); //if there is not template then exit if (!contentRequest.HasTemplate) @@ -103,14 +103,20 @@ namespace Umbraco.Web.Templates //after this page has rendered. SaveExistingItems(); - //set the new items on context objects for this templates execution - SetNewItemsOnContextObjects(contentRequest); + try + { + //set the new items on context objects for this templates execution + SetNewItemsOnContextObjects(contentRequest); - //Render the template - ExecuteTemplateRendering(writer, contentRequest); - - //restore items on context objects to continuing rendering the parent template - RestoreItems(); + //Render the template + ExecuteTemplateRendering(writer, contentRequest); + } + finally + { + //restore items on context objects to continuing rendering the parent template + RestoreItems(); + } + } private void ExecuteTemplateRendering(TextWriter sw, PublishedContentRequest contentRequest) diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 3ff9884ba7..3ae74ba08b 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -20,6 +20,7 @@ 'lib/jquery-file-upload/jquery.fileupload-angular.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d6ed99c4cd..be9cc0881a 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -104,9 +104,9 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - - False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + True False @@ -115,9 +115,9 @@ ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - + False - ..\packages\Examine.0.1.63.0\lib\Examine.dll + ..\packages\Examine.0.1.65.0\lib\Examine.dll False @@ -268,6 +268,7 @@ + @@ -500,7 +501,6 @@ - diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 6efe4d4d6a..6f89864d9e 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -36,37 +36,46 @@ namespace Umbraco.Web { #region HttpModule event handlers + private static void EnsureApplicationUrl(HttpRequestBase request) + { + var appctx = ApplicationContext.Current; + + // already initialized = ok + // note that getting ApplicationUrl will ALSO try the various settings + if (appctx.UmbracoApplicationUrl.IsNullOrWhiteSpace() == false) return; + + // so if we reach that point, nothing was configured + // use the current request as application url + + // if (HTTP and SSL not required) or (HTTPS and SSL required), + // use ports from request + // otherwise, + // if non-standard ports used, + // user may need to set umbracoApplicationUrl manually per + // http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks + var port = (request.IsSecureConnection == false && GlobalSettings.UseSSL == false) + || (request.IsSecureConnection && GlobalSettings.UseSSL) + ? ":" + request.ServerVariables["SERVER_PORT"] + : ""; + + var ssl = GlobalSettings.UseSSL ? "s" : ""; // force, whatever the first request + var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); + + appctx.UmbracoApplicationUrl = UriUtility.TrimPathEndSlash(url); + LogHelper.Info("ApplicationUrl: " + appctx.UmbracoApplicationUrl + " (UmbracoModule request)"); + } + + /// /// Begins to process a request. /// /// static void BeginRequest(HttpContextBase httpContext) { + // ensure application url is initialized + EnsureApplicationUrl(httpContext.Request); - //we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must - //exist on a global context because the keep alive service doesn't run in a web context. - //we are NOT going to put a lock on this because locking will slow down the application and we don't really care - //if two threads write to this at the exact same time during first page hit. - //see: http://issues.umbraco.org/issue/U4-2059 - if (ApplicationContext.Current.OriginalRequestUrl.IsNullOrWhiteSpace()) - { - // If (HTTP and SSL not required) or (HTTPS and SSL required), use ports from request to configure OriginalRequestUrl. - // Otherwise, user may need to set baseUrl manually per http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks if non-standard ports used. - if ((!httpContext.Request.IsSecureConnection && !GlobalSettings.UseSSL) || (httpContext.Request.IsSecureConnection && GlobalSettings.UseSSL)) - { - // Use port from request. - ApplicationContext.Current.OriginalRequestUrl = string.Format("{0}:{1}{2}", httpContext.Request.ServerVariables["SERVER_NAME"], httpContext.Request.ServerVariables["SERVER_PORT"], IOHelper.ResolveUrl(SystemDirectories.Umbraco)); - } - else - { - // Omit port entirely. - ApplicationContext.Current.OriginalRequestUrl = string.Format("{0}{1}", httpContext.Request.ServerVariables["SERVER_NAME"], IOHelper.ResolveUrl(SystemDirectories.Umbraco)); - } - - LogHelper.Info("Setting OriginalRequestUrl: " + ApplicationContext.Current.OriginalRequestUrl); - } - - // do not process if client-side request + // do not process if client-side request if (httpContext.Request.Url.IsClientSideRequest()) return; diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 315d780852..72a521895e 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -1,24 +1,24 @@  - + - - + + - - - + + + - + - - + + \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ff08f36207..7673a9d75d 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,31 +1,30 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; -using System.Web.Hosting; using System.Xml; +using umbraco.BusinessLogic; +using umbraco.cms.businesslogic; +using umbraco.cms.businesslogic.web; +using umbraco.DataLayer; +using umbraco.presentation.nodeFactory; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.web; using Umbraco.Core.Models; using Umbraco.Core.Profiling; -using umbraco.DataLayer; -using umbraco.presentation.nodeFactory; using Umbraco.Web; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Scheduling; -using Node = umbraco.NodeFactory.Node; using File = System.IO.File; +using Node = umbraco.NodeFactory.Node; +using Task = System.Threading.Tasks.Task; namespace umbraco { @@ -36,58 +35,44 @@ namespace umbraco { private XmlCacheFilePersister _persisterTask; + private volatile bool _released; + #region Constructors private content() { if (SyncToXmlFile) { - // if we write to file, prepare the lock - // (if we don't use the file, or just read from it, no need to lock) - InitializeFileLock(); - - // and prepare the persister task + // prepare the persister task // there's always be one task keeping a ref to the runner // so it's safe to just create it as a local var here var runner = new BackgroundTaskRunner("XmlCacheFilePersister", new BackgroundTaskRunnerOptions { LongRunning = true, - KeepAlive = true + KeepAlive = true, + Hosted = false // main domain will take care of stopping the runner (see below) }); - // when the runner is terminating we need to ensure that no modifications - // to content are possible anymore, as they would not be written out to - // the xml file - unfortunately that is not possible in 7.x because we - // cannot lock the content service... and so we do nothing... - //runner.Terminating += (sender, args) => - //{ - //}; - - // when the runner has terminated we know we will not be writing to the file - // anymore, so we can release the lock now - no need to wait for the AppDomain - // unload - which means any "last minute" saves will be lost - but waiting for - // the AppDomain to unload has issues... - runner.Terminated += (sender, args) => - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - LogHelper.Debug("Release file lock."); - _fileLocked.Dispose(); - _fileLocked = null; - _fileLock = null; // ensure we don't lock again - } - }; - // create (and add to runner) _persisterTask = new XmlCacheFilePersister(runner, this); + + var registered = ApplicationContext.Current.MainDom.Register( + null, + () => + { + // once released, the cache still works but does not write to file anymore, + // which is OK with database server messenger but will cause data loss with + // another messenger... + + runner.Shutdown(false, true); // wait until flushed + _released = true; + }); + + // failed to become the main domain, we will never use the file + if (registered == false) + runner.Shutdown(false, true); + + _released = (registered == false); } // initialize content - populate the cache @@ -127,23 +112,26 @@ namespace umbraco private static readonly object DbReadSyncLock = new object(); private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; - private string _umbracoXmlDiskCacheFileName = string.Empty; + private static string _umbracoXmlDiskCacheFileName = string.Empty; private volatile XmlDocument _xmlContent; /// /// Gets the path of the umbraco XML disk cache file. /// /// The name of the umbraco XML disk cache file. + public static string GetUmbracoXmlDiskFileName() + { + if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) + { + _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); + } + return _umbracoXmlDiskCacheFileName; + } + + [Obsolete("Use the safer static GetUmbracoXmlDiskFileName() method instead to retrieve this value")] public string UmbracoXmlDiskCacheFileName { - get - { - if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) - { - _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); - } - return _umbracoXmlDiskCacheFileName; - } + get { return GetUmbracoXmlDiskFileName(); } set { _umbracoXmlDiskCacheFileName = value; } } @@ -204,7 +192,7 @@ namespace umbraco var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); var attr = ((XmlElement)node).GetAttributeNode("sortOrder"); attr.Value = d.sortOrder.ToString(); - AddOrUpdateXmlNode(xmlContentCopy, d.Id, d.Level, parentId, node); + xmlContentCopy = GetAddOrUpdateXmlNode(xmlContentCopy, d.Id, d.Level, parentId, node); // update sitemapprovider if (updateSitemapProvider && SiteMap.Provider is UmbracoSiteMapProvider) @@ -368,7 +356,7 @@ namespace umbraco { foreach (Document d in Documents) { - PublishNodeDo(d, safeXml.Xml, true); + safeXml.Xml = PublishNodeDo(d, safeXml.Xml, true); } } @@ -390,7 +378,18 @@ namespace umbraco public virtual void ClearDocumentCache(int documentId) { // Get the document - var d = new Document(documentId); + Document d; + try + { + d = new Document(documentId); + } + catch + { + // if we need the document to remove it... this cannot be LB?! + // shortcut everything here + ClearDocumentXmlCache(documentId); + return; + } ClearDocumentCache(d); } @@ -411,26 +410,8 @@ namespace umbraco // remove from xml db cache doc.XmlRemoveFromDB(); - // We need to lock content cache here, because we cannot allow other threads - // making changes at the same time, they need to be queued - using (var safeXml = GetSafeXmlReader()) - { - // Check if node present, before cloning - x = safeXml.Xml.GetElementById(doc.Id.ToString()); - if (x == null) - return; - - safeXml.UpgradeToWriter(false); - - // Find the document in the xml cache - x = safeXml.Xml.GetElementById(doc.Id.ToString()); - if (x != null) - { - // The document already exists in cache, so repopulate it - x.ParentNode.RemoveChild(x); - safeXml.Commit(); - } - } + // clear xml cache + ClearDocumentXmlCache(doc.Id); ClearContextCache(); @@ -446,6 +427,30 @@ namespace umbraco } } + internal void ClearDocumentXmlCache(int id) + { + // We need to lock content cache here, because we cannot allow other threads + // making changes at the same time, they need to be queued + using (var safeXml = GetSafeXmlReader()) + { + // Check if node present, before cloning + var x = safeXml.Xml.GetElementById(id.ToString()); + if (x == null) + return; + + safeXml.UpgradeToWriter(false); + + // Find the document in the xml cache + x = safeXml.Xml.GetElementById(id.ToString()); + if (x != null) + { + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + safeXml.Commit(); + } + } + } + /// /// Unpublishes the node. /// @@ -669,9 +674,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; { //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? - if (File.Exists(UmbracoXmlDiskCacheFileName)) + if (File.Exists(GetUmbracoXmlDiskFileName())) { - return new FileInfo(UmbracoXmlDiskCacheFileName).LastWriteTimeUtc; + return new FileInfo(GetUmbracoXmlDiskFileName()).LastWriteTimeUtc; } return DateTime.MinValue; @@ -801,7 +806,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return xmlDoc == null ? null : (XmlDocument) xmlDoc.CloneNode(true); } - private static void EnsureSchema(string contentTypeAlias, XmlDocument xml) + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -814,15 +819,22 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // ensure it contains the content type if (subset != null && subset.Contains(string.Format("", contentTypeAlias))) - return; + return xml; - // remove current doctype - xml.RemoveChild(n); + // alas, that does not work, replacing a doctype is ignored and GetElementById fails + // + //// remove current doctype, set new doctype + //xml.RemoveChild(n); + //subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); + //var doctype = xml.CreateDocumentType("root", null, null, subset); + //xml.InsertAfter(doctype, xml.FirstChild); - // set new doctype + var xml2 = new XmlDocument(); subset = string.Format("{0}{0}{2}", Environment.NewLine, contentTypeAlias, subset); - var doctype = xml.CreateDocumentType("root", null, null, subset); - xml.InsertAfter(doctype, xml.FirstChild); + var doctype = xml2.CreateDocumentType("root", null, null, subset); + xml2.AppendChild(doctype); + xml2.AppendChild(xml2.ImportNode(xml.DocumentElement, true)); + return xml2; } private static void InitializeXml(XmlDocument xml, string dtd) @@ -977,100 +989,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; private readonly string _xmlFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); private DateTime _lastFileRead; // last time the file was read private DateTime _nextFileCheck; // last time we checked whether the file was changed - private AsyncLock _fileLock; // protects the file - private IDisposable _fileLocked; // protects the file - - private const int FileLockTimeoutMilliseconds = 4*60*1000; // 4' - - private void InitializeFileLock() - { - // initialize file lock - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - // - // From MSDN C++ CreateSemaphore doc: - // "The name can have a "Global\" or "Local\" prefix to explicitly create the object in - // the global or session namespace. The remainder of the name can contain any character - // except the backslash character (\). For more information, see Kernel Object Namespaces." - // - // From MSDN "Kernel object namespaces" doc: - // "The separate client session namespaces enable multiple clients to run the same - // applications without interfering with each other. For processes started under - // a client session, the system uses the session namespace by default. However, these - // processes can use the global namespace by prepending the "Global\" prefix to the object name." - // - // just use "default" (whatever it is) for now - ie, no prefix - // - var name = HostingEnvironment.ApplicationID + "/XmlStore/XmlFile"; - _fileLock = new AsyncLock(name); - - // the file lock works with a shared, system-wide, semaphore - and we don't want - // to leak a count on that semaphore else the whole process will hang - so we have - // to ensure we dispose of the locker when the domain goes down - in theory the - // async lock should do it via its finalizer, but then there are some weird cases - // where the semaphore has been disposed of before it's been released, and then - // we'd need to GC-pin the semaphore... better dispose the locker explicitely - // when the app domain unloads. - - if (AppDomain.CurrentDomain.IsDefaultAppDomain()) - { - LogHelper.Debug("Registering Unload handler for default app domain."); - AppDomain.CurrentDomain.ProcessExit += OnDomainUnloadReleaseFileLock; - } - else - { - LogHelper.Debug("Registering Unload handler for non-default app domain."); - AppDomain.CurrentDomain.DomainUnload += OnDomainUnloadReleaseFileLock; - } - } - - private void EnsureFileLock() - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked != null) return; // locked already - - // thread-safety, acquire lock only once! - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLock == null) return; - if (_fileLocked != null) return; - - // don't hang forever, throws if it cannot lock within the timeout - LogHelper.Debug("Acquiring exclusive access to file for this AppDomain..."); - _fileLocked = _fileLock.Lock(FileLockTimeoutMilliseconds); - LogHelper.Debug("Acquired exclusive access to file for this AppDomain."); - } - } - - private void OnDomainUnloadReleaseFileLock(object sender, EventArgs args) - { - // the unload event triggers AFTER all hosted objects (eg the file persister - // background task runner) have been stopped, so we should have released the - // lock already - this is for safety - might be possible to get rid of it - - // NOTE - // trying to write to the log via LogHelper at that point is a BAD idea - // it can lead to ugly deadlocks with the named semaphore - DONT do it - - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - // in case you really need to debug... that should be safe... - //System.IO.File.AppendAllText(HostingEnvironment.MapPath("~/App_Data/log.txt"), string.Format("{0} {1} unlock", DateTime.Now, AppDomain.CurrentDomain.Id)); - _fileLocked.Dispose(); - - _fileLock = null; // ensure we don't lock again - } - } // not used - just try to read the file //private bool XmlFileExists @@ -1092,7 +1010,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) internal void SaveXmlToFile() { LogHelper.Info("Save Xml to file..."); @@ -1102,8 +1022,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1111,7 +1029,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1132,8 +1050,10 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock - internal async System.Threading.Tasks.Task SaveXmlToFileAsync() + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) + internal async Task SaveXmlToFileAsync() { LogHelper.Info("Save Xml to file..."); @@ -1142,8 +1062,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1151,7 +1069,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1200,17 +1118,15 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return sb.ToString(); } - // assumes file lock private XmlDocument LoadXmlFromFile() { + // do NOT try to load if we are not the main domain anymore + if (_released) return null; + LogHelper.Info("Load Xml from file..."); try { - // if we're not writing back to the file, no need to lock - if (SyncToXmlFile) - EnsureFileLock(); - var xml = new XmlDocument(); using (var fs = new FileStream(_xmlFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { @@ -1233,12 +1149,11 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // (files is always locked) private void DeleteXmlFile() { - if (System.IO.File.Exists(_xmlFileName) == false) return; - System.IO.File.SetAttributes(_xmlFileName, FileAttributes.Normal); - System.IO.File.Delete(_xmlFileName); + if (File.Exists(_xmlFileName) == false) return; + File.SetAttributes(_xmlFileName, FileAttributes.Normal); + File.Delete(_xmlFileName); } private void ReloadXmlFromFileIfChanged() @@ -1268,8 +1183,15 @@ order by umbracoNode.level, umbracoNode.sortOrder"; #region Manage change - // adds or updates a node (docNode) into a cache (xml) + //TODO remove as soon as we can break backward compatibility + [Obsolete("Use GetAddOrUpdateXmlNode which returns an updated Xml document.", false)] public static void AddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) + { + GetAddOrUpdateXmlNode(xml, id, level, parentId, docNode); + } + + // adds or updates a node (docNode) into a cache (xml) + public static XmlDocument GetAddOrUpdateXmlNode(XmlDocument xml, int id, int level, int parentId, XmlNode docNode) { // sanity checks if (id != docNode.AttributeValue("id")) @@ -1283,7 +1205,12 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // if the document is not there already then it's a new document // we must make sure that its document type exists in the schema if (currentNode == null && UseLegacySchema == false) - EnsureSchema(docNode.Name, xml); + { + var xml2 = EnsureSchema(docNode.Name, xml); + if (ReferenceEquals(xml, xml2) == false) + docNode = xml2.ImportNode(docNode, true); + xml = xml2; + } // find the parent XmlNode parentNode = level == 1 @@ -1292,7 +1219,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // no parent = cannot do anything if (parentNode == null) - return; + return xml; // insert/move the node under the parent if (currentNode == null) @@ -1360,6 +1287,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // then we just need to ensure that currentNode is at the right position. // should be faster that moving all the nodes around. XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + return xml; } private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode documentNode, XmlNode publishedNode) diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index 6d80d8bedd..abc3425f3f 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -18,9 +18,7 @@ namespace umbraco.presentation var appContext = (ApplicationContext) sender; - //TODO: This won't always work, in load balanced scenarios ping will not work because - // this original request url will be public and not internal to the server. - var url = string.Format("http://{0}/ping.aspx", appContext.OriginalRequestUrl); + var url = appContext.UmbracoApplicationUrl + "/ping.aspx"; try { using (var wc = new WebClient()) diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 7b9c63e6b8..136c30c799 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -537,8 +537,8 @@ namespace umbraco /// private Control AddMacroResultToCache(Control macroControl) { - // Add result to cache if successful - if (Model.CacheDuration > 0) + // Add result to cache if successful (and cache is enabled) + if (UmbracoContext.Current.InPreviewMode == false && Model.CacheDuration > 0) { // do not add to cache if there's no member and it should cache by personalization if (!Model.CacheByMember || (Model.CacheByMember && Member.IsLoggedOn())) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs index c9d7a21dfb..436ead7c93 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs @@ -67,7 +67,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string JsSource { - get { return "/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.js"; } + get { return "~/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.js"; } } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs index 2dcbbfb077..4b36559da2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs @@ -67,7 +67,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string JsSource { - get { return "/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.js"; } + get { return "~/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.js"; } } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 6a117e732c..e2a2588620 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -102,7 +102,7 @@ namespace umbraco.presentation.preview if (document.Content.Published == false && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - content.AddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); + XmlContent = content.GetAddOrUpdateXmlNode(XmlContent, document.Id, document.Level, parentId, previewXml); } if (includeSubs) @@ -112,7 +112,7 @@ namespace umbraco.presentation.preview var previewXml = XmlContent.ReadNode(XmlReader.Create(new StringReader(prevNode.Xml))); if (prevNode.IsDraft) previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); - content.AddOrUpdateXmlNode(XmlContent, prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml); + XmlContent = content.GetAddOrUpdateXmlNode(XmlContent, prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml); } } diff --git a/src/UmbracoExamine/Config/IndexSetExtensions.cs b/src/UmbracoExamine/Config/IndexSetExtensions.cs index ac432b16ec..1255f50a3c 100644 --- a/src/UmbracoExamine/Config/IndexSetExtensions.cs +++ b/src/UmbracoExamine/Config/IndexSetExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Examine; @@ -17,60 +16,7 @@ namespace UmbracoExamine.Config internal static IIndexCriteria ToIndexCriteria(this IndexSet set, IDataService svc, IEnumerable indexFieldPolicies) { - - var attributeFields = set.IndexAttributeFields.Cast().ToArray(); - var userFields = set.IndexUserFields.Cast().ToArray(); - var includeNodeTypes = set.IncludeNodeTypes.ToList().Select(x => x.Name).ToArray(); - var excludeNodeTypes = set.ExcludeNodeTypes.ToList().Select(x => x.Name).ToArray(); - var parentId = set.IndexParentId; - - //if there are no user fields defined, we'll populate them from the data source (include them all) - if (set.IndexUserFields.Count == 0) - { - //we need to add all user fields to the collection if it is empty (this is the default if none are specified) - var userProps = svc.ContentService.GetAllUserPropertyNames(); - var fields = new List(); - foreach (var u in userProps) - { - var field = new IndexField() { Name = u }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == u); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } - fields.Add(field); - } - userFields = fields.ToArray(); - } - - //if there are no attribute fields defined, we'll populate them from the data source (include them all) - if (set.IndexAttributeFields.Count == 0) - { - //we need to add all system fields to the collection if it is empty (this is the default if none are specified) - var sysProps = svc.ContentService.GetAllSystemPropertyNames(); - var fields = new List(); - foreach (var s in sysProps) - { - var field = new IndexField() { Name = s }; - var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == s); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } - fields.Add(field); - } - attributeFields = fields.ToArray(); - } - - - return new IndexCriteria( - attributeFields, - userFields, - includeNodeTypes, - excludeNodeTypes, - parentId); + return new LazyIndexCriteria(set, svc, indexFieldPolicies); } /// diff --git a/src/UmbracoExamine/Config/LazyIndexCriteria.cs b/src/UmbracoExamine/Config/LazyIndexCriteria.cs new file mode 100644 index 0000000000..72ab3f31ba --- /dev/null +++ b/src/UmbracoExamine/Config/LazyIndexCriteria.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Config; +using UmbracoExamine.DataServices; + +namespace UmbracoExamine.Config +{ + internal class LazyIndexCriteria : IIndexCriteria + { + public LazyIndexCriteria( + IndexSet set, + IDataService svc, + IEnumerable indexFieldPolicies) + { + if (set == null) throw new ArgumentNullException("set"); + if (indexFieldPolicies == null) throw new ArgumentNullException("indexFieldPolicies"); + if (svc == null) throw new ArgumentNullException("svc"); + + _lazyCriteria = new Lazy(() => + { + var attributeFields = set.IndexAttributeFields.Cast().ToArray(); + var userFields = set.IndexUserFields.Cast().ToArray(); + var includeNodeTypes = set.IncludeNodeTypes.Cast().Select(x => x.Name).ToArray(); + var excludeNodeTypes = set.ExcludeNodeTypes.Cast().Select(x => x.Name).ToArray(); + var parentId = set.IndexParentId; + + //if there are no user fields defined, we'll populate them from the data source (include them all) + if (set.IndexUserFields.Count == 0) + { + //we need to add all user fields to the collection if it is empty (this is the default if none are specified) + var userProps = svc.ContentService.GetAllUserPropertyNames(); + var fields = new List(); + foreach (var u in userProps) + { + var field = new IndexField() { Name = u }; + var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == u); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } + fields.Add(field); + } + userFields = fields.ToArray(); + } + + //if there are no attribute fields defined, we'll populate them from the data source (include them all) + if (set.IndexAttributeFields.Count == 0) + { + //we need to add all system fields to the collection if it is empty (this is the default if none are specified) + var sysProps = svc.ContentService.GetAllSystemPropertyNames(); + var fields = new List(); + foreach (var s in sysProps) + { + var field = new IndexField() { Name = s }; + var policy = indexFieldPolicies.FirstOrDefault(x => x.Name == s); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } + fields.Add(field); + } + attributeFields = fields.ToArray(); + } + + + return new IndexCriteria( + attributeFields, + userFields, + includeNodeTypes, + excludeNodeTypes, + parentId); + }); + } + + private readonly Lazy _lazyCriteria; + + public IEnumerable ExcludeNodeTypes + { + get { return _lazyCriteria.Value.ExcludeNodeTypes; } + } + + public IEnumerable IncludeNodeTypes + { + get { return _lazyCriteria.Value.IncludeNodeTypes; } + } + + public int? ParentNodeId + { + get { return _lazyCriteria.Value.ParentNodeId; } + } + + public IEnumerable StandardFields + { + get { return _lazyCriteria.Value.StandardFields; } + } + + public IEnumerable UserFields + { + get { return _lazyCriteria.Value.UserFields; } + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index b7fc35542a..90e246e3cd 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -82,9 +82,9 @@ ..\Solution Items\TheFARM-Public.snk - + False - ..\packages\Examine.0.1.63.0\lib\Examine.dll + ..\packages\Examine.0.1.65.0\lib\Examine.dll False @@ -111,6 +111,7 @@ + diff --git a/src/UmbracoExamine/packages.config b/src/UmbracoExamine/packages.config index efa216d153..fad9b5dabd 100644 --- a/src/UmbracoExamine/packages.config +++ b/src/UmbracoExamine/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/packages.config b/src/umbraco.MacroEngines/packages.config index 9951ba7e01..b259997c0e 100644 --- a/src/umbraco.MacroEngines/packages.config +++ b/src/umbraco.MacroEngines/packages.config @@ -1,15 +1,15 @@  - - + + - - + + - + diff --git a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj index a34198b5ce..4e414f3c69 100644 --- a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj +++ b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj @@ -45,9 +45,9 @@ false - + False - ..\packages\Examine.0.1.63.0\lib\Examine.dll + ..\packages\Examine.0.1.65.0\lib\Examine.dll False diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 51f059dbb5..8e7f1f47e1 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; +using System.Web.UI; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -181,7 +182,14 @@ namespace umbraco.BasePages /// public static int GetUserId() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity == null) return -1; return Convert.ToInt32(identity.Id); @@ -205,7 +213,14 @@ namespace umbraco.BasePages /// public static bool ValidateCurrentUser() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity != null) { return true; @@ -232,7 +247,14 @@ namespace umbraco.BasePages { get { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + return identity == null ? "" : identity.SessionId; } set diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index 075cd84436..724ae421da 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -13,6 +13,7 @@ using umbraco.cms.businesslogic.workflow; using umbraco.interfaces; using System.Text.RegularExpressions; using System.Linq; +using Umbraco.Core.IO; using TypeFinder = Umbraco.Core.TypeFinder; namespace umbraco.BusinessLogic.Actions @@ -88,7 +89,7 @@ namespace umbraco.BusinessLogic.Actions { return ActionsResolver.Current.Actions .Where(x => !string.IsNullOrWhiteSpace(x.JsSource)) - .Select(x => x.JsSource).ToList(); + .Select(x => IOHelper.ResolveUrl(x.JsSource)).ToList(); //return ActionJsReference; } diff --git a/src/umbraco.cms/packages.config b/src/umbraco.cms/packages.config index bdaf00b95d..539757211e 100644 --- a/src/umbraco.cms/packages.config +++ b/src/umbraco.cms/packages.config @@ -1,8 +1,8 @@  - - + + - - + + \ No newline at end of file diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index 7f81309863..708c5f4d27 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -106,9 +106,9 @@ false - - False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + True False diff --git a/src/umbraco.controls/packages.config b/src/umbraco.controls/packages.config index 0859f3d4e9..1e49726191 100644 --- a/src/umbraco.controls/packages.config +++ b/src/umbraco.controls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.controls/umbraco.controls.csproj b/src/umbraco.controls/umbraco.controls.csproj index a5c08e525a..3980e5ed27 100644 --- a/src/umbraco.controls/umbraco.controls.csproj +++ b/src/umbraco.controls/umbraco.controls.csproj @@ -68,9 +68,9 @@ false - - False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + True diff --git a/src/umbraco.editorControls/packages.config b/src/umbraco.editorControls/packages.config index 0859f3d4e9..1e49726191 100644 --- a/src/umbraco.editorControls/packages.config +++ b/src/umbraco.editorControls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.editorControls/umbraco.editorControls.csproj b/src/umbraco.editorControls/umbraco.editorControls.csproj index 1266015268..f49aac9dcb 100644 --- a/src/umbraco.editorControls/umbraco.editorControls.csproj +++ b/src/umbraco.editorControls/umbraco.editorControls.csproj @@ -114,9 +114,9 @@ {651E1350-91B6-44B7-BD60-7207006D7003} Umbraco.Web - - False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + True System