diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 2ec80299ea..54c6cf2dc5 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -31,10 +31,10 @@ - + - - + + diff --git a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs index e88a51021a..a8307044a1 100644 --- a/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs +++ b/src/Umbraco.Core/Cache/DictionaryCacheProviderBase.cs @@ -1,218 +1,204 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System.Threading; namespace Umbraco.Core.Cache { internal abstract class DictionaryCacheProviderBase : ICacheProvider { - protected static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - protected abstract DictionaryCacheWrapper DictionaryCache { get; } - - /// - /// Clears everything in umbraco's runtime cache - /// - /// - /// Does not clear other stuff the user has put in httpruntime.cache! - /// - public virtual void ClearAllCache() - { - using (new WriteLock(Locker)) - { - var keysToRemove = DictionaryCache.Cast() - .Select(item => new DictionaryItemWrapper(item)) - .Where(c => c.Key is string && ((string)c.Key).StartsWith(CacheItemPrefix) && DictionaryCache[c.Key.ToString()] != null) - .Select(c => c.Key) - .ToList(); - - foreach (var k in keysToRemove) - { - DictionaryCache.Remove(k); - } - } - } - - /// - /// Clears the item in umbraco's runtime cache with the given key - /// - /// Key - public virtual void ClearCacheItem(string key) - { - using (new WriteLock(Locker)) - { - if (DictionaryCache[GetCacheKey(key)] == null) return; - DictionaryCache.Remove(GetCacheKey(key)); ; - } - } - - /// - /// Clears all objects in the System.Web.Cache with the System.Type name as the - /// input parameter. (using [object].GetType()) - /// - /// The name of the System.Type which should be cleared from cache ex "System.Xml.XmlDocument" - public virtual void ClearCacheObjectTypes(string typeName) - { - using (new WriteLock(Locker)) - { - var keysToRemove = DictionaryCache - .Cast() - .Select(item => new DictionaryItemWrapper(item)) - .Where(c => - { - var k = c.Key.ToString(); - var v = DictionaryCache[k]; - return v != null && v.GetType().ToString().InvariantEquals(typeName); - }) - .Select(c => c.Key) - .ToList(); - - foreach (var k in keysToRemove) - DictionaryCache.Remove(k); - } - } - - public virtual void ClearCacheObjectTypes() - { - using (new WriteLock(Locker)) - { - var typeOfT = typeof(T); - var keysToRemove = DictionaryCache - .Cast() - .Select(item => new DictionaryItemWrapper(item)) - .Where(c => - { - var k = c.Key.ToString(); - var v = DictionaryCache[k]; - return v != null && v.GetType() == typeOfT; - }) - .Select(c => c.Key) - .ToList(); - - foreach (var k in keysToRemove) - DictionaryCache.Remove(k); - } - } - - public virtual void ClearCacheObjectTypes(Func predicate) - { - using (new WriteLock(Locker)) - { - var typeOfT = typeof(T); - var keysToRemove = DictionaryCache - .Cast() - .Select(item => new DictionaryItemWrapper(item)) - .Where(c => - { - var k = c.Key.ToString(); - var v = DictionaryCache[k]; - return v != null && v.GetType() == typeOfT && predicate(k, (T)v); - }) - .Select(c => c.Key) - .ToList(); - - foreach (var k in keysToRemove) - DictionaryCache.Remove(k); - } - } - - /// - /// Clears all cache items that starts with the key passed. - /// - /// The start of the key - public virtual void ClearCacheByKeySearch(string keyStartsWith) - { - var keysToRemove = DictionaryCache.Cast() - .Select(item => new DictionaryItemWrapper(item)) - .Where(c => c.Key is string && ((string)c.Key).InvariantStartsWith(string.Format("{0}-{1}", CacheItemPrefix, keyStartsWith))) - .Select(c => c.Key) - .ToList(); - - foreach (var k in keysToRemove) - { - DictionaryCache.Remove(k); - } - } - - /// - /// Clears all cache items that have a key that matches the regular expression - /// - /// - public virtual void ClearCacheByKeyExpression(string regexString) - { - var keysToRemove = new List(); - foreach (var item in DictionaryCache) - { - var c = new DictionaryItemWrapper(item); - var s = c.Key as string; - if (s != null) - { - var withoutPrefix = s.TrimStart(string.Format("{0}-", CacheItemPrefix)); - if (Regex.IsMatch(withoutPrefix, regexString)) - { - keysToRemove.Add(c.Key); - } - } - } - - foreach (var k in keysToRemove) - { - DictionaryCache.Remove(k); - } - } - - public virtual IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) - { - return (from object item in DictionaryCache - select new DictionaryItemWrapper(item) - into c - where c.Key is string && ((string) c.Key).InvariantStartsWith(string.Format("{0}-{1}", CacheItemPrefix, keyStartsWith)) - select c.Value).ToList(); - } - - public IEnumerable GetCacheItemsByKeyExpression(string regexString) - { - var found = new List(); - foreach (var item in DictionaryCache) - { - var c = new DictionaryItemWrapper(item); - var s = c.Key as string; - if (s != null) - { - var withoutPrefix = s.TrimStart(string.Format("{0}-", CacheItemPrefix)); - if (Regex.IsMatch(withoutPrefix, regexString)) - { - found.Add(c.Value); - } - } - } - - return found; - } - - /// - /// Returns a cache item by key, does not update the cache if it isn't there. - /// - /// - /// - public virtual object GetCacheItem(string cacheKey) - { - var result = DictionaryCache.Get(GetCacheKey(cacheKey)); - return result; - } - - public abstract object GetCacheItem(string cacheKey, Func getCacheItem); - - /// - /// We prefix all cache keys with this so that we know which ones this class has created when - /// using the HttpRuntime cache so that when we clear it we don't clear other entries we didn't create. - /// + // prefix cache keys so we know which one are ours protected const string CacheItemPrefix = "umbrtmche"; + // an object that represent a value that has not been created yet + protected readonly object ValueNotCreated = new object(); + + // manupulate the underlying cache entries + // these *must* be called from within the appropriate locks + // and use the full prefixed cache keys + protected abstract IEnumerable GetDictionaryEntries(); + protected abstract void RemoveEntry(string key); + protected abstract object GetEntry(string key); + + // read-write lock the underlying cache + protected abstract IDisposable ReadLock { get; } + protected abstract IDisposable WriteLock { get; } + protected string GetCacheKey(string key) { return string.Format("{0}-{1}", CacheItemPrefix, key); } + + protected object GetSafeLazyValue(Lazy lazy, bool onlyIfValueIsCreated = false) + { + try + { + // if onlyIfValueIsCreated, do not trigger value creation + // must return something, though, to differenciate from null values + if (onlyIfValueIsCreated && lazy.IsValueCreated == false) return ValueNotCreated; + return lazy.Value; + } + catch + { + return null; + } + } + + #region Clear + + public virtual void ClearAllCache() + { + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .ToArray()) + RemoveEntry((string) entry.Key); + } + } + + public virtual void ClearCacheItem(string key) + { + var cacheKey = GetCacheKey(key); + using (WriteLock) + { + RemoveEntry(cacheKey); + } + } + + public virtual void ClearCacheObjectTypes(string typeName) + { + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // 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); + }) + .ToArray()) + RemoveEntry((string) entry.Key); + } + } + + public virtual void ClearCacheObjectTypes() + { + var typeOfT = typeof(T); + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // 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; + }) + .ToArray()) + RemoveEntry((string) entry.Key); + } + } + + public virtual void ClearCacheObjectTypes(Func predicate) + { + var typeOfT = typeof(T); + var plen = CacheItemPrefix.Length + 1; + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .Where(x => + { + // entry.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // compare on exact type, don't use "is" + // get non-created as NonCreatedValue & exceptions as null + var value = GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) return true; + return value.GetType() == typeOfT + // run predicate on the 'public key' part only, ie without prefix + && predicate(((string)x.Key).Substring(plen), (T)value); + })) + RemoveEntry((string) entry.Key); + } + } + + public virtual void ClearCacheByKeySearch(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray()) + RemoveEntry((string) entry.Key); + } + } + + public virtual void ClearCacheByKeyExpression(string regexString) + { + var plen = CacheItemPrefix.Length + 1; + using (WriteLock) + { + foreach (var entry in GetDictionaryEntries() + .Where(x => Regex.IsMatch(((string)x.Key).Substring(plen), regexString)) + .ToArray()) + RemoveEntry((string) entry.Key); + } + } + + #endregion + + #region Get + + public virtual IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) + { + var plen = CacheItemPrefix.Length + 1; + IEnumerable entries; + using (ReadLock) + { + entries = GetDictionaryEntries() + .Where(x => ((string)x.Key).Substring(plen).InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + return entries + .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null); // backward compat, don't store null values in the cache + } + + public virtual IEnumerable GetCacheItemsByKeyExpression(string regexString) + { + const string prefix = CacheItemPrefix + "-"; + var plen = prefix.Length; + IEnumerable entries; + using (ReadLock) + { + entries = GetDictionaryEntries() + .Where(x => Regex.IsMatch(((string)x.Key).Substring(plen), regexString)) + .ToArray(); // evaluate while locked + } + return entries + .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null); // backward compat, don't store null values in the cache + } + + public virtual object GetCacheItem(string cacheKey) + { + cacheKey = GetCacheKey(cacheKey); + Lazy result; + using (ReadLock) + { + result = GetEntry(cacheKey) as Lazy; // null if key not found + } + return result == null ? null : GetSafeLazyValue(result); // return exceptions as null + } + + public abstract object GetCacheItem(string cacheKey, Func getCacheItem); + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DictionaryCacheWrapper.cs b/src/Umbraco.Core/Cache/DictionaryCacheWrapper.cs deleted file mode 100644 index 840a1c01b3..0000000000 --- a/src/Umbraco.Core/Cache/DictionaryCacheWrapper.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections; - -namespace Umbraco.Core.Cache -{ - internal class DictionaryCacheWrapper : IEnumerable - { - private readonly IEnumerable _inner; - private readonly Func _get; - private readonly Action _remove; - - public DictionaryCacheWrapper( - IEnumerable inner, - Func get, - Action remove) - { - _inner = inner; - _get = get; - _remove = remove; - } - - public object this[object key] - { - get - { - return Get(key); - } - } - - public object Get(object key) - { - return _get(key); - } - - public void Remove(object key) - { - _remove(key); - } - - public IEnumerator GetEnumerator() - { - return _inner.GetEnumerator(); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DictionaryItemWrapper.cs b/src/Umbraco.Core/Cache/DictionaryItemWrapper.cs deleted file mode 100644 index 724f5f43b0..0000000000 --- a/src/Umbraco.Core/Cache/DictionaryItemWrapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Umbraco.Core.Cache -{ - internal class DictionaryItemWrapper - { - public DictionaryItemWrapper(dynamic item) - { - Key = item.Key; - Value = item.Value; - } - - public object Key { get; private set; } - public object Value { get; private set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index b71317b31d..0a95ff6fd2 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Web; namespace Umbraco.Core.Cache @@ -12,36 +11,115 @@ namespace Umbraco.Core.Cache /// internal class HttpRequestCacheProvider : DictionaryCacheProviderBase { - private readonly Func _context; + // context provider + // the idea is that there is only one, application-wide HttpRequestCacheProvider instance, + // that is initialized with a method that returns the "current" context. + // NOTE + // but then it is initialized with () => new HttpContextWrapper(HttpContent.Current) + // which is higly inefficient because it creates a new wrapper each time we refer to _context() + // so replace it with _context1 and _context2 below + a way to get context.Items. + //private readonly Func _context; - public HttpRequestCacheProvider(HttpContext context) + // NOTE + // and then in almost 100% cases _context2 will be () => HttpContext.Current + // so why not bring that logic in here and fallback on to HttpContext.Current when + // _context1 is null? + //private readonly HttpContextBase _context1; + //private readonly Func _context2; + private readonly HttpContextBase _context; + + private IDictionary ContextItems { - _context = () => new HttpContextWrapper(context); + //get { return _context1 != null ? _context1.Items : _context2().Items; } + get { return _context != null ? _context.Items : HttpContext.Current.Items; } } - public HttpRequestCacheProvider(Func context) + // for unit tests + public HttpRequestCacheProvider(HttpContextBase context) { _context = context; } - protected override DictionaryCacheWrapper DictionaryCache + // main constructor + // will use HttpContext.Current + public HttpRequestCacheProvider(/*Func context*/) { + //_context2 = context; + } + + protected override IEnumerable GetDictionaryEntries() + { + const string prefix = CacheItemPrefix + "-"; + return ContextItems.Cast() + .Where(x => x.Key is string && ((string)x.Key).StartsWith(prefix)); + } + + protected override void RemoveEntry(string key) + { + ContextItems.Remove(key); + } + + protected override object GetEntry(string key) + { + return ContextItems[key]; + } + + #region Lock + + protected override IDisposable ReadLock + { + // there's no difference between ReadLock and WriteLock here + get { return WriteLock; } + } + + protected override IDisposable WriteLock + { + // NOTE + // could think about just overriding base.Locker to return a different + // object but then we'd create a ReaderWriterLockSlim per request, + // which is less efficient than just using a basic monitor lock. + get { - var ctx = _context(); - return new DictionaryCacheWrapper( - ctx.Items, - o => ctx.Items[o], - o => ctx.Items.Remove(o)); + return new MonitorLock(ContextItems.SyncRoot); } } + #endregion + + #region Get + public override object GetCacheItem(string cacheKey, Func getCacheItem) { - var ctx = _context(); - var ck = GetCacheKey(cacheKey); - return ctx.Items[ck] ?? (ctx.Items[ck] = getCacheItem()); + cacheKey = GetCacheKey(cacheKey); + + Lazy result; + + using (WriteLock) + { + result = ContextItems[cacheKey] as Lazy; // null if key not found + + // 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 = new Lazy(getCacheItem); + ContextItems[cacheKey] = result; + } + } + + // this may throw if getCacheItem throws, but this is the only place where + // it would throw as everywhere else we use GetLazySaveValue() to hide exceptions + // and pretend exceptions were never inserted into cache to begin with. + return result.Value; } + + #endregion + + #region Insert + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index c52d27f24a..c5870f26e8 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -1,10 +1,9 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Web; using System.Web.Caching; -using Umbraco.Core.Logging; using CacheItemPriority = System.Web.Caching.CacheItemPriority; namespace Umbraco.Core.Cache @@ -14,48 +13,52 @@ namespace Umbraco.Core.Cache /// internal class HttpRuntimeCacheProvider : DictionaryCacheProviderBase, IRuntimeCacheProvider { + // locker object that supports upgradeable read locking + // does not need to support recursion if we implement the cache correctly and ensure + // that methods cannot be reentrant, ie we do NOT create values while holding a lock. + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + private readonly System.Web.Caching.Cache _cache; - private readonly DictionaryCacheWrapper _wrapper; - + public HttpRuntimeCacheProvider(System.Web.Caching.Cache cache) { _cache = cache; - _wrapper = new DictionaryCacheWrapper(_cache, s => _cache.Get(s.ToString()), o => _cache.Remove(o.ToString())); } - protected override DictionaryCacheWrapper DictionaryCache + protected override IEnumerable GetDictionaryEntries() { - get { return _wrapper; } + const string prefix = CacheItemPrefix + "-"; + return _cache.Cast() + .Where(x => x.Key is string && ((string) x.Key).StartsWith(prefix)); } + protected override void RemoveEntry(string key) + { + _cache.Remove(key); + } + + protected override object GetEntry(string key) + { + return _cache.Get(key); + } + + #region Lock + + protected override IDisposable ReadLock + { + get { return new ReadLock(_locker); } + } + + protected override IDisposable WriteLock + { + get { return new WriteLock(_locker); } + } + + #endregion + + #region Get + /// - /// Clears all objects in the System.Web.Cache with the System.Type specified that satisfy the predicate - /// - public override void ClearCacheObjectTypes(Func predicate) - { - try - { - lock (Locker) - { - foreach (DictionaryEntry c in _cache) - { - var key = c.Key.ToString(); - if (_cache[key] != null - && _cache[key] is T - && predicate(key, (T)_cache[key])) - { - _cache.Remove(c.Key.ToString()); - } - } - } - } - catch (Exception e) - { - LogHelper.Error("Cache clearing error", e); - } - } - - /// /// Gets (and adds if necessary) an item from the cache with all of the default parameters /// /// @@ -81,25 +84,64 @@ namespace Umbraco.Core.Cache { cacheKey = GetCacheKey(cacheKey); - using (var lck = new UpgradeableReadLock(Locker)) + // NOTE - because we don't know what getCacheItem does, how long it will take and whether it will hang, + // getCacheItem should run OUTSIDE of the global application lock else we run into lock contention and + // nasty performance issues. + + // So.... we insert a Lazy in the cache while holding the global application lock, and then rely + // on the Lazy lock to ensure that getCacheItem runs once and everybody waits on it, while the global + // application lock has been released. + + // NOTE + // The Lazy value creation may produce a null value. + // Must make sure (for backward compatibility) that we pretend they are not in the cache. + // So if we find an entry in the cache that already has its value created and is null, + // pretend it was not there. If value is not already created, wait... and return null, that's + // what prior code did. + + // NOTE + // The Lazy value creation may throw. + + // So... the null value _will_ be in the cache but never returned + + Lazy result; + + // Fast! + // Only one thread can enter an UpgradeableReadLock at a time, but it does not prevent other + // threads to enter a ReadLock in the meantime -- only upgrading to WriteLock will prevent all + // reads. We first try with a normal ReadLock for maximum concurrency and take the penalty of + // having to re-lock in case there's no value. Would need to benchmark to figure out whether + // it's worth it, though... + using (new ReadLock(_locker)) { - var result = DictionaryCache.Get(cacheKey); - if (result == null) - { - lck.UpgradeToWriteLock(); - - result = getCacheItem(); - if (result != null) - { - 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); - - _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); - } - - } - return result; + result = _cache.Get(cacheKey) as Lazy; // null if key not found } + var value = result == null ? null : GetSafeLazyValue(result); + if (value != null) return value; + + using (var lck = new UpgradeableReadLock(_locker)) + { + result = _cache.Get(cacheKey) as Lazy; // null if key not found + + // 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 = new Lazy(getCacheItem); + 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); + + lck.UpgradeToWriteLock(); + _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); + } + } + + // this may throw if getCacheItem throws, but this is the only place where + // it would throw as everywhere else we use GetLazySaveValue() to hide exceptions + // and pretend exceptions were never inserted into cache to begin with. + return result.Value; } public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) @@ -112,6 +154,10 @@ namespace Umbraco.Core.Cache return GetCacheItem(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, dependency); } + #endregion + + #region Insert + /// /// This overload is here for legacy purposes /// @@ -124,15 +170,22 @@ namespace Umbraco.Core.Cache /// internal void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) { - var result = getCacheItem(); - if (result == null) return; + // NOTE - here also we must insert a Lazy but we can evaluate it right now + // and make sure we don't store a null value. + + var result = new Lazy(getCacheItem); + 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); 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); - _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); + using (new WriteLock(_locker)) + { + _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); + } } public void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) @@ -144,5 +197,7 @@ namespace Umbraco.Core.Cache } InsertCacheItem(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, dependency); } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs index 7b1053c8a4..edf1ba5aa6 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheRuntimeCacheProvider.cs @@ -16,17 +16,37 @@ namespace Umbraco.Core.Cache /// internal class ObjectCacheRuntimeCacheProvider : IRuntimeCacheProvider { - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); internal ObjectCache MemoryCache; + // an object that represent a value that has not been created yet + protected readonly object ValueNotCreated = new object(); + public ObjectCacheRuntimeCacheProvider() { MemoryCache = new MemoryCache("in-memory"); } + protected object GetSafeLazyValue(Lazy lazy, bool onlyIfValueIsCreated = false) + { + try + { + // if onlyIfValueIsCreated, do not trigger value creation + // must return something, though, to differenciate from null values + if (onlyIfValueIsCreated && lazy.IsValueCreated == false) return ValueNotCreated; + return lazy.Value; + } + catch + { + return null; + } + } + + #region Clear + public virtual void ClearAllCache() { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { MemoryCache.DisposeIfDisposable(); MemoryCache = new MemoryCache("in-memory"); @@ -35,7 +55,7 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheItem(string key) { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { if (MemoryCache[key] == null) return; MemoryCache.Remove(key); @@ -44,131 +64,179 @@ namespace Umbraco.Core.Cache public virtual void ClearCacheObjectTypes(string typeName) { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { - var keysToRemove = MemoryCache - .Where(c => c.Value != null && c.Value.GetType().ToString().InvariantEquals(typeName)) - .Select(c => c.Key) - .ToArray(); - foreach (var k in keysToRemove) - MemoryCache.Remove(k); + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // 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); + }) + .Select(x => x.Key) + .ToArray()) // ToArray required to remove + MemoryCache.Remove(key); } } public virtual void ClearCacheObjectTypes() { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { var typeOfT = typeof (T); - var keysToRemove = MemoryCache - .Where(c => c.Value != null && c.Value.GetType() == typeOfT) - .Select(c => c.Key) - .ToArray(); - foreach (var k in keysToRemove) - MemoryCache.Remove(k); + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // 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() == typeOfT; + }) + .Select(x => x.Key) + .ToArray()) // ToArray required to remove + MemoryCache.Remove(key); } } public virtual void ClearCacheObjectTypes(Func predicate) { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { var typeOfT = typeof(T); - var keysToRemove = MemoryCache - .Where(c => c.Value != null && c.Value.GetType() == typeOfT && predicate(c.Key, (T)c.Value)) - .Select(c => c.Key) - .ToArray(); - foreach (var k in keysToRemove) - MemoryCache.Remove(k); + foreach (var key in MemoryCache + .Where(x => + { + // x.Value is Lazy and not null, its value may be null + // remove null values as well, does not hurt + // get non-created as NonCreatedValue & exceptions as null + var value = GetSafeLazyValue((Lazy)x.Value, true); + if (value == null) return true; + return value.GetType() == typeOfT + && predicate(x.Key, (T) value); + }) + .Select(x => x.Key) + .ToArray()) // ToArray required to remove + MemoryCache.Remove(key); } } public virtual void ClearCacheByKeySearch(string keyStartsWith) { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { - var keysToRemove = (from c in MemoryCache where c.Key.InvariantStartsWith(keyStartsWith) select c.Key).ToList(); - foreach (var k in keysToRemove) - { - MemoryCache.Remove(k); - } + foreach (var key in MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .Select(x => x.Key) + .ToArray()) // ToArray required to remove + MemoryCache.Remove(key); } } public virtual void ClearCacheByKeyExpression(string regexString) { - using (new WriteLock(Locker)) + using (new WriteLock(_locker)) { - var keysToRemove = (from c in MemoryCache where Regex.IsMatch(c.Key, regexString) select c.Key).ToList(); - foreach (var k in keysToRemove) - { - MemoryCache.Remove(k); - } + foreach (var key in MemoryCache + .Where(x => Regex.IsMatch(x.Key, regexString)) + .Select(x => x.Key) + .ToArray()) // ToArray required to remove + MemoryCache.Remove(key); } } - public virtual IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) + #endregion + + #region Get + + public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) { - return (from c in MemoryCache - where c.Key.InvariantStartsWith(keyStartsWith) - select c.Value).ToList(); + KeyValuePair[] entries; + using (new ReadLock(_locker)) + { + entries = MemoryCache + .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) + .ToArray(); // evaluate while locked + } + return entries + .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList(); } public IEnumerable GetCacheItemsByKeyExpression(string regexString) { - return (from c in MemoryCache - where Regex.IsMatch(c.Key, regexString) - select c.Value).ToList(); + KeyValuePair[] entries; + using (new ReadLock(_locker)) + { + entries = MemoryCache + .Where(x => Regex.IsMatch(x.Key, regexString)) + .ToArray(); // evaluate while locked + } + return entries + .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null + .Where(x => x != null) // backward compat, don't store null values in the cache + .ToList(); } - public virtual object GetCacheItem(string cacheKey) + public object GetCacheItem(string cacheKey) { - var result = MemoryCache.Get(cacheKey); - return result; + Lazy result; + using (new ReadLock(_locker)) + { + result = MemoryCache.Get(cacheKey) as Lazy; // null if key not found + } + return result == null ? null : GetSafeLazyValue(result); // return exceptions as null } - public virtual object GetCacheItem(string cacheKey, Func getCacheItem) + public object GetCacheItem(string cacheKey, Func getCacheItem) { 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) { - using (var lck = new UpgradeableReadLock(Locker)) - { - var result = MemoryCache.Get(cacheKey); - if (result == null) - { - lck.UpgradeToWriteLock(); + // see notes in HttpRuntimeCacheProvider - result = getCacheItem(); - if (result != null) - { - var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); - MemoryCache.Set(cacheKey, result, policy); - } + Lazy result; + + using (var lck = new UpgradeableReadLock(_locker)) + { + result = MemoryCache.Get(cacheKey) as Lazy; + if (result == null || GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null + { + result = new Lazy(getCacheItem); + var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); + + lck.UpgradeToWriteLock(); + MemoryCache.Set(cacheKey, result, policy); } - return result; } + + return result.Value; } + #endregion + + #region Insert + public void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - object result = getCacheItem(); - if (result != null) - { - var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); - MemoryCache.Set(cacheKey, result, policy); - } + // NOTE - here also we must insert a Lazy but we can evaluate it right now + // and make sure we don't store a null value. + + var result = new Lazy(getCacheItem); + var value = result.Value; // force evaluation now + if (value == null) return; // do not store null values (backward compat) + + var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); + MemoryCache.Set(cacheKey, result, policy); } + #endregion + private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 601e2819ff..51cf37aa23 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -44,7 +44,7 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(HttpRuntime.Cache), new StaticCacheProvider(), - new HttpRequestCacheProvider(() => new HttpContextWrapper(HttpContext.Current))) + new HttpRequestCacheProvider()) { } @@ -56,7 +56,7 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(cache), new StaticCacheProvider(), - new HttpRequestCacheProvider(() => new HttpContextWrapper(HttpContext.Current))) + new HttpRequestCacheProvider()) { } diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index ea0303eadc..9c069c4c2f 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -279,6 +279,22 @@ namespace Umbraco.Core /// public const string AltTemplate = "altTemplate"; } + + /// + /// Defines the alias identifiers for Umbraco relation types. + /// + public static class RelationTypes + { + /// + /// ContentType name for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyName = "Relate Document On Copy"; + + /// + /// ContentType alias for default relation type "Relate Document On Copy". + /// + public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; + } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Packaging.cs b/src/Umbraco.Core/Constants-Packaging.cs index 4e9f294406..31cbad1246 100644 --- a/src/Umbraco.Core/Constants-Packaging.cs +++ b/src/Umbraco.Core/Constants-Packaging.cs @@ -23,6 +23,7 @@ public const string DictionaryItemsNodeName = "DictionaryItems"; public const string DictionaryItemNodeName = "DictionaryItem"; public const string MacrosNodeName = "Macros"; + public const string DocumentsNodeName = "Documents"; public const string DocumentSetNodeName = "DocumentSet"; public const string DocumentTypesNodeName = "DocumentTypes"; public const string DocumentTypeNodeName = "DocumentType"; diff --git a/src/Umbraco.Core/Events/CopyEventArgs.cs b/src/Umbraco.Core/Events/CopyEventArgs.cs index f53f8a536e..16f4fae982 100644 --- a/src/Umbraco.Core/Events/CopyEventArgs.cs +++ b/src/Umbraco.Core/Events/CopyEventArgs.cs @@ -1,35 +1,47 @@ namespace Umbraco.Core.Events { - public class CopyEventArgs : CancellableObjectEventArgs - { - public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) : base(original, canCancel) - { - Copy = copy; - ParentId = parentId; - } + public class CopyEventArgs : CancellableObjectEventArgs + { + public CopyEventArgs(TEntity original, TEntity copy, bool canCancel, int parentId) + : base(original, canCancel) + { + Copy = copy; + ParentId = parentId; + } - public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) : base(eventObject) - { - Copy = copy; - ParentId = parentId; - } + public CopyEventArgs(TEntity eventObject, TEntity copy, int parentId) + : base(eventObject) + { + Copy = copy; + ParentId = parentId; + } - /// - /// The copied entity - /// - public TEntity Copy { get; set; } + public CopyEventArgs(TEntity eventObject, TEntity copy, bool canCancel, int parentId, bool relateToOriginal) + : base(eventObject, canCancel) + { + Copy = copy; + ParentId = parentId; + RelateToOriginal = relateToOriginal; + } - /// - /// The original entity - /// - public TEntity Original - { - get { return EventObject; } - } + /// + /// The copied entity + /// + public TEntity Copy { get; set; } - /// - /// Gets or Sets the Id of the objects new parent. - /// - public int ParentId { get; private set; } - } + /// + /// The original entity + /// + public TEntity Original + { + get { return EventObject; } + } + + /// + /// Gets or Sets the Id of the objects new parent. + /// + public int ParentId { get; private set; } + + public bool RelateToOriginal { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 768ae37c9b..dceed0e259 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -240,6 +240,28 @@ namespace Umbraco.Core.Models return content.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Contains(recycleBinId.ToInvariantString()); } + + /// + /// Removes characters that are not valide XML characters from all entity properties + /// of type string. See: http://stackoverflow.com/a/961504/5018 + /// + /// + /// + /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. + /// + /// + public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) + { + entity.Name = entity.Name.ToValidXmlString(); + foreach (var property in entity.Properties) + { + if (property.Value is string) + { + var value = (string)property.Value; + property.Value = value.ToValidXmlString(); + } + } + } /// /// Checks if the IContentBase has children @@ -734,10 +756,6 @@ namespace Umbraco.Core.Models { return ((PackagingService)(ApplicationContext.Current.Services.PackagingService)).Export(member); } - #endregion } - - - } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index d8ef10751b..eb3be9d145 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -27,6 +28,11 @@ namespace Umbraco.Core.Models public static class DeepCloneHelper { + /// + /// Used to avoid constant reflection (perf) + /// + private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + /// /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') /// @@ -43,16 +49,18 @@ namespace Umbraco.Core.Models throw new InvalidOperationException("Both the input and output types must be the same"); } - var refProperties = inputType.GetProperties() - .Where(x => - //is not attributed with the ignore clone attribute - x.GetCustomAttribute() == null - //reference type but not string - && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) - //settable - && x.CanWrite - //non-indexed - && x.GetIndexParameters().Any() == false); + var refProperties = PropCache.GetOrAdd(inputType, type => + inputType.GetProperties() + .Where(x => + //is not attributed with the ignore clone attribute + x.GetCustomAttribute() == null + //reference type but not string + && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) + //settable + && x.CanWrite + //non-indexed + && x.GetIndexParameters().Any() == false) + .ToArray()); foreach (var propertyInfo in refProperties) { diff --git a/src/Umbraco.Core/Models/EntityExtensions.cs b/src/Umbraco.Core/Models/EntityExtensions.cs index 6daf99a58d..9fbd4ce592 100644 --- a/src/Umbraco.Core/Models/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/EntityExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models @@ -23,5 +20,5 @@ namespace Umbraco.Core.Models var dirty = (IRememberBeingDirty)entity; return dirty.WasPropertyDirty("Id"); } - } -} + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ITag.cs b/src/Umbraco.Core/Models/ITag.cs index 8c1c1aa5e0..53b23d65c5 100644 --- a/src/Umbraco.Core/Models/ITag.cs +++ b/src/Umbraco.Core/Models/ITag.cs @@ -3,6 +3,9 @@ using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models { + /// + /// Represents a Tag, which is composed of a Text, Group and NodeCount property. + /// public interface ITag : IAggregateRoot { [DataMember] @@ -11,6 +14,8 @@ namespace Umbraco.Core.Models [DataMember] string Group { get; set; } + int NodeCount { get; } + //TODO: enable this at some stage //int ParentId { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs index b304f72777..1aead03726 100644 --- a/src/Umbraco.Core/Models/Membership/IMembershipUser.cs +++ b/src/Umbraco.Core/Models/Membership/IMembershipUser.cs @@ -1,9 +1,11 @@ using System; -using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership { + /// + /// Defines the base contract for and + /// public interface IMembershipUser : IAggregateRoot { object ProviderUserKey { get; set; } diff --git a/src/Umbraco.Core/Models/PartialView.cs b/src/Umbraco.Core/Models/PartialView.cs new file mode 100644 index 0000000000..f963ff0e1a --- /dev/null +++ b/src/Umbraco.Core/Models/PartialView.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Partial View file + /// + [Serializable] + [DataContract(IsReference = true)] + internal class PartialView : File + { + private readonly Regex _headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline | RegexOptions.Compiled); + + public PartialView(string path) + : base(path) + { + base.Path = path; + } + + /// + /// Boolean indicating whether the file could be validated + /// + /// True if file is valid, otherwise false + public override bool IsValid() + { + //TODO: Validate using the macro engine + //var engine = MacroEngineFactory.GetEngine(PartialViewMacroEngine.EngineName); + //engine.Validate(...) + + var validatePath = IOHelper.ValidateEditPath(IOHelper.MapPath(Path), BasePath); + var verifyFileExtension = IOHelper.VerifyFileExtension(Path, new List { "cshtml" }); + + return validatePath && verifyFileExtension; + } + + public string OldFileName { get; set; } + + public string FileName { get; set; } + + public string SnippetName { get; set; } + + public bool CreateMacro { get; set; } + + public string CodeHeader { get; set; } + + public string ParentFolderName { get; set; } + + public string EditViewFile { get; set; } + + public string BasePath { get; set; } + + public string ReturnUrl { get; set; } + + internal Regex HeaderMatch + { + get { return _headerMatch; } + } + + internal Attempt TryGetSnippetPath(string fileName) + { + var partialViewsFileSystem = new PhysicalFileSystem(BasePath); + var snippetPath = IOHelper.MapPath(string.Format("{0}/PartialViewMacros/Templates/{1}", SystemDirectories.Umbraco, fileName)); + + return partialViewsFileSystem.FileExists(snippetPath) + ? Attempt.Succeed(snippetPath) + : Attempt.Fail(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/TagDto.cs b/src/Umbraco.Core/Models/Rdbms/TagDto.cs index 19ff41db90..d612f656bb 100644 --- a/src/Umbraco.Core/Models/Rdbms/TagDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/TagDto.cs @@ -27,5 +27,8 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] public string Group { get; set; }//NOTE Is set to [varchar] (100) in Sql Server script + + [ResultColumn("NodeCount")] + public int NodeCount { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Tag.cs b/src/Umbraco.Core/Models/Tag.cs index 1bd5bff76d..bfe4c10f42 100644 --- a/src/Umbraco.Core/Models/Tag.cs +++ b/src/Umbraco.Core/Models/Tag.cs @@ -16,9 +16,15 @@ namespace Umbraco.Core.Models public Tag(int id, string text, string @group) { + Id = id; Text = text; Group = @group; - Id = id; + } + + public Tag(int id, string text, string @group, int nodeCount) + : this(id, text, @group) + { + NodeCount = nodeCount; } private static readonly PropertyInfo TextSelector = ExpressionHelper.GetPropertyInfo(x => x.Text); @@ -52,6 +58,8 @@ namespace Umbraco.Core.Models } } + public int NodeCount { get; internal set; } + //TODO: enable this at some stage //public int ParentId { get; set; } } diff --git a/src/Umbraco.Core/Models/TaggedEntity.cs b/src/Umbraco.Core/Models/TaggedEntity.cs index decd4220fe..890412a6b6 100644 --- a/src/Umbraco.Core/Models/TaggedEntity.cs +++ b/src/Umbraco.Core/Models/TaggedEntity.cs @@ -2,6 +2,11 @@ namespace Umbraco.Core.Models { + /// + /// Represents a tagged entity. + /// + /// Note that it is the properties of an entity (like Content, Media, Members, etc.) that is tagged, + /// which is why this class is composed of a list of tagged properties and an Id reference to the actual entity. public class TaggedEntity { public TaggedEntity(int entityId, IEnumerable taggedProperties) @@ -10,7 +15,14 @@ namespace Umbraco.Core.Models TaggedProperties = taggedProperties; } + /// + /// Id of the entity, which is tagged + /// public int EntityId { get; private set; } + + /// + /// An enumerable list of tagged properties + /// public IEnumerable TaggedProperties { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/TaggedProperty.cs b/src/Umbraco.Core/Models/TaggedProperty.cs index 3b92413cdb..9013c25887 100644 --- a/src/Umbraco.Core/Models/TaggedProperty.cs +++ b/src/Umbraco.Core/Models/TaggedProperty.cs @@ -2,6 +2,9 @@ namespace Umbraco.Core.Models { + /// + /// Represents a tagged property on an entity. + /// public class TaggedProperty { public TaggedProperty(int propertyTypeId, string propertyTypeAlias, IEnumerable tags) @@ -11,8 +14,19 @@ namespace Umbraco.Core.Models Tags = tags; } + /// + /// Id of the PropertyType, which this tagged property is based on + /// public int PropertyTypeId { get; private set; } + + /// + /// Alias of the PropertyType, which this tagged property is based on + /// public string PropertyTypeAlias { get; private set; } + + /// + /// An enumerable list of Tags for the property + /// public IEnumerable Tags { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs index 7cc130600b..34ee29b96f 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs @@ -87,13 +87,13 @@ namespace Umbraco.Core.Models { var type = typeof(UmbracoObjectTypes); var memInfo = type.GetMember(umbracoObjectType.ToString()); - var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoObjectTypeAttribute), + var attributes = memInfo[0].GetCustomAttributes(typeof(FriendlyNameAttribute), false); if (attributes.Length == 0) return string.Empty; - var attribute = ((UmbracoObjectTypeAttribute)attributes[0]); + var attribute = ((FriendlyNameAttribute)attributes[0]); if (attribute == null) return string.Empty; diff --git a/src/Umbraco.Core/MonitorLock.cs b/src/Umbraco.Core/MonitorLock.cs new file mode 100644 index 0000000000..9d17c86be8 --- /dev/null +++ b/src/Umbraco.Core/MonitorLock.cs @@ -0,0 +1,32 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Provides an equivalent to the c# lock statement, to be used in a using block. + /// + /// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... } + public class MonitorLock : IDisposable + { + private readonly object _locker; + private readonly bool _entered; + + /// + /// Initializes a new instance of the class with an object to lock. + /// + /// The object to lock. + /// Should always be used within a using block. + public MonitorLock(object locker) + { + _locker = locker; + _entered = false; + System.Threading.Monitor.Enter(_locker, ref _entered); + } + + void IDisposable.Dispose() + { + if (_entered) + System.Threading.Monitor.Exit(_locker); + } + } +} diff --git a/src/Umbraco.Core/ObjectResolution/Resolution.cs b/src/Umbraco.Core/ObjectResolution/Resolution.cs index 17097ee7a5..87eb06e295 100644 --- a/src/Umbraco.Core/ObjectResolution/Resolution.cs +++ b/src/Umbraco.Core/ObjectResolution/Resolution.cs @@ -63,6 +63,11 @@ namespace Umbraco.Core.ObjectResolution } } + // NOTE - the ugly code below exists only because of umbraco.BusinessLogic.Actions.Action.ReRegisterActionsAndHandlers + // which wants to re-register actions and handlers instead of properly restarting the application. Don't even think + // about using it for anything else. Also, while the backdoor is open, the resolution system is locked so nothing + // can work properly => deadlocks. Therefore, open the backdoor, do resolution changes EXCLUSIVELY, and close the door! + /// /// Returns a disposable object that reprents dirty access to temporarily unfrozen resolution configuration. /// diff --git a/src/Umbraco.Core/Packaging/PackageBinaryByteInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryByteInspector.cs deleted file mode 100644 index 010e3a8521..0000000000 --- a/src/Umbraco.Core/Packaging/PackageBinaryByteInspector.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security; -using System.Security.Permissions; -using Umbraco.Core.Logging; - -namespace Umbraco.Core.Packaging -{ - internal class PackageBinaryByteInspector : MarshalByRefObject - { - /// - /// Entry point to call from your code - /// - /// - /// - /// - /// - /// - /// Will perform the assembly scan in a separate app domain - /// - public static IEnumerable ScanAssembliesForTypeReference(IEnumerable assemblys, out string[] errorReport) - { - var appDomain = GetTempAppDomain(); - var type = typeof(PackageBinaryByteInspector); - try - { - var value = (PackageBinaryByteInspector)appDomain.CreateInstanceAndUnwrap( - type.Assembly.FullName, - type.FullName); - var result = value.PerformScan(assemblys.ToArray(), out errorReport); - return result; - } - finally - { - AppDomain.Unload(appDomain); - } - } - - /// - /// Performs the assembly scanning - /// - /// - /// - /// - /// - /// - /// This method is executed in a separate app domain - /// - internal IEnumerable PerformScan(IEnumerable assemblies, out string[] errorReport) - { - var dllsWithReference = new List(); - var errors = new List(); - var assembliesWithErrors = new List(); - - //we need this handler to resolve assembly dependencies below - AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (s, e) => - { - var a = Assembly.ReflectionOnlyLoad(e.Name); - if (a == null) throw new TypeLoadException("Could not load assembly " + e.Name); - return a; - }; - - //First load each dll file into the context - var loaded = assemblies.Select(Assembly.ReflectionOnlyLoad).ToList(); - - //load each of the LoadFrom assemblies into the Load context too - foreach (var a in loaded) - { - Assembly.ReflectionOnlyLoad(a.FullName); - } - - //get the list of assembly names to compare below - var loadedNames = loaded.Select(x => x.GetName().Name).ToArray(); - - //Then load each referenced assembly into the context - foreach (var a in loaded) - { - //don't load any referenced assemblies that are already found in the loaded array - this is based on name - // regardless of version. We'll assume that if the assembly found in the folder matches the assembly name - // being looked for, that is the version the user has shipped their package with and therefore it 'must' be correct - foreach (var assemblyName in a.GetReferencedAssemblies().Where(ass => loadedNames.Contains(ass.Name) == false)) - { - try - { - Assembly.ReflectionOnlyLoad(assemblyName.FullName); - } - catch (FileNotFoundException) - { - //if an exception occurs it means that a referenced assembly could not be found - errors.Add( - string.Concat("This package references the assembly '", - assemblyName.Name, - "' which was not found")); - assembliesWithErrors.Add(a); - } - catch (Exception ex) - { - //if an exception occurs it means that a referenced assembly could not be found - errors.Add( - string.Concat("This package could not be verified for compatibility. An error occurred while loading a referenced assembly '", - assemblyName.Name, - "' see error log for full details.")); - assembliesWithErrors.Add(a); - LogHelper.Error("An error occurred scanning package assemblies", ex); - } - } - } - - var contractType = GetLoadFromContractType(); - - //now that we have all referenced types into the context we can look up stuff - foreach (var a in loaded.Except(assembliesWithErrors)) - { - //now we need to see if they contain any type 'T' - var reflectedAssembly = a; - - try - { - var found = reflectedAssembly.GetExportedTypes() - .Where(contractType.IsAssignableFrom); - - if (found.Any()) - { - dllsWithReference.Add(reflectedAssembly.FullName); - } - } - catch (Exception ex) - { - //This is a hack that nobody can seem to get around, I've read everything and it seems that - // this is quite a common thing when loading types into reflection only load context, so - // we're just going to ignore this specific one for now - var typeLoadEx = ex as TypeLoadException; - if (typeLoadEx != null) - { - if (typeLoadEx.Message.InvariantContains("does not have an implementation")) - { - //ignore - continue; - } - } - else - { - errors.Add( - string.Concat("This package could not be verified for compatibility. An error occurred while scanning a packaged assembly '", - a.GetName().Name, - "' see error log for full details.")); - assembliesWithErrors.Add(a); - LogHelper.Error("An error occurred scanning package assemblies", ex); - } - } - - } - - errorReport = errors.ToArray(); - return dllsWithReference; - } - - - /// - /// In order to compare types, the types must be in the same context, this method will return the type that - /// we are checking against but from the Load context. - /// - /// - /// - private static Type GetLoadFromContractType() - { - var contractAssemblyLoadFrom =Assembly.ReflectionOnlyLoad(typeof (T).Assembly.FullName); - - var contractType = contractAssemblyLoadFrom.GetExportedTypes() - .FirstOrDefault(x => x.FullName == typeof(T).FullName && x.Assembly.FullName == typeof(T).Assembly.FullName); - - if (contractType == null) - { - throw new InvalidOperationException("Could not find type " + typeof(T) + " in the LoadFrom assemblies"); - } - return contractType; - } - - /// - /// Create an app domain - /// - /// - private static AppDomain GetTempAppDomain() - { - //copy the current app domain setup but don't shadow copy files - var appName = "TempDomain" + Guid.NewGuid(); - var domainSetup = new AppDomainSetup - { - ApplicationName = appName, - ShadowCopyFiles = "false", - ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase, - ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, - DynamicBase = AppDomain.CurrentDomain.SetupInformation.DynamicBase, - LicenseFile = AppDomain.CurrentDomain.SetupInformation.LicenseFile, - LoaderOptimization = AppDomain.CurrentDomain.SetupInformation.LoaderOptimization, - PrivateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath, - PrivateBinPathProbe = AppDomain.CurrentDomain.SetupInformation.PrivateBinPathProbe - }; - - //create new domain with full trust - return AppDomain.CreateDomain( - appName, - AppDomain.CurrentDomain.Evidence, - domainSetup, - new PermissionSet(PermissionState.Unrestricted)); - } - - private static string GetAssemblyPath(Assembly a) - { - var codeBase = a.CodeBase; - var uri = new Uri(codeBase); - return uri.LocalPath; - } - - } -} diff --git a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs index ccf1345074..8852475543 100644 --- a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs +++ b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs @@ -9,8 +9,43 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.Packaging { + // Note + // That class uses ReflectionOnlyLoad which does NOT handle policies (bindingRedirect) and + // therefore raised warnings when installing a package, if an exact dependency could not be + // found, though it would be found via policies. So we have to explicitely apply policies + // where appropriate. + internal class PackageBinaryInspector : MarshalByRefObject { + /// + /// Entry point to call from your code + /// + /// + /// + /// + /// + /// + /// Will perform the assembly scan in a separate app domain + /// + public static IEnumerable ScanAssembliesForTypeReference(IEnumerable assemblys, out string[] errorReport) + { + var appDomain = GetTempAppDomain(); + var type = typeof(PackageBinaryInspector); + try + { + var value = (PackageBinaryInspector)appDomain.CreateInstanceAndUnwrap( + type.Assembly.FullName, + type.FullName); + // do NOT turn PerformScan into static (even if ReSharper says so)! + var result = value.PerformScan(assemblys.ToArray(), out errorReport); + return result; + } + finally + { + AppDomain.Unload(appDomain); + } + } + /// /// Entry point to call from your code /// @@ -30,6 +65,7 @@ namespace Umbraco.Core.Packaging var value = (PackageBinaryInspector)appDomain.CreateInstanceAndUnwrap( type.Assembly.FullName, type.FullName); + // do NOT turn PerformScan into static (even if ReSharper says so)! var result = value.PerformScan(dllPath, out errorReport); return result; } @@ -39,6 +75,35 @@ namespace Umbraco.Core.Packaging } } + /// + /// Performs the assembly scanning + /// + /// + /// + /// + /// + /// + /// This method is executed in a separate app domain + /// + private IEnumerable PerformScan(IEnumerable assemblies, out string[] errorReport) + { + //we need this handler to resolve assembly dependencies when loading below + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (s, e) => + { + var name = AppDomain.CurrentDomain.ApplyPolicy(e.Name); + var a = Assembly.ReflectionOnlyLoad(name); + if (a == null) throw new TypeLoadException("Could not load assembly " + e.Name); + return a; + }; + + //First load each dll file into the context + // do NOT apply policy here: we want to scan the dlls that are in the binaries + var loaded = assemblies.Select(Assembly.ReflectionOnlyLoad).ToList(); + + //scan + return PerformScan(loaded, out errorReport); + } + /// /// Performs the assembly scanning /// @@ -49,33 +114,42 @@ namespace Umbraco.Core.Packaging /// /// This method is executed in a separate app domain /// - internal IEnumerable PerformScan(string dllPath, out string[] errorReport) + private IEnumerable PerformScan(string dllPath, out string[] errorReport) { if (Directory.Exists(dllPath) == false) { throw new DirectoryNotFoundException("Could not find directory " + dllPath); } + //we need this handler to resolve assembly dependencies when loading below + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (s, e) => + { + var name = AppDomain.CurrentDomain.ApplyPolicy(e.Name); + var a = Assembly.ReflectionOnlyLoad(name); + if (a == null) throw new TypeLoadException("Could not load assembly " + e.Name); + return a; + }; + + //First load each dll file into the context + // do NOT apply policy here: we want to scan the dlls that are in the path var files = Directory.GetFiles(dllPath, "*.dll"); + var loaded = files.Select(Assembly.ReflectionOnlyLoadFrom).ToList(); + + //scan + return PerformScan(loaded, out errorReport); + } + + private static IEnumerable PerformScan(IList loaded, out string[] errorReport) + { var dllsWithReference = new List(); var errors = new List(); var assembliesWithErrors = new List(); - //we need this handler to resolve assembly dependencies below - AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += (s, e) => - { - var a = Assembly.ReflectionOnlyLoad(e.Name); - if (a == null) throw new TypeLoadException("Could not load assembly " + e.Name); - return a; - }; - - //First load each dll file into the context - var loaded = files.Select(Assembly.ReflectionOnlyLoadFrom).ToList(); - //load each of the LoadFrom assemblies into the Load context too foreach (var a in loaded) { - Assembly.ReflectionOnlyLoad(a.FullName); + var name = AppDomain.CurrentDomain.ApplyPolicy(a.FullName); + Assembly.ReflectionOnlyLoad(name); } //get the list of assembly names to compare below @@ -91,7 +165,8 @@ namespace Umbraco.Core.Packaging { try { - Assembly.ReflectionOnlyLoad(assemblyName.FullName); + var name = AppDomain.CurrentDomain.ApplyPolicy(assemblyName.FullName); + Assembly.ReflectionOnlyLoad(name); } catch (FileNotFoundException) { @@ -164,7 +239,6 @@ namespace Umbraco.Core.Packaging return dllsWithReference; } - /// /// In order to compare types, the types must be in the same context, this method will return the type that /// we are checking against but from the Load context. @@ -173,7 +247,8 @@ namespace Umbraco.Core.Packaging /// private static Type GetLoadFromContractType() { - var contractAssemblyLoadFrom =Assembly.ReflectionOnlyLoad(typeof (T).Assembly.FullName); + var name = AppDomain.CurrentDomain.ApplyPolicy(typeof(T).Assembly.FullName); + var contractAssemblyLoadFrom = Assembly.ReflectionOnlyLoad(name); var contractType = contractAssemblyLoadFrom.GetExportedTypes() .FirstOrDefault(x => x.FullName == typeof(T).FullName && x.Assembly.FullName == typeof(T).Assembly.FullName); diff --git a/src/Umbraco.Core/Packaging/PackageInstallation.cs b/src/Umbraco.Core/Packaging/PackageInstallation.cs index 8ca115ea8b..522048d2d7 100644 --- a/src/Umbraco.Core/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageInstallation.cs @@ -116,6 +116,7 @@ namespace Umbraco.Core.Packaging XElement documentTypes; XElement styleSheets; XElement documentSet; + XElement documents; XElement actions; MetaData metaData; InstallationSummary installationSummary; @@ -134,6 +135,7 @@ namespace Umbraco.Core.Packaging documentTypes = rootElement.Element(Constants.Packaging.DocumentTypesNodeName); styleSheets = rootElement.Element(Constants.Packaging.StylesheetsNodeName); documentSet = rootElement.Element(Constants.Packaging.DocumentSetNodeName); + documents = rootElement.Element(Constants.Packaging.DocumentsNodeName); actions = rootElement.Element(Constants.Packaging.ActionsNodeName); metaData = GetMetaData(rootElement); @@ -170,7 +172,9 @@ namespace Umbraco.Core.Packaging var stylesheetsInstalled = EmptyEnumerableIfNull(styleSheets) ?? InstallStylesheets(styleSheets, userId); installationSummary.StylesheetsInstalled = stylesheetsInstalled; - var documentsInstalled = EmptyEnumerableIfNull(documentSet) ?? InstallDocuments(documentSet, userId); + var documentsInstalled = documents != null ? InstallDocuments(documents, userId) + : EmptyEnumerableIfNull(documentSet) + ?? InstallDocuments(documentSet, userId); installationSummary.ContentInstalled = documentsInstalled; var packageActions = EmptyEnumerableIfNull(actions) ?? GetPackageActions(actions, metaData.Name); @@ -303,17 +307,25 @@ namespace Umbraco.Core.Packaging return packageAction; - }); + }).ToArray(); } private IEnumerable InstallDocuments(XElement documentsElement, int userId = 0) { - if (string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName) == false) + if ((string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName) == false) + && (string.Equals(Constants.Packaging.DocumentsNodeName, documentsElement.Name.LocalName) == false)) { - throw new ArgumentException("Must be \"" + Constants.Packaging.DocumentSetNodeName + "\" as root", + throw new ArgumentException("Must be \"" + Constants.Packaging.DocumentsNodeName + "\" as root", "documentsElement"); } - return _packagingService.ImportContent(documentsElement, -1, userId); + + if (string.Equals(Constants.Packaging.DocumentSetNodeName, documentsElement.Name.LocalName)) + return _packagingService.ImportContent(documentsElement, -1, userId); + + return + documentsElement.Elements(Constants.Packaging.DocumentSetNodeName) + .SelectMany(documentSetElement => _packagingService.ImportContent(documentSetElement, -1, userId)) + .ToArray(); } private IEnumerable InstallStylesheets(XElement styleSheetsElement, int userId = 0) @@ -361,7 +373,7 @@ namespace Umbraco.Core.Packaging _packageExtraction.CopyFilesFromArchive(packageFilePath, sourceDestination); - return sourceDestination.Select(sd => sd.Value); + return sourceDestination.Select(sd => sd.Value).ToArray(); } private KeyValuePair[] AppendRootToDestination(string fullpathToRoot, IEnumerable> sourceDestination) @@ -455,7 +467,7 @@ namespace Umbraco.Core.Packaging // Now we want to see if the DLLs contain any legacy data types since we want to warn people about that string[] assemblyErrors; IEnumerable assemblyesToScan =_packageExtraction.ReadFilesFromArchive(packagePath, dlls); - return PackageBinaryByteInspector.ScanAssembliesForTypeReference(assemblyesToScan, out assemblyErrors).ToArray(); + return PackageBinaryInspector.ScanAssembliesForTypeReference(assemblyesToScan, out assemblyErrors).ToArray(); } private KeyValuePair[] FindUnsecureFiles(IEnumerable> sourceDestinationPair) diff --git a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs index acacadb0cc..611036f852 100644 --- a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs @@ -41,7 +41,8 @@ namespace Umbraco.Core.Persistence.Caching public static RuntimeCacheProvider Current { get { return lazy.Value; } } - private RuntimeCacheProvider() + //internal for testing! - though I'm not a huge fan of these being singletons! + internal RuntimeCacheProvider() { if (HttpContext.Current == null) { @@ -246,7 +247,7 @@ namespace Umbraco.Core.Persistence.Caching private string GetCompositeId(Type type, Guid id) { - return string.Format("{0}{1}-{2}", CacheItemPrefix, type.Name, id.ToString()); + return string.Format("{0}{1}-{2}", CacheItemPrefix, type.Name, id); } private string GetCompositeId(Type type, int id) diff --git a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs index c154a60198..7cebe5f65e 100644 --- a/src/Umbraco.Core/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/TagFactory.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Factories { public ITag BuildEntity(TagDto dto) { - var model = new Tag(dto.Id, dto.Tag, dto.Group); + var model = new Tag(dto.Id, dto.Tag, dto.Group, dto.NodeCount); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 model.ResetDirtyProperties(false); @@ -20,7 +20,8 @@ namespace Umbraco.Core.Persistence.Factories { Id = entity.Id, Group = entity.Group, - Tag = entity.Text + Tag = entity.Text, + NodeCount = entity.NodeCount, }; } } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index cf78e3a5cb..35730cebbe 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -132,10 +132,10 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateCmsContentTypeData() { - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "folder.png", IsContainer = false, AllowAtRoot = true }); - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "mediaPhoto.png" }); - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "mediaFile.png" }); - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "folder.png" }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture" }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document" }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user" }); } private void CreateUmbracoUserData() @@ -255,7 +255,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateUmbracoRelationTypeData() { - _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 1, Alias = "relateDocumentOnCopy", ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), Dual = true, Name = "Relate Document On Copy" }); + _database.Insert("umbracoRelationType", "id", false, new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }); } private void CreateCmsTaskTypeData() diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 6d479b9eb1..faa247d4a9 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -221,6 +221,7 @@ namespace Umbraco.Core.Persistence if (overwrite && tableExist) { db.DropTable(tableName); + tableExist = false; } if (tableExist == false) diff --git a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs index 4e1a3de28f..c606c24a59 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using Umbraco.Core.Persistence.Querying; @@ -125,5 +126,10 @@ namespace Umbraco.Core.Persistence SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(rightColumnName)); return sql.On(onClause); } + + public static Sql OrderByDescending(this Sql sql, params object[] columns) + { + return sql.Append(new Sql("ORDER BY " + String.Join(", ", (from x in columns select x.ToString() + " DESC").ToArray()))); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index c1633daa75..e05ca04bdb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Xml.Linq; using Umbraco.Core.Configuration; +using Umbraco.Core.Dynamics; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -255,6 +257,9 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); var factory = new ContentFactory(NodeObjectTypeId, entity.Id); var dto = factory.BuildDto(entity); @@ -366,6 +371,9 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (((ICanBeDirty)entity).IsPropertyDirty("ParentId")) { @@ -636,7 +644,117 @@ namespace Umbraco.Core.Persistence.Repositories _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(previewExists, content, xml)); } - + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, int pageNumber, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, string filter = "") + { + // Get base query + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .Where(x => x.Newest); + + // Apply filter + if (!string.IsNullOrEmpty(filter)) + { + sql = sql.Where("cmsDocument.text LIKE @0", "%" + filter + "%"); + } + + // Apply order according to parameters + if (!string.IsNullOrEmpty(orderBy)) + { + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sql = sql.OrderBy(orderByParams); + } + else + { + sql = sql.OrderByDescending(orderByParams); + } + } + + // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, + // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. + // So we'll modify the SQL. + var modifiedSQL = sql.SQL.Replace("SELECT *", "SELECT cmsDocument.nodeId"); + + // Get page of results and total count + IEnumerable result; + var pagedResult = Database.Page(pageNumber, pageSize, modifiedSQL, sql.Arguments); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + if (totalRecords > 0) + { + // Parse out node Ids and load content (we need the cast here in order to be able to call the IQueryable extension + // methods OrderBy or OrderByDescending) + var content = GetAll(pagedResult.Items + .DistinctBy(x => x.NodeId) + .Select(x => x.NodeId).ToArray()) + .Cast() + .AsQueryable(); + + // Now we need to ensure this result is also ordered by the same order by clause + var orderByProperty = GetIContentPropertyNameForOrderBy(orderBy); + if (orderDirection == Direction.Ascending) + { + result = content.OrderBy(orderByProperty); + } + else + { + result = content.OrderByDescending(orderByProperty); + } + } + else + { + result = Enumerable.Empty(); + } + + return result; + } + + private string GetDatabaseFieldNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the database field names. + switch (orderBy) + { + case "Name": + return "cmsDocument.text"; + case "Owner": + return "umbracoNode.nodeUser"; + case "Updator": + return "cmsDocument.documentUser"; + default: + return orderBy; + } + } + + private string GetIContentPropertyNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the IContent property names. + switch (orderBy) + { + case "Owner": + return "CreatorId"; + case "Updator": + return "WriterId"; + default: + return orderBy; + } + } + #endregion /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index f98c0f1e71..2609231c63 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -146,6 +146,9 @@ namespace Umbraco.Core.Persistence.Repositories { ((DictionaryItem)entity).AddingEntity(); + foreach (var translation in entity.Translations) + translation.Value = translation.Value.ToValidXmlString(); + var factory = new DictionaryItemFactory(); var dto = factory.BuildDto(entity); @@ -167,6 +170,9 @@ namespace Umbraco.Core.Persistence.Repositories { ((Entity)entity).UpdatingEntity(); + foreach (var translation in entity.Translations) + translation.Value = translation.Value.ToValidXmlString(); + var factory = new DictionaryItemFactory(); var dto = factory.BuildDto(entity); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 2fd0a5685e..0b3673d55b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories @@ -66,5 +68,18 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdatePreviewXml(IContent content, Func xml); + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, int pageNumber, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index c8b6f79f7d..81783ccfbd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -12,6 +12,17 @@ namespace Umbraco.Core.Persistence.Repositories public interface IRepositoryVersionable : IRepositoryQueryable where TEntity : IAggregateRoot { + /// + /// Get the total count of entities + /// + /// + /// + int Count(string contentTypeAlias = null); + + int CountChildren(int parentId, string contentTypeAlias = null); + + int CountDescendants(int parentId, string contentTypeAlias = null); + /// /// Gets a list of all versions for an . /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagsRepository.cs index 479f9c85d8..ae89c469fc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITagsRepository.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Returns all tags for an entity type (content/media/member) /// - /// + /// Entity type /// Optional group /// IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null); diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index d916c91e96..a736f16e3c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -220,6 +220,9 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + var factory = new MediaFactory(NodeObjectTypeId, entity.Id); var dto = factory.BuildDto(entity); @@ -289,6 +292,9 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (((ICanBeDirty)entity).IsPropertyDirty("ParentId")) { diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 2bbb3439ca..f84cb9c1b6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -209,6 +209,9 @@ namespace Umbraco.Core.Persistence.Repositories { ((Member)entity).AddingEntity(); + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + var factory = new MemberFactory(NodeObjectTypeId, entity.Id); var dto = factory.BuildDto(entity); @@ -284,6 +287,9 @@ namespace Umbraco.Core.Persistence.Repositories //Updates Modified date ((Member)entity).UpdatingEntity(); + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + var dirtyEntity = (ICanBeDirty) entity; //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed diff --git a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs index f98545ebe5..1d687d1974 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs @@ -230,63 +230,96 @@ namespace Umbraco.Core.Persistence.Repositories { var nodeObjectType = GetNodeObjectType(objectType); - var sql = new Sql() - .Select("DISTINCT cmsTags.*") - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) + var sql = GetTagsQuerySelect(true); + + sql = ApplyRelationshipJoinToTagsQuery(sql); + + sql = sql .InnerJoin() .On(left => left.NodeId, right => right.NodeId) .InnerJoin() .On(left => left.NodeId, right => right.NodeId) .Where(dto => dto.NodeObjectType == nodeObjectType); - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql.Where(dto => dto.Group == group); - } + sql = ApplyGroupFilterToTagsQuery(sql, group); - var factory = new TagFactory(); + sql = ApplyGroupByToTagsQuery(sql); - return Database.Fetch(sql).Select(factory.BuildEntity); + return ExecuteTagsQuery(sql); } public IEnumerable GetTagsForEntity(int contentId, string group = null) { - var sql = new Sql() - .Select("DISTINCT cmsTags.*") - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) + var sql = GetTagsQuerySelect(); + + sql = ApplyRelationshipJoinToTagsQuery(sql); + + sql = sql .Where(dto => dto.NodeId == contentId); - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql.Where(dto => dto.Group == group); - } + sql = ApplyGroupFilterToTagsQuery(sql, group); - var factory = new TagFactory(); - - return Database.Fetch(sql).Select(factory.BuildEntity); + return ExecuteTagsQuery(sql); } public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null) { - var sql = new Sql() - .Select("DISTINCT cmsTags.*") - .From() - .InnerJoin() - .On(left => left.TagId, right => right.Id) + var sql = GetTagsQuerySelect(); + + sql = ApplyRelationshipJoinToTagsQuery(sql); + + sql = sql .InnerJoin() .On(left => left.Id, right => right.PropertyTypeId) .Where(dto => dto.NodeId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); - if (group.IsNullOrWhiteSpace() == false) + sql = ApplyGroupFilterToTagsQuery(sql, group); + + return ExecuteTagsQuery(sql); + } + + private Sql GetTagsQuerySelect(bool withGrouping = false) + { + var sql = new Sql(); + + if (withGrouping) + { + sql = sql.Select("cmsTags.Id, cmsTags.Tag, cmsTags.[Group], Count(*) NodeCount"); + } + else + { + sql = sql.Select("DISTINCT cmsTags.*"); + } + + return sql; + } + + private Sql ApplyRelationshipJoinToTagsQuery(Sql sql) + { + return sql + .From() + .InnerJoin() + .On(left => left.TagId, right => right.Id); + } + + private Sql ApplyGroupFilterToTagsQuery(Sql sql, string group) + { + if (!group.IsNullOrWhiteSpace()) { sql = sql.Where(dto => dto.Group == group); } + return sql; + } + + private Sql ApplyGroupByToTagsQuery(Sql sql) + { + return sql.GroupBy(new string[] { "cmsTags.Id", "cmsTags.Tag", "cmsTags.[Group]" }); + } + + private IEnumerable ExecuteTagsQuery(Sql sql) + { var factory = new TagFactory(); return Database.Fetch(sql).Select(factory.BuildEntity); diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index bf68a1a9a2..8729c7d8f2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -96,6 +96,90 @@ namespace Umbraco.Core.Persistence.Repositories #endregion + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var pathMatch = parentId == -1 + ? "-1," + : "," + parentId + ","; + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)) + .Where(x => x.Alias == contentTypeAlias); + } + + return Database.ExecuteScalar(sql); + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId) + .Where(x => x.Alias == contentTypeAlias); + } + + return Database.ExecuteScalar(sql); + } + + /// + /// Get the total count of entities + /// + /// + /// + public int Count(string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Alias == contentTypeAlias); + } + + return Database.ExecuteScalar(sql); + } + /// /// This removes associated tags from the entity - used generally when an entity is recycled /// diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs index 306ca4dbb0..6ba084bb8c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs @@ -223,6 +223,12 @@ namespace Umbraco.Core.PropertyEditors /// public virtual object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { + //if it's json but it's empty json, then return null + if (ValueType.InvariantEquals("JSON") && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson()) + { + return null; + } + var result = TryConvertValueToCrlType(editorValue.Value); if (result.Success == false) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 444906e3a1..1e3b684b08 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -16,12 +16,34 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters { // in xml a boolean is: string // in the database a boolean is: string "1" or "0" or empty - // the converter does not need to handle anything else ("true"...) + // typically the converter does not need to handle anything else ("true"...) + // however there are cases where the value passed to the converter could be a non-string object, e.g. int, bool + + if (source is string) + { + var str = (string)source; + + if (str == null || str.Length == 0 || str == "0") + return false; + + if (str == "1") + return true; + + bool result; + if (bool.TryParse(str, out result)) + return result; + + return false; + } + + if (source is int) + return (int)source == 1; + + if (source is bool) + return (bool)source; // default value is: false - var sourceString = source as string; - if (sourceString == null) return false; - return sourceString == "1"; + return false; } // default ConvertSourceToObject just returns source ie a boolean value @@ -29,7 +51,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) { // source should come from ConvertSource and be a boolean already - return (bool) source ? "1" : "0"; + return (bool)source ? "1" : "0"; } } } diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index b89b13630f..ebcd967cc2 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -211,7 +211,7 @@ namespace Umbraco.Core.Security _enablePasswordReset = config.GetValue("enablePasswordReset", false); _requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false); _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true); - _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); + _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 20, false, 0); _passwordAttemptWindow = GetIntValue(config, "passwordAttemptWindow", 10, false, 0); _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", DefaultMinPasswordLength, true, 0x80); _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", DefaultMinNonAlphanumericChars, true, 0x80); diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 6e59526d5f..be81292bb0 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -6,15 +6,14 @@ using System.Threading; using System.Xml.Linq; using Umbraco.Core.Auditing; using Umbraco.Core.Events; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Caching; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; @@ -74,6 +73,33 @@ namespace Umbraco.Core.Services _dataTypeService = dataTypeService; } + public int Count(string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + /// /// Used to bulk update the permissions set for a content item. This will replace all permissions /// assigned to an entity with a list of user id & permission pairs. @@ -446,6 +472,29 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, int pageNumber, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, string filter = "") + { + using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var contents = repository.GetPagedResultsByQuery(query, pageNumber, pageSize, out totalChildren, orderBy, orderDirection, filter); + + return contents; + } + } + /// /// Gets a collection of objects by its name or partial name /// @@ -703,7 +752,7 @@ namespace Umbraco.Core.Services { var result = SaveAndPublishDo(content, userId); LogHelper.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); - return result.Success; + return result.Success; } /// @@ -733,7 +782,7 @@ namespace Umbraco.Core.Services if (!result.Any(x => x.Result.ContentItem.Id == content.Id)) return false; - return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; + return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; } /// @@ -770,7 +819,7 @@ namespace Umbraco.Core.Services public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { var result = SaveAndPublishDo(content, userId, raiseEvents); - return result.Success; + return result.Success; } /// @@ -834,7 +883,7 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); } } else @@ -845,7 +894,7 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); //add or update preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); - } + } } uow.Commit(); @@ -1086,7 +1135,7 @@ namespace Umbraco.Core.Services MoveToRecycleBin(content, userId); return; } - + if (Moving.IsRaisedEventCancelled( new MoveEventArgs( new MoveEventInfo(content, content.Path, parentId)), this)) @@ -1179,37 +1228,15 @@ namespace Umbraco.Core.Services //don't copy tags data in tags table if the item is in the recycle bin if (parentId != Constants.System.RecycleBinContent) { - + var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); foreach (var tag in tags) { uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); - } + } } } - - //NOTE This 'Relation' part should eventually be delegated to a RelationService - //TODO: This should be part of a single commit - if (relateToOriginal) - { - IRelationType relationType = null; - using (var relationTypeRepository = _repositoryFactory.CreateRelationTypeRepository(uow)) - { - relationType = relationTypeRepository.Get(1); - } - - using (var relationRepository = _repositoryFactory.CreateRelationRepository(uow)) - { - var relation = new Relation(content.Id, copy.Id, relationType); - relationRepository.AddOrUpdate(relation); - uow.Commit(); - } - - Audit.Add(AuditTypes.Copy, - string.Format("Copied content with Id: '{0}' related to original content with Id: '{1}'", - copy.Id, content.Id), copy.WriterId, copy.Id); - } - + //Look for children and copy those as well var children = GetChildren(content.Id); foreach (var child in children) @@ -1219,10 +1246,10 @@ namespace Umbraco.Core.Services Copy(child, copy.Id, relateToOriginal, userId); } - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId), this); + Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); Audit.Add(AuditTypes.Copy, "Copy Content performed by user", content.WriterId, content.Id); - + //TODO: Don't think we need this here because cache should be cleared by the event listeners // and the correct ICacheRefreshers!? RuntimeCacheProvider.Current.Clear(); @@ -1351,9 +1378,9 @@ namespace Umbraco.Core.Services //add or update a preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); } - + foreach (var content in shouldBePublished) - { + { //Create and Save ContentXml DTO repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); } @@ -1544,7 +1571,7 @@ namespace Umbraco.Core.Services //bulk insert it into the database uow.Database.BulkInsertRecords(xmlItems, tr); - tr.Complete(); + tr.Complete(); } Audit.Add(AuditTypes.Publish, "RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); @@ -1731,7 +1758,7 @@ namespace Umbraco.Core.Services //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c)); - + if (published) { //Content Xml @@ -1871,7 +1898,7 @@ namespace Umbraco.Core.Services } return PublishStatusType.Success; - } + } private IContentType FindContentTypeByAlias(string contentTypeAlias) { diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 848097f6f0..c96365f7af 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -1,7 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Web; using Umbraco.Core.Auditing; using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; @@ -17,19 +23,21 @@ namespace Umbraco.Core.Services private readonly RepositoryFactory _repositoryFactory; private readonly IUnitOfWorkProvider _fileUowProvider; private readonly IDatabaseUnitOfWorkProvider _dataUowProvider; + private readonly IMacroService _macroService; public FileService() : this(new RepositoryFactory()) { } public FileService(RepositoryFactory repositoryFactory) - : this(new FileUnitOfWorkProvider(), new PetaPocoUnitOfWorkProvider(), repositoryFactory) + : this(new FileUnitOfWorkProvider(), new PetaPocoUnitOfWorkProvider(), repositoryFactory, new MacroService()) { } - public FileService(IUnitOfWorkProvider fileProvider, IDatabaseUnitOfWorkProvider dataProvider, RepositoryFactory repositoryFactory) + public FileService(IUnitOfWorkProvider fileProvider, IDatabaseUnitOfWorkProvider dataProvider, RepositoryFactory repositoryFactory, IMacroService macroService) { _repositoryFactory = repositoryFactory; + _macroService = macroService; _fileUowProvider = fileProvider; _dataUowProvider = dataProvider; } @@ -362,6 +370,138 @@ namespace Umbraco.Core.Services return template.IsValid(); } + // TODO: Before making this public: How to get feedback in the UI when cancelled + internal Attempt CreatePartialView(PartialView partialView) + { + var partialViewsFileSystem = new PhysicalFileSystem(partialView.BasePath); + var relativeFilePath = partialView.ParentFolderName.EnsureEndsWith('/') + partialViewsFileSystem.GetRelativePath(partialView.FileName); + partialView.ReturnUrl = string.Format(partialView.EditViewFile + "?file={0}", HttpUtility.UrlEncode(relativeFilePath)); + + //return the link to edit the file if it already exists + if (partialViewsFileSystem.FileExists(partialView.Path)) + return Attempt.Succeed(partialView); + + if (CreatingPartialView.IsRaisedEventCancelled(new NewEventArgs(partialView, true, partialView.Alias, -1), this)) + { + // We have nowhere to return to, clear ReturnUrl + partialView.ReturnUrl = string.Empty; + + var failureMessage = string.Format("Creating Partial View {0} was cancelled by an event handler.", partialViewsFileSystem.GetFullPath(partialView.FileName)); + LogHelper.Info(failureMessage); + + return Attempt.Fail(partialView, new ArgumentException(failureMessage)); + } + + //create the file + var snippetPathAttempt = partialView.TryGetSnippetPath(partialView.SnippetName); + if (snippetPathAttempt.Success == false) + { + throw new InvalidOperationException("Could not load template with name " + partialView.SnippetName); + } + + using (var snippetFile = new StreamReader(partialViewsFileSystem.OpenFile(snippetPathAttempt.Result))) + { + var snippetContent = snippetFile.ReadToEnd().Trim(); + + //strip the @inherits if it's there + snippetContent = partialView.HeaderMatch.Replace(snippetContent, string.Empty); + + var content = string.Format("{0}{1}{2}", partialView.CodeHeader, Environment.NewLine, snippetContent); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + { + partialViewsFileSystem.AddFile(partialView.Path, stream); + } + } + + if (partialView.CreateMacro) + CreatePartialViewMacro(partialView); + + CreatedPartialView.RaiseEvent(new NewEventArgs(partialView, false, partialView.Alias, -1), this); + + return Attempt.Succeed(partialView); + } + + internal void CreatePartialViewMacro(PartialView partialView) + { + var name = partialView.FileName.Substring(0, (partialView.FileName.LastIndexOf('.') + 1)) + .Trim('.') + .SplitPascalCasing() + .ToFirstUpperInvariant(); + + var macro = new Macro(name, name) { ScriptPath = partialView.BasePath + partialView.FileName }; + _macroService.Save(macro); + } + + // TODO: Before making this public: How to get feedback in the UI when cancelled + internal bool DeletePartialView(PartialView partialView, int userId = 0) + { + var partialViewsFileSystem = new PhysicalFileSystem(partialView.BasePath); + + if (DeletingPartialView.IsRaisedEventCancelled(new DeleteEventArgs(partialView), this)) + { + LogHelper.Info(string.Format("Deleting Partial View {0} was cancelled by an event handler.", partialViewsFileSystem.GetFullPath(partialView.FileName))); + return false; + } + + if (partialViewsFileSystem.FileExists(partialView.FileName)) + { + partialViewsFileSystem.DeleteFile(partialView.FileName); + LogHelper.Info(string.Format("Partial View file {0} deleted by user {1}", partialViewsFileSystem.GetFullPath(partialView.FileName), userId)); + } + // TODO: does this ever even happen? I don't think folders show up in the tree currently. + // Leaving this here as it was in the original PartialViewTasks code - SJ + else if (partialViewsFileSystem.DirectoryExists(partialView.FileName)) + { + partialViewsFileSystem.DeleteDirectory(partialView.FileName, true); + LogHelper.Info(string.Format("Partial View directory {0} deleted by user {1}", partialViewsFileSystem.GetFullPath(partialView.FileName), userId)); + } + + DeletedPartialView.RaiseEvent(new DeleteEventArgs(partialView, false), this); + + return true; + } + + internal Attempt SavePartialView(PartialView partialView, int userId = 0) + { + if (SavingPartialView.IsRaisedEventCancelled(new SaveEventArgs(partialView, true), this)) + { + return Attempt.Fail(new ArgumentException("Save was cancelled by an event handler " + partialView.FileName)); + } + + //Directory check.. only allow files in script dir and below to be edited + if (partialView.IsValid() == false) + { + return Attempt.Fail( + new ArgumentException(string.Format("Illegal path: {0} or illegal file extension {1}", + partialView.Path, + partialView.FileName.Substring(partialView.FileName.LastIndexOf(".", StringComparison.Ordinal))))); + } + + //NOTE: I've left the below here just for informational purposes. If we save a file this way, then the UTF8 + // BOM mucks everything up, strangely, if we use WriteAllText everything is ok! + // http://issues.umbraco.org/issue/U4-2118 + //using (var sw = System.IO.File.CreateText(savePath)) + //{ + // sw.Write(val); + //} + + System.IO.File.WriteAllText(partialView.Path, partialView.Content, Encoding.UTF8); + + //deletes the old file + if (partialView.FileName != partialView.OldFileName) + { + // Create a new PartialView class so that we can set the FileName of the file that needs deleting + var deletePartial = partialView; + deletePartial.FileName = partialView.OldFileName; + DeletePartialView(deletePartial, userId); + } + + SavedPartialView.RaiseEvent(new SaveEventArgs(partialView), this); + + return Attempt.Succeed(partialView); + } + //TODO Method to change name and/or alias of view/masterpage template #region Event Handlers @@ -425,6 +565,37 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> SavedStylesheet; + /// + /// Occurs before Save + /// + internal static event TypedEventHandler> SavingPartialView; + + /// + /// Occurs after Save + /// + internal static event TypedEventHandler> SavedPartialView; + + /// + /// Occurs before Create + /// + internal static event TypedEventHandler> CreatingPartialView; + + /// + /// Occurs after Create + /// + internal static event TypedEventHandler> CreatedPartialView; + + /// + /// Occurs before Delete + /// + internal static event TypedEventHandler> DeletingPartialView; + + /// + /// Occurs after Delete + /// + internal static event TypedEventHandler> DeletedPartialView; + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index a623b1d8e9..a2e44578a8 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Publishing; namespace Umbraco.Core.Services @@ -11,6 +12,10 @@ namespace Umbraco.Core.Services /// public interface IContentService : IService { + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); + /// /// Used to bulk update the permissions set for a content item. This will replace all permissions /// assigned to an entity with a list of user id & permission pairs. @@ -104,6 +109,20 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetChildren(int id); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, int pageNumber, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, string filter = ""); + /// /// Gets a collection of an objects versions by its Id /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 48afe70317..a457f69502 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -9,6 +9,10 @@ namespace Umbraco.Core.Services /// public interface IMediaService : IService { + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); + IEnumerable GetByIds(IEnumerable ids); /// diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index f7918b2d8d..82c21dd2fc 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -5,126 +5,180 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. /// public interface IMemberService : IMembershipMemberService { + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// IMember CreateMember(string username, string email, string name, string memberTypeAlias); + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// IMember CreateMember(string username, string email, string name, IMemberType memberType); + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias); + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// - /// The member to save the password for - /// - /// - /// This method exists so that Umbraco developers can use one entry point to create/update members if they choose to. - /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save void SavePassword(IMember member, string password); /// - /// Checks if a member with the id exists + /// Gets the count of Members by an optional MemberType alias /// - /// - /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + int Count(string memberTypeAlias = null); + + /// + /// Checks if a Member with the id exists + /// + /// Id of the Member + /// True if the Member exists otherwise False bool Exists(int id); /// - /// Get a member by the unique key + /// Gets a Member by the unique key /// - /// - /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// IMember GetByKey(Guid id); /// - /// Gets a member by it's id + /// Gets a Member by its integer id /// - /// - /// + /// Id + /// IMember GetById(int id); /// - /// Get all members for the member type alias + /// Gets all Members for the specified MemberType alias /// - /// - /// + /// Alias of the MemberType + /// IEnumerable GetMembersByMemberType(string memberTypeAlias); /// - /// Get all members for the member type id + /// Gets all Members for the MemberType id /// - /// - /// + /// Id of the MemberType + /// IEnumerable GetMembersByMemberType(int memberTypeId); /// - /// Get all members in the member group name specified + /// Gets all Members within the specified MemberGroup name /// - /// - /// + /// Name of the MemberGroup + /// IEnumerable GetMembersByGroup(string memberGroupName); /// - /// Get all members with the ids specified + /// Gets all Members with the ids specified /// - /// - /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// IEnumerable GetAllMembers(params int[] ids); /// - /// Delete members of the specified member type id + /// Delete Members of the specified MemberType id /// - /// + /// Id of the MemberType void DeleteMembersOfType(int memberTypeId); /// - /// Find members based on their display name + /// Finds Members based on their display name /// - /// - /// - /// - /// - /// - /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); /// - /// Get members based on a property search + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact); /// - /// Get members based on a property search + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); /// - /// Get members based on a property search + /// Gets a list of Members based on a property search /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value); /// - /// Get members based on a property search + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMembershipMemberService.cs b/src/Umbraco.Core/Services/IMembershipMemberService.cs index d4d865614a..801f88d9ac 100644 --- a/src/Umbraco.Core/Services/IMembershipMemberService.cs +++ b/src/Umbraco.Core/Services/IMembershipMemberService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; @@ -13,12 +12,21 @@ namespace Umbraco.Core.Services /// Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl. /// public interface IMembershipMemberService : IMembershipMemberService, IMembershipRoleService - { + { + /// + /// Creates and persists a new Member + /// + /// Username of the Member to create + /// Email of the Member to create + /// which the Member should be based on + /// IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType); } /// /// Defines part of the UserService/MemberService, which is specific to methods used by the membership provider. + /// The generic type is restricted to . The implementation of this interface uses + /// either for the MemberService or for the UserService. /// /// /// Idea is to have this is an isolated interface so that it can be easily 'replaced' in the membership provider impl. @@ -27,69 +35,124 @@ namespace Umbraco.Core.Services where T : class, IMembershipUser { /// - /// Returns the default member type alias + /// Gets the total number of Members or Users based on the count type /// - /// - string GetDefaultMemberType(); - - /// - /// Checks if a member with the username exists - /// - /// - /// - bool Exists(string username); - - /// - /// Creates and persists a new member - /// - /// - /// - /// - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// - /// - /// - T CreateWithIdentity(string username, string email, string rawPasswordValue, string memberTypeAlias); - - /// - /// Gets the member by the provider key - /// - /// - /// - T GetByProviderKey(object id); - - /// - /// Get a member by email - /// - /// - /// - T GetByEmail(string email); - - T GetByUsername(string login); - - void Delete(T membershipUser); - - void Save(T entity, bool raiseEvents = true); - - void Save(IEnumerable entities, bool raiseEvents = true); - - IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - - IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - - /// - /// Gets the total number of members based on the count type - /// - /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Members or Users for passed in type int GetCount(MemberCountType countType); /// - /// Gets a list of paged member data + /// Gets the default MemberType alias /// - /// - /// - /// - /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType + string GetDefaultMemberType(); + + /// + /// Checks if a Member with the username exists + /// + /// Username to check + /// True if the Member exists otherwise False + bool Exists(string username); + + /// + /// Creates and persists a new + /// + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// + T CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias); + + /// + /// Gets an by its provider key + /// + /// An can be of type or + /// Id to use for retrieval + /// + T GetByProviderKey(object id); + + /// + /// Get an by email + /// + /// An can be of type or + /// Email to use for retrieval + /// + T GetByEmail(string email); + + /// + /// Get an by username + /// + /// An can be of type or + /// Username to use for retrieval + /// + T GetByUsername(string username); + + /// + /// Deletes an + /// + /// An can be of type or + /// or to Delete + void Delete(T membershipUser); + + /// + /// Saves an + /// + /// An can be of type or + /// or to Save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + void Save(T entity, bool raiseEvents = true); + + /// + /// Saves a list of objects + /// + /// An can be of type or + /// to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + void Save(IEnumerable entities, bool raiseEvents = true); + + /// + /// Finds a list of objects by a partial email string + /// + /// An can be of type or + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + + /// + /// Finds a list of objects by a partial username + /// + /// An can be of type or + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + + /// + /// Gets a list of paged objects + /// + /// An can be of type or + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMembershipUserService.cs b/src/Umbraco.Core/Services/IMembershipUserService.cs index 86dc09d225..182d08f9bd 100644 --- a/src/Umbraco.Core/Services/IMembershipUserService.cs +++ b/src/Umbraco.Core/Services/IMembershipUserService.cs @@ -10,6 +10,16 @@ namespace Umbraco.Core.Services /// public interface IMembershipUserService : IMembershipMemberService { + /// + /// Creates and persists a new User + /// + /// The user will be saved in the database and returned with an Id. + /// This method is convenient when you need to perform operations, which needs the + /// Id of the user once its been created. + /// Username of the User to create + /// Email of the User to create + /// which the User should be based on + /// IUser CreateUserWithIdentity(string username, string email, IUserType userType); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 68a53d1b7f..9dcc6df615 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -237,6 +237,15 @@ namespace Umbraco.Core.Services /// Returns True if any relations exist between the entities, otherwise False bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias); + /// + /// Checks whether two items are related + /// + /// Id of the Parent relation + /// Id of the Child relation + /// Alias of the type of relation to create + /// Returns True if any relations exist between the entities, otherwise False + bool AreRelated(int parentId, int childId, string relationTypeAlias); + /// /// Saves a /// diff --git a/src/Umbraco.Core/Services/ITagService.cs b/src/Umbraco.Core/Services/ITagService.cs index 87b47c1cbc..02ac6f79ff 100644 --- a/src/Umbraco.Core/Services/ITagService.cs +++ b/src/Umbraco.Core/Services/ITagService.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using System.Linq; -using System.Threading; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -16,56 +14,110 @@ namespace Umbraco.Core.Services /// public interface ITagService : IService { - + /// + /// Gets tagged Content by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Content, not the actual Content item. + /// Name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedContentByTagGroup(string tagGroup); + + /// + /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Content, not the actual Content item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null); + + /// + /// Gets tagged Media by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Media, not the actual Media item. + /// Name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedMediaByTagGroup(string tagGroup); + + /// + /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Media, not the actual Media item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null); + + /// + /// Gets tagged Members by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Member, not the actual Member item. + /// Name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedMembersByTagGroup(string tagGroup); + + /// + /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Member, not the actual Member item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null); /// - /// Get every tag stored in the database (with optional group) + /// Gets every tag stored in the database /// - IEnumerable GetAllTags(string group = null); + /// Optional name of the 'Tag Group' + /// An enumerable list of + IEnumerable GetAllTags(string tagGroup = null); /// - /// Get all tags for content items (with optional group) + /// Gets all tags for content items /// - /// - /// - IEnumerable GetAllContentTags(string group = null); + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + IEnumerable GetAllContentTags(string tagGroup = null); /// - /// Get all tags for media items (with optional group) + /// Gets all tags for media items /// - /// - /// - IEnumerable GetAllMediaTags(string group = null); + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + IEnumerable GetAllMediaTags(string tagGroup = null); /// - /// Get all tags for member items (with optional group) + /// Gets all tags for member items /// - /// - /// - IEnumerable GetAllMemberTags(string group = null); + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + IEnumerable GetAllMemberTags(string tagGroup = null); /// - /// Returns all tags attached to a property by entity id + /// Gets all tags attached to a property by entity id /// - /// - /// - /// - /// + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// The content item id to get tags for + /// Property type alias + /// Optional name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null); /// - /// Returns all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity (content, media or member) by entity id /// - /// - /// - /// + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// The content item id to get tags for + /// Optional name of the 'Tag Group' + /// An enumerable list of IEnumerable GetTagsForEntity(int contentId, string tagGroup = null); - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 191f9085f6..559bf100d0 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Web; using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services @@ -12,18 +11,18 @@ namespace Umbraco.Core.Services /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// - /// The user to save the password for - /// /// /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. /// + /// The user to save the password for + /// The password to save void SavePassword(IUser user, string password); /// - /// To permanently delete the user pass in true, otherwise they will just be disabled + /// Deletes or disables a User /// - /// - /// + /// to delete + /// True to permanently delete the user, False to disable the user void Delete(IUser user, bool deletePermanently); /// @@ -34,79 +33,86 @@ namespace Umbraco.Core.Services IProfile GetProfileById(int id); /// - /// Get profile by username + /// Gets a profile by username /// - /// - /// + /// Username + /// IProfile GetProfileByUserName(string username); /// - /// Get user by Id + /// Gets a user by Id /// - /// - /// + /// Id of the user to retrieve + /// IUser GetUserById(int id); /// - /// This is useful when an entire section is removed from config + /// Removes a specific section from all users /// - /// + /// This is useful when an entire section is removed from config + /// Alias of the section to remove void DeleteSectionFromAllUsers(string sectionAlias); /// - /// Get permissions set for user and specified node ids + /// Get permissions set for a user and optional node ids /// - /// - /// - /// Specifiying nothing will return all user permissions for all nodes - /// - /// + /// If no permissions are found for a particular entity then the user's default permissions will be applied + /// User to retrieve permissions for + /// Specifiying nothing will return all user permissions for all nodes + /// An enumerable list of IEnumerable GetPermissions(IUser user, params int[] nodeIds); /// /// Replaces the same permission set for a single user to any number of entities /// - /// - /// - /// + /// If no 'entityIds' are specified all permissions will be removed for the specified user. + /// Id of the user + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds); #region User types + /// + /// Gets all UserTypes or thosed specified as parameters + /// + /// Optional Ids of UserTypes to retrieve + /// An enumerable list of IEnumerable GetAllUserTypes(params int[] ids); /// - /// Gets an IUserType by its Alias + /// Gets a UserType by its Alias /// /// Alias of the UserType to retrieve /// IUserType GetUserTypeByAlias(string alias); /// - /// Gets an IUserType by its Id + /// Gets a UserType by its Id /// - /// - /// + /// Id of the UserType to retrieve + /// IUserType GetUserTypeById(int id); /// - /// Gets an IUserType by its Name + /// Gets a UserType by its Name /// /// Name of the UserType to retrieve /// IUserType GetUserTypeByName(string name); /// - /// Saves an IUserType + /// Saves a UserType /// - /// - /// + /// UserType to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events void SaveUserType(IUserType userType, bool raiseEvents = true); /// - /// Deletes an IUserType + /// Deletes a UserType /// - /// + /// UserType to delete void DeleteUserType(IUserType userType); #endregion diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index accbba33cb..518be0ef07 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -251,6 +251,33 @@ namespace Umbraco.Core.Services } } + public int Count(string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + /// /// Gets an object by Id /// diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 437105c642..21a063935f 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -1,21 +1,15 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Web.Security; using System.Xml.Linq; -using System.Xml.Linq; -using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; -using Umbraco.Core.Events; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using System.Linq; @@ -23,7 +17,6 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Services { - /// /// Represents the MemberService. /// @@ -77,9 +70,11 @@ namespace Umbraco.Core.Services #region IMemberService Implementation /// - /// Get the default member type from the database - first check if the type "Member" is there, if not choose the first one found + /// Gets the default MemberType alias /// - /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType public string GetDefaultMemberType() { using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) @@ -101,10 +96,10 @@ namespace Umbraco.Core.Services } /// - /// Checks if a member with the username exists + /// Checks if a Member with the username exists /// - /// - /// + /// Username to check + /// True if the Member exists otherwise False public bool Exists(string username) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -116,11 +111,10 @@ namespace Umbraco.Core.Services /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// - /// The member to save the password for - /// - /// - /// This method exists so that Umbraco developers can use one entry point to create/update members if they choose to. - /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save public void SavePassword(IMember member, string password) { if (member == null) throw new ArgumentNullException("member"); @@ -148,10 +142,10 @@ namespace Umbraco.Core.Services } /// - /// Checks if a member with the id exists + /// Checks if a Member with the id exists /// - /// - /// + /// Id of the Member + /// True if the Member exists otherwise False public bool Exists(int id) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -161,10 +155,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a Member by its integer Id + /// Gets a Member by its integer id /// - /// - /// + /// Id + /// public IMember GetById(int id) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -174,14 +168,12 @@ namespace Umbraco.Core.Services } /// - /// Gets a Member by its Guid key + /// Gets a Member by the unique key /// - /// - /// The guid key corresponds to the unique id in the database - /// and the user id in the membership provider. - /// - /// - /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// public IMember GetByKey(Guid id) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -193,10 +185,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members by their MemberType + /// Gets all Members for the specified MemberType alias /// - /// - /// + /// Alias of the MemberType + /// public IEnumerable GetMembersByMemberType(string memberTypeAlias) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -208,10 +200,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members by their MemberType + /// Gets all Members for the MemberType id /// - /// - /// + /// Id of the MemberType + /// public IEnumerable GetMembersByMemberType(int memberTypeId) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -224,10 +216,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members by the MemberGroup they are part of + /// Gets all Members within the specified MemberGroup name /// - /// - /// + /// Name of the MemberGroup + /// public IEnumerable GetMembersByGroup(string memberGroupName) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -237,10 +229,11 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of all Members + /// Gets all Members with the ids specified /// - /// - /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// public IEnumerable GetAllMembers(params int[] ids) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -249,6 +242,10 @@ namespace Umbraco.Core.Services } } + /// + /// Delete Members of the specified MemberType id + /// + /// Id of the MemberType public void DeleteMembersOfType(int memberTypeId) { using (new WriteLock(Locker)) @@ -272,6 +269,15 @@ namespace Umbraco.Core.Services } } + /// + /// Finds Members based on their display name + /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = _uowProvider.GetUnitOfWork(); @@ -314,14 +320,14 @@ namespace Umbraco.Core.Services } /// - /// Does a search for members that contain the specified string in their email address + /// Finds a list of objects by a partial email string /// - /// - /// - /// - /// - /// - /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = _uowProvider.GetUnitOfWork(); @@ -354,6 +360,15 @@ namespace Umbraco.Core.Services } } + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = _uowProvider.GetUnitOfWork(); @@ -387,12 +402,12 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members with a certain string property value + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -443,12 +458,12 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members with a certain integer property value + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -502,11 +517,11 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members with a certain boolean property value + /// Gets a list of Members based on a property search /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -523,12 +538,12 @@ namespace Umbraco.Core.Services } /// - /// Gets a list of Members with a certain date time property value + /// Gets a list of Members based on a property search /// - /// - /// - /// - /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -586,15 +601,15 @@ namespace Umbraco.Core.Services #region IMembershipMemberService Implementation /// - /// Returns the count of members based on the countType + /// Gets the total number of Members based on the count type /// - /// - /// /// /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science /// but that is how MS have made theirs so we'll follow that principal. /// + /// to count by + /// with number of Members for passed in type public int GetCount(MemberCountType countType) { using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) @@ -635,6 +650,13 @@ namespace Umbraco.Core.Services } + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) { var uow = _uowProvider.GetUnitOfWork(); @@ -645,13 +667,31 @@ namespace Umbraco.Core.Services } /// - /// Creates a member object + /// Gets the count of Members by an optional MemberType alias /// - /// - /// - /// - /// - /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + public int Count(string memberTypeAlias = null) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMemberRepository(uow)) + { + return repository.Count(memberTypeAlias); + } + } + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// public IMember CreateMember(string username, string email, string name, string memberTypeAlias) { var memberType = FindMemberTypeByAlias(memberTypeAlias); @@ -659,13 +699,16 @@ namespace Umbraco.Core.Services } /// - /// Creates a new member object + /// Creates an object without persisting it /// - /// - /// - /// - /// - /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// public IMember CreateMember(string username, string email, string name, IMemberType memberType) { var member = new Member(name, email.ToLower().Trim(), username, memberType); @@ -676,13 +719,15 @@ namespace Umbraco.Core.Services } /// - /// Creates a member with an Id + /// Creates and persists a Member /// - /// - /// - /// - /// - /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) { var memberType = FindMemberTypeByAlias(memberTypeAlias); @@ -690,49 +735,65 @@ namespace Umbraco.Core.Services } /// - /// Creates a member with an Id, the username will be used as their name + /// Creates and persists a Member /// - /// - /// - /// - /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// MemberType the Member should be based on + /// public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) { return CreateMemberWithIdentity(username, email, username, memberType); } /// - /// Creates a member with an Id + /// Creates and persists a Member /// - /// - /// - /// - /// - /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) { return CreateMemberWithIdentity(username, email, name, "", memberType); } /// - /// Creates and persists a new Member + /// Creates and persists a new /// - /// - /// - /// - /// - /// - IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string rawPasswordValue, string memberTypeAlias) + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMemberWithIdentity(username, email, username, rawPasswordValue, memberType); + return CreateMemberWithIdentity(username, email, username, passwordValue, memberType); } - private IMember CreateMemberWithIdentity(string username, string email, string name, string rawPasswordValue, IMemberType memberType) + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// MemberType the Member should be based on + /// + private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType) { if (memberType == null) throw new ArgumentNullException("memberType"); - var member = new Member(name, email.ToLower().Trim(), username, rawPasswordValue, memberType); + var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType); if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) { @@ -762,13 +823,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a Member by its Id + /// Gets an by its provider key /// - /// - /// The Id should be an integer or Guid. - /// - /// - /// + /// Id to use for retrieval + /// public IMember GetByProviderKey(object id) { var asGuid = id.TryConvertTo(); @@ -786,10 +844,10 @@ namespace Umbraco.Core.Services } /// - /// Gets a Member by its Email + /// Get an by email /// - /// - /// + /// Email to use for retrieval + /// public IMember GetByEmail(string email) { var uow = _uowProvider.GetUnitOfWork(); @@ -803,16 +861,20 @@ namespace Umbraco.Core.Services } /// - /// Gets a Member by its Username + /// Get an by username /// - /// - /// - public IMember GetByUsername(string userName) + /// Username to use for retrieval + /// + public IMember GetByUsername(string username) { + //TODO: Somewhere in here, whether at this level or the repository level, we need to add + // a caching mechanism since this method is used by all the membership providers and could be + // called quite a bit when dealing with members. + var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMemberRepository(uow)) { - var query = Query.Builder.Where(x => x.Username.Equals(userName)); + var query = Query.Builder.Where(x => x.Username.Equals(username)); var member = repository.GetByQuery(query).FirstOrDefault(); return member; @@ -820,9 +882,9 @@ namespace Umbraco.Core.Services } /// - /// Deletes a Member + /// Deletes an /// - /// + /// to Delete public void Delete(IMember member) { if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(member), this)) @@ -837,12 +899,13 @@ namespace Umbraco.Core.Services Deleted.RaiseEvent(new DeleteEventArgs(member, false), this); } - + /// - /// Saves an updated Member + /// Saves an /// - /// - /// + /// to Save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events public void Save(IMember entity, bool raiseEvents = true) { if (raiseEvents) @@ -851,7 +914,6 @@ namespace Umbraco.Core.Services { return; } - } var uow = _uowProvider.GetUnitOfWork(); @@ -872,6 +934,12 @@ namespace Umbraco.Core.Services Saved.RaiseEvent(new SaveEventArgs(entity, false), this); } + /// + /// Saves a list of objects + /// + /// to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { var asArray = entities.ToArray(); @@ -1252,7 +1320,5 @@ namespace Umbraco.Core.Services return new Member(name, email, username, password, memType); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f358e41bf3..375e1ca1ba 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -144,7 +144,7 @@ namespace Umbraco.Core.Services _dataTypeService = new Lazy(() => new DataTypeService(provider, repositoryFactory.Value)); if (_fileService == null) - _fileService = new Lazy(() => new FileService(fileProvider, provider, repositoryFactory.Value)); + _fileService = new Lazy(() => new FileService(fileProvider, provider, repositoryFactory.Value, _macroService.Value)); if (_localizationService == null) _localizationService = new Lazy(() => new LocalizationService(provider, repositoryFactory.Value)); diff --git a/src/Umbraco.Core/Services/TagService.cs b/src/Umbraco.Core/Services/TagService.cs index 01c2826cea..ebe39377ad 100644 --- a/src/Umbraco.Core/Services/TagService.cs +++ b/src/Umbraco.Core/Services/TagService.cs @@ -1,23 +1,19 @@ using System.Collections.Generic; -using System.Linq; using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { /// - /// Tag service to query for tags in the tags db table. The tags returned are only relavent for published content & saved media or members + /// Tag service to query for tags in the tags db table. The tags returned are only relevant for published content & saved media or members /// /// /// If there is unpublished content with tags, those tags will not be contained /// public class TagService : ITagService { - private readonly RepositoryFactory _repositoryFactory; private readonly IDatabaseUnitOfWorkProvider _uowProvider; @@ -41,6 +37,12 @@ namespace Umbraco.Core.Services _uowProvider = provider; } + /// + /// Gets tagged Content by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Content, not the actual Content item. + /// Name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedContentByTagGroup(string tagGroup) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -49,6 +51,13 @@ namespace Umbraco.Core.Services } } + /// + /// Gets tagged Content by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Content, not the actual Content item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedContentByTag(string tag, string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -57,6 +66,12 @@ namespace Umbraco.Core.Services } } + /// + /// Gets tagged Media by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Media, not the actual Media item. + /// Name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedMediaByTagGroup(string tagGroup) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -65,6 +80,13 @@ namespace Umbraco.Core.Services } } + /// + /// Gets tagged Media by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Media, not the actual Media item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedMediaByTag(string tag, string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -73,6 +95,12 @@ namespace Umbraco.Core.Services } } + /// + /// Gets tagged Members by a specific 'Tag Group'. + /// + /// The contains the Id and Tags of the Member, not the actual Member item. + /// Name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedMembersByTagGroup(string tagGroup) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -81,6 +109,13 @@ namespace Umbraco.Core.Services } } + /// + /// Gets tagged Members by a specific 'Tag' and optional 'Tag Group'. + /// + /// The contains the Id and Tags of the Member, not the actual Member item. + /// Tag + /// Optional name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTaggedMembersByTag(string tag, string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -90,69 +125,79 @@ namespace Umbraco.Core.Services } /// - /// Get every tag stored in the database (with optional group) + /// Gets every tag stored in the database /// - public IEnumerable GetAllTags(string group = null) + /// Optional name of the 'Tag Group' + /// An enumerable list of + public IEnumerable GetAllTags(string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) { - if (group.IsNullOrWhiteSpace()) + if (tagGroup.IsNullOrWhiteSpace()) { return repository.GetAll(); } - var query = Query.Builder.Where(x => x.Group == group); + var query = Query.Builder.Where(x => x.Group == tagGroup); var definitions = repository.GetByQuery(query); return definitions; } } /// - /// Get all tags for content items (with optional group) + /// Gets all tags for content items /// - /// - /// - public IEnumerable GetAllContentTags(string group = null) + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + public IEnumerable GetAllContentTags(string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) { - return repository.GetTagsForEntityType(TaggableObjectTypes.Content, group); + return repository.GetTagsForEntityType(TaggableObjectTypes.Content, tagGroup); } } /// - /// Get all tags for media items (with optional group) + /// Gets all tags for media items /// - /// - /// - public IEnumerable GetAllMediaTags(string group = null) + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + public IEnumerable GetAllMediaTags(string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) { - return repository.GetTagsForEntityType(TaggableObjectTypes.Media, group); + return repository.GetTagsForEntityType(TaggableObjectTypes.Media, tagGroup); } } /// - /// Get all tags for member items (with optional group) + /// Gets all tags for member items /// - /// - /// - public IEnumerable GetAllMemberTags(string group = null) + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// Optional name of the 'Tag Group' + /// An enumerable list of + public IEnumerable GetAllMemberTags(string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) { - return repository.GetTagsForEntityType(TaggableObjectTypes.Member, group); + return repository.GetTagsForEntityType(TaggableObjectTypes.Member, tagGroup); } } /// - /// Returns all tags attached to a property by entity id + /// Gets all tags attached to a property by entity id /// - /// - /// - /// - /// + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// The content item id to get tags for + /// Property type alias + /// Optional name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) @@ -162,11 +207,13 @@ namespace Umbraco.Core.Services } /// - /// Returns all tags attached to an entity (content, media or member) by entity id + /// Gets all tags attached to an entity (content, media or member) by entity id /// - /// - /// - /// + /// Use the optional tagGroup parameter to limit the + /// result to a specific 'Tag Group'. + /// The content item id to get tags for + /// Optional name of the 'Tag Group' + /// An enumerable list of public IEnumerable GetTagsForEntity(int contentId, string tagGroup = null) { using (var repository = _repositoryFactory.CreateTagsRepository(_uowProvider.GetUnitOfWork())) diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 3b426a2a0d..1a523835c8 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core.Events; -using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; @@ -13,8 +10,6 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Services { - - /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. /// @@ -40,10 +35,11 @@ namespace Umbraco.Core.Services #region Implementation of IMembershipUserService /// - /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll return the first type that is not an admin, otherwise if there's only one - /// we will return that one. + /// Gets the default MemberType alias /// - /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType public string GetDefaultMemberType() { using (var repository = _repositoryFactory.CreateUserTypeRepository(_uowProvider.GetUnitOfWork())) @@ -70,6 +66,11 @@ namespace Umbraco.Core.Services } } + /// + /// Checks if a User with the username exists + /// + /// Username to check + /// True if the User exists otherwise False public bool Exists(string username) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) @@ -78,12 +79,28 @@ namespace Umbraco.Core.Services } } + /// + /// Creates a new User + /// + /// The user will be saved in the database and returned with an Id + /// Username of the user to create + /// Email of the user to create + /// which the User should be based on + /// public IUser CreateUserWithIdentity(string username, string email, IUserType userType) { return CreateUserWithIdentity(username, email, "", userType); } - IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string rawPasswordValue, string memberTypeAlias) + /// + /// Creates and persists a new + /// + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// + IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { var userType = GetUserTypeByAlias(memberTypeAlias); if (userType == null) @@ -91,10 +108,20 @@ namespace Umbraco.Core.Services throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved"); } - return CreateUserWithIdentity(username, email, rawPasswordValue, userType); + return CreateUserWithIdentity(username, email, passwordValue, userType); } - private IUser CreateUserWithIdentity(string username, string email, string rawPasswordValue, IUserType userType) + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// MemberType the Member should be based on + /// + private IUser CreateUserWithIdentity(string username, string email, string passwordValue, IUserType userType) { if (userType == null) throw new ArgumentNullException("userType"); @@ -113,7 +140,7 @@ namespace Umbraco.Core.Services Email = email, Language = Configuration.GlobalSettings.DefaultUILanguage, Name = username, - RawPasswordValue = rawPasswordValue, + RawPasswordValue = passwordValue, Username = username, StartContentId = -1, StartMediaId = -1, @@ -133,6 +160,11 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a User by its integer id + /// + /// Id + /// public IUser GetById(int id) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) @@ -143,6 +175,11 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an by its provider key + /// + /// Id to use for retrieval + /// public IUser GetByProviderKey(object id) { var asInt = id.TryConvertTo(); @@ -154,6 +191,11 @@ namespace Umbraco.Core.Services return null; } + /// + /// Get an by email + /// + /// Email to use for retrieval + /// public IUser GetByEmail(string email) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) @@ -165,19 +207,24 @@ namespace Umbraco.Core.Services } } - public IUser GetByUsername(string login) + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + public IUser GetByUsername(string username) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.Username.Equals(login)); + var query = Query.Builder.Where(x => x.Username.Equals(username)); return repository.GetByQuery(query).FirstOrDefault(); } } /// - /// This disables and renames the user, it does not delete them, use the overload to delete them + /// Deletes an /// - /// + /// to Delete public void Delete(IUser membershipUser) { //disable @@ -197,11 +244,11 @@ namespace Umbraco.Core.Services /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// - /// The user to save the password for - /// /// /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. /// + /// The user to save the password for + /// The password to save public void SavePassword(IUser user, string password) { if (user == null) throw new ArgumentNullException("user"); @@ -225,10 +272,10 @@ namespace Umbraco.Core.Services } /// - /// To permanently delete the user pass in true, otherwise they will just be disabled + /// Deletes or disables a User /// - /// - /// + /// to delete + /// True to permanently delete the user, False to disable the user public void Delete(IUser user, bool deletePermanently) { if (deletePermanently == false) @@ -251,6 +298,12 @@ namespace Umbraco.Core.Services } } + /// + /// Saves an + /// + /// to Save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events public void Save(IUser entity, bool raiseEvents = true) { if (raiseEvents) @@ -270,6 +323,12 @@ namespace Umbraco.Core.Services SavedUser.RaiseEvent(new SaveEventArgs(entity, false), this); } + /// + /// Saves a list of objects + /// + /// to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { if (raiseEvents) @@ -293,6 +352,15 @@ namespace Umbraco.Core.Services SavedUser.RaiseEvent(new SaveEventArgs(entities, false), this); } + /// + /// Finds a list of objects by a partial email string + /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = _uowProvider.GetUnitOfWork(); @@ -325,6 +393,15 @@ namespace Umbraco.Core.Services } } + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = _uowProvider.GetUnitOfWork(); @@ -357,6 +434,16 @@ namespace Umbraco.Core.Services } } + /// + /// Gets the total number of Users based on the count type + /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Users for passed in type public int GetCount(MemberCountType countType) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) @@ -393,6 +480,13 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) { var uow = _uowProvider.GetUnitOfWork(); @@ -417,12 +511,22 @@ namespace Umbraco.Core.Services return user.ProfileData; } - public IProfile GetProfileByUserName(string login) + /// + /// Gets a profile by username + /// + /// Username + /// + public IProfile GetProfileByUserName(string username) { - var user = GetByUsername(login); + var user = GetByUsername(username); return user.ProfileData; } - + + /// + /// Gets a user by Id + /// + /// Id of the user to retrieve + /// public IUser GetUserById(int id) { using (var repository = _repositoryFactory.CreateUserRepository(_uowProvider.GetUnitOfWork())) @@ -430,13 +534,14 @@ namespace Umbraco.Core.Services return repository.Get(id); } } - + /// /// Replaces the same permission set for a single user to any number of entities /// - /// - /// - /// + /// If no 'entityIds' are specified all permissions will be removed for the specified user. + /// Id of the user + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) { var uow = _uowProvider.GetUnitOfWork(); @@ -446,6 +551,11 @@ namespace Umbraco.Core.Services } } + /// + /// Gets all UserTypes or thosed specified as parameters + /// + /// Optional Ids of UserTypes to retrieve + /// An enumerable list of public IEnumerable GetAllUserTypes(params int[] ids) { var uow = _uowProvider.GetUnitOfWork(); @@ -456,7 +566,7 @@ namespace Umbraco.Core.Services } /// - /// Gets an IUserType by its Alias + /// Gets a UserType by its Alias /// /// Alias of the UserType to retrieve /// @@ -470,6 +580,11 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a UserType by its Id + /// + /// Id of the UserType to retrieve + /// public IUserType GetUserTypeById(int id) { using (var repository = _repositoryFactory.CreateUserTypeRepository(_uowProvider.GetUnitOfWork())) @@ -479,7 +594,7 @@ namespace Umbraco.Core.Services } /// - /// Gets an IUserType by its Name + /// Gets a UserType by its Name /// /// Name of the UserType to retrieve /// @@ -493,6 +608,12 @@ namespace Umbraco.Core.Services } } + /// + /// Saves a UserType + /// + /// UserType to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events public void SaveUserType(IUserType userType, bool raiseEvents = true) { if (raiseEvents) @@ -512,6 +633,10 @@ namespace Umbraco.Core.Services SavedUserType.RaiseEvent(new SaveEventArgs(userType, false), this); } + /// + /// Deletes a UserType + /// + /// UserType to delete public void DeleteUserType(IUserType userType) { if (DeletingUserType.IsRaisedEventCancelled(new DeleteEventArgs(userType), this)) @@ -528,9 +653,10 @@ namespace Umbraco.Core.Services } /// - /// This is useful for when a section is removed from config + /// Removes a specific section from all users /// - /// + /// This is useful when an entire section is removed from config + /// Alias of the section to remove public void DeleteSectionFromAllUsers(string sectionAlias) { var uow = _uowProvider.GetUnitOfWork(); @@ -548,14 +674,12 @@ namespace Umbraco.Core.Services } /// - /// Returns permissions for a given user for any number of nodes + /// Get permissions set for a user and optional node ids /// - /// - /// - /// - /// - /// If no permissions are found for a particular entity then the user's default permissions will be applied - /// + /// If no permissions are found for a particular entity then the user's default permissions will be applied + /// User to retrieve permissions for + /// Specifiying nothing will return all user permissions for all nodes + /// An enumerable list of public IEnumerable GetPermissions(IUser user, params int[] nodeIds) { var uow = _uowProvider.GetUnitOfWork(); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 3eedb69eb4..b192704adb 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); private static readonly char[] ToCSharpEscapeChars; - + static StringExtensions() { var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" }; @@ -56,7 +56,7 @@ namespace Umbraco.Core } return fileName; - + } /// @@ -72,6 +72,13 @@ namespace Umbraco.Core || (input.StartsWith("[") && input.EndsWith("]")); } + internal static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); + internal static readonly string[] JsonEmpties = new [] { "[]", "{}" }; + internal static bool DetectIsEmptyJson(this string input) + { + return JsonEmpties.Contains(Whitespace.Replace(input, string.Empty)); + } + /// /// Returns a JObject/JArray instance if the string can be converted to json, otherwise returns the string /// @@ -158,7 +165,7 @@ namespace Umbraco.Core //remove any prefixed '&' or '?' for (var i = 0; i < queryStrings.Length; i++) { - queryStrings[i] = queryStrings[i].TrimStart('?', '&').TrimEnd('&'); + queryStrings[i] = queryStrings[i].TrimStart('?', '&').TrimEnd('&'); } var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); @@ -169,66 +176,66 @@ namespace Umbraco.Core } return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); } - - /// - /// Encrypt the string using the MachineKey in medium trust - /// - /// The string value to be encrypted. - /// The encrypted string. - public static string EncryptWithMachineKey(this string value) + + /// + /// Encrypt the string using the MachineKey in medium trust + /// + /// The string value to be encrypted. + /// The encrypted string. + public static string EncryptWithMachineKey(this string value) { - if (value == null) - return null; + if (value == null) + return null; - string valueToEncrypt = value; - List parts = new List(); + string valueToEncrypt = value; + List parts = new List(); - const int EncrpytBlockSize = 500; + const int EncrpytBlockSize = 500; - while (valueToEncrypt.Length > EncrpytBlockSize) - { - parts.Add(valueToEncrypt.Substring(0, EncrpytBlockSize)); - valueToEncrypt = valueToEncrypt.Remove(0, EncrpytBlockSize); - } + while (valueToEncrypt.Length > EncrpytBlockSize) + { + parts.Add(valueToEncrypt.Substring(0, EncrpytBlockSize)); + valueToEncrypt = valueToEncrypt.Remove(0, EncrpytBlockSize); + } - if (valueToEncrypt.Length > 0) - { - parts.Add(valueToEncrypt); - } + if (valueToEncrypt.Length > 0) + { + parts.Add(valueToEncrypt); + } - StringBuilder encrpytedValue = new StringBuilder(); + StringBuilder encrpytedValue = new StringBuilder(); - foreach (var part in parts) - { - var encrpytedBlock = FormsAuthentication.Encrypt(new FormsAuthenticationTicket(0, string.Empty, DateTime.Now, DateTime.MaxValue, false, part)); - encrpytedValue.AppendLine(encrpytedBlock); - } + foreach (var part in parts) + { + var encrpytedBlock = FormsAuthentication.Encrypt(new FormsAuthenticationTicket(0, string.Empty, DateTime.Now, DateTime.MaxValue, false, part)); + encrpytedValue.AppendLine(encrpytedBlock); + } - return encrpytedValue.ToString().TrimEnd(); + return encrpytedValue.ToString().TrimEnd(); } /// - /// Decrypt the encrypted string using the Machine key in medium trust - /// - /// The string value to be decrypted - /// The decrypted string. - public static string DecryptWithMachineKey(this string value) + /// Decrypt the encrypted string using the Machine key in medium trust + /// + /// The string value to be decrypted + /// The decrypted string. + public static string DecryptWithMachineKey(this string value) { - if (value == null) - return null; + if (value == null) + return null; - string[] parts = value.Split('\n'); + string[] parts = value.Split('\n'); - StringBuilder decryptedValue = new StringBuilder(); + StringBuilder decryptedValue = new StringBuilder(); - foreach (var part in parts) - { - decryptedValue.Append(FormsAuthentication.Decrypt(part.TrimEnd()).UserData); - } + foreach (var part in parts) + { + decryptedValue.Append(FormsAuthentication.Decrypt(part.TrimEnd()).UserData); + } - return decryptedValue.ToString(); + return decryptedValue.ToString(); } - + //this is from SqlMetal and just makes it a bit of fun to allow pluralisation public static string MakePluralName(this string name) { @@ -974,8 +981,8 @@ namespace Umbraco.Core { var helper = ShortStringHelper; var legacy = helper as LegacyShortStringHelper; - return legacy != null - ? legacy.LegacyToUrlAlias(value, charReplacements, replaceDoubleDashes, stripNonAscii, urlEncode) + return legacy != null + ? legacy.LegacyToUrlAlias(value, charReplacements, replaceDoubleDashes, stripNonAscii, urlEncode) : helper.CleanStringForUrlSegment(value); } @@ -1082,7 +1089,7 @@ namespace Umbraco.Core } // the new methods to get a url segment - + /// /// Cleans a string to produce a string that can safely be used in an url segment. /// @@ -1220,7 +1227,7 @@ namespace Umbraco.Core { return ShortStringHelper.CleanStringForSafeFileName(text, culture); } - + /// /// An extension method that returns a new string in which all occurrences of a /// specified string in the current instance are replaced with another specified string. @@ -1239,7 +1246,7 @@ namespace Umbraco.Core int index = -1 * newString.Length; // Determine if there are any matches left in source, starting from just after the result of replacing the last match. - while((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) + while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0) { // Remove the old text. source = source.Remove(index, oldString.Length); @@ -1310,5 +1317,28 @@ namespace Umbraco.Core var idCheckList = csv.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); return idCheckList.Contains(value); } + + // From: http://stackoverflow.com/a/961504/5018 + // filters control characters but allows only properly-formed surrogate sequences + private static readonly Regex InvalidXmlChars = + new Regex( + @"(? + /// An extension method that returns a new string in which all occurrences of an + /// unicode characters that are invalid in XML files are replaced with an empty string. + /// + /// Current instance of the string + /// Updated string + /// + /// + /// removes any unusual unicode characters that can't be encoded into XML + /// + internal static string ToValidXmlString(this string text) + { + return string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Replace(text, ""); + } } } diff --git a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs index fa9c331b10..f302dbe4d8 100644 --- a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs @@ -20,18 +20,19 @@ namespace Umbraco.Core.Sync { private readonly Func> _getUserNamePasswordDelegate; private volatile bool _hasResolvedDelegate = false; - private readonly object _locker = new object(); - private bool _useDistributedCalls; + private readonly object _locker = new object(); protected string Login { get; private set; } protected string Password{ get; private set; } + protected bool UseDistributedCalls { get; private set; } + /// /// Without a username/password all distribuion will be disabled /// internal DefaultServerMessenger() { - _useDistributedCalls = false; + UseDistributedCalls = false; } /// @@ -55,7 +56,7 @@ namespace Umbraco.Core.Sync if (login == null) throw new ArgumentNullException("login"); if (password == null) throw new ArgumentNullException("password"); - _useDistributedCalls = useDistributedCalls; + UseDistributedCalls = useDistributedCalls; Login = login; Password = password; } @@ -221,13 +222,13 @@ namespace Umbraco.Core.Sync { Login = null; Password = null; - _useDistributedCalls = false; + UseDistributedCalls = false; } else { Login = result.Item1; Password = result.Item2; - _useDistributedCalls = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; + UseDistributedCalls = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; } } catch (Exception ex) @@ -235,7 +236,7 @@ namespace Umbraco.Core.Sync LogHelper.Error("Could not resolve username/password delegate, server distribution will be disabled", ex); Login = null; Password = null; - _useDistributedCalls = false; + UseDistributedCalls = false; } } } @@ -314,7 +315,7 @@ namespace Umbraco.Core.Sync //Now, check if we are using Distrubuted calls. If there are no servers in the list then we // can definitely not distribute. - if (!_useDistributedCalls || !servers.Any()) + if (!UseDistributedCalls || !servers.Any()) { //if we are not, then just invoke the call on the cache refresher InvokeMethodOnRefresherInstance(refresher, dispatchType, getId, instances); @@ -325,7 +326,7 @@ namespace Umbraco.Core.Sync MessageSeversForIdsOrJson(servers, refresher, dispatchType, instances.Select(getId)); } - private void MessageSeversForIdsOrJson( + protected virtual void MessageSeversForIdsOrJson( IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType, @@ -345,7 +346,7 @@ namespace Umbraco.Core.Sync //Now, check if we are using Distrubuted calls. If there are no servers in the list then we // can definitely not distribute. - if (!_useDistributedCalls || !servers.Any()) + if (!UseDistributedCalls || !servers.Any()) { //if we are not, then just invoke the call on the cache refresher InvokeMethodOnRefresherInstance(refresher, dispatchType, ids, jsonPayload); @@ -456,16 +457,16 @@ namespace Umbraco.Core.Sync } } - List waitHandlesList; - var asyncResults = GetAsyncResults(asyncResultsList, out waitHandlesList); - + var waitHandlesList = asyncResultsList.Select(x => x.AsyncWaitHandle).ToArray(); + var errorCount = 0; - // Once for each WaitHandle that we have, wait for a response and log it - // We're previously submitted all these requests effectively in parallel and will now retrieve responses on a FIFO basis - foreach (var t in asyncResults) + //Wait for all requests to complete + WaitHandle.WaitAll(waitHandlesList.ToArray()); + + foreach (var t in asyncResultsList) { - var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); + //var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); try { @@ -520,18 +521,7 @@ namespace Umbraco.Core.Sync LogDispatchBatchError(ee); } } - - internal IEnumerable GetAsyncResults(List asyncResultsList, out List waitHandlesList) - { - var asyncResults = asyncResultsList.ToArray(); - waitHandlesList = new List(); - foreach (var asyncResult in asyncResults) - { - waitHandlesList.Add(asyncResult.AsyncWaitHandle); - } - return asyncResults; - } - + private void LogDispatchBatchError(Exception ee) { LogHelper.Error("Error refreshing distributed list", ee); diff --git a/src/Umbraco.Core/Sync/IServerAddress.cs b/src/Umbraco.Core/Sync/IServerAddress.cs index 78645e0767..0483af1800 100644 --- a/src/Umbraco.Core/Sync/IServerAddress.cs +++ b/src/Umbraco.Core/Sync/IServerAddress.cs @@ -8,5 +8,7 @@ namespace Umbraco.Core.Sync public interface IServerAddress { string ServerAddress { get; } + + //TODO : Should probably add things like port, protocol, server name, app id } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs index 923ed3c670..6a620bc4c3 100644 --- a/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs +++ b/src/Umbraco.Core/Sync/ServerSyncWebServiceClient.cs @@ -22,19 +22,21 @@ namespace Umbraco.Core.Sync /// [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://umbraco.org/webservices/BulkRefresh", RequestNamespace = "http://umbraco.org/webservices/", ResponseNamespace = "http://umbraco.org/webservices/", Use = System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public void BulkRefresh(RefreshInstruction[] instructions, string login, string password) + public void BulkRefresh(RefreshInstruction[] instructions, string appId, string login, string password) { this.Invoke("BulkRefresh", new object[] { instructions, + appId, login, password}); } /// - public System.IAsyncResult BeginBulkRefresh(RefreshInstruction[] instructions, string login, string password, System.AsyncCallback callback, object asyncState) + public System.IAsyncResult BeginBulkRefresh(RefreshInstruction[] instructions, string appId, string login, string password, System.AsyncCallback callback, object asyncState) { return this.BeginInvoke("BulkRefresh", new object[] { instructions, + appId, login, password}, callback, asyncState); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index fa1f6767b9..c317594937 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -66,9 +66,9 @@ False ..\packages\MySql.Data.6.6.5\lib\net40\MySql.Data.dll - + False - ..\packages\Newtonsoft.Json.6.0.2\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll @@ -149,8 +149,6 @@ - - @@ -366,13 +364,14 @@ + + - diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 34e5d2a110..f01b9e68ba 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Tests/Cache/HttpRequestCacheProviderTests.cs b/src/Umbraco.Tests/Cache/HttpRequestCacheProviderTests.cs index 5483a4341b..87a468ddd5 100644 --- a/src/Umbraco.Tests/Cache/HttpRequestCacheProviderTests.cs +++ b/src/Umbraco.Tests/Cache/HttpRequestCacheProviderTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Tests.Cache { base.Setup(); _ctx = new FakeHttpContextFactory("http://localhost/test"); - _provider = new HttpRequestCacheProvider(() => _ctx.HttpContext); + _provider = new HttpRequestCacheProvider(_ctx.HttpContext); } internal override ICacheProvider Provider diff --git a/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs b/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs index c786a61996..609452978c 100644 --- a/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs +++ b/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs @@ -148,7 +148,7 @@ namespace Umbraco.Tests.Membership Assert.AreEqual(false, provider.EnablePasswordReset); Assert.AreEqual(false, provider.RequiresQuestionAndAnswer); Assert.AreEqual(true, provider.RequiresUniqueEmail); - Assert.AreEqual(5, provider.MaxInvalidPasswordAttempts); + Assert.AreEqual(20, provider.MaxInvalidPasswordAttempts); Assert.AreEqual(10, provider.PasswordAttemptWindow); Assert.AreEqual(provider.DefaultMinPasswordLength, provider.MinRequiredPasswordLength); Assert.AreEqual(provider.DefaultMinNonAlphanumericChars, provider.MinRequiredNonAlphanumericCharacters); diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 1501cf09f3..e687a74d1b 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Web; using Moq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence.Caching; using Umbraco.Core.Serialization; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -14,7 +16,7 @@ using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Models { [TestFixture] - public class ContentTests + public class ContentTests : BaseUmbracoConfigurationTest { [SetUp] public void Init() @@ -181,6 +183,62 @@ namespace Umbraco.Tests.Models Assert.AreNotSame(content.Properties, clone.Properties); } + [Ignore] + [Test] + public void Can_Deep_Clone_Perf_Test() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentType.Id = 99; + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + var i = 200; + foreach (var property in content.Properties) + { + property.Id = ++i; + } + content.Id = 10; + content.CreateDate = DateTime.Now; + content.CreatorId = 22; + content.ExpireDate = DateTime.Now; + content.Key = Guid.NewGuid(); + content.Language = "en"; + content.Level = 3; + content.Path = "-1,4,10"; + content.ReleaseDate = DateTime.Now; + content.ChangePublishedState(PublishedState.Published); + content.SortOrder = 5; + content.Template = new Template("-1,2,3,4", "Test Template", "testTemplate") + { + Id = 88 + }; + content.Trashed = false; + content.UpdateDate = DateTime.Now; + content.Version = Guid.NewGuid(); + content.WriterId = 23; + + ((IUmbracoEntity)content).AdditionalData.Add("test1", 123); + ((IUmbracoEntity)content).AdditionalData.Add("test2", "hello"); + + var runtimeCache = new RuntimeCacheProvider(); + runtimeCache.Save(typeof(IContent), content); + + using (DisposableTimer.DebugDuration("STARTING PERF TEST WITH RUNTIME CACHE")) + { + for (int j = 0; j < 1000; j++) + { + var clone = runtimeCache.GetById(typeof(IContent), content.Id.ToGuid()); + } + } + + using (DisposableTimer.DebugDuration("STARTING PERF TEST WITHOUT RUNTIME CACHE")) + { + for (int j = 0; j < 1000; j++) + { + var clone = (ContentType)contentType.DeepClone(); + } + } + } + [Test] public void Can_Deep_Clone() { diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index bb61a84d82..b62252e987 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Serialization; @@ -90,6 +91,58 @@ namespace Umbraco.Tests.Models } } + [Ignore] + [Test] + public void Can_Deep_Clone_Content_Type_Perf_Test() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentType.Id = 99; + + var i = 200; + foreach (var propertyType in contentType.PropertyTypes) + { + propertyType.Id = ++i; + } + foreach (var group in contentType.PropertyGroups) + { + group.Id = ++i; + } + contentType.AllowedTemplates = new[] { new Template("-1,2", "Name", "name") { Id = 200 }, new Template("-1,3", "Name2", "name2") { Id = 201 } }; + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; + contentType.Id = 10; + contentType.CreateDate = DateTime.Now; + contentType.CreatorId = 22; + contentType.SetDefaultTemplate(new Template("-1,2,3,4", "Test Template", "testTemplate") + { + Id = 88 + }); + contentType.Description = "test"; + contentType.Icon = "icon"; + contentType.IsContainer = true; + contentType.Thumbnail = "thumb"; + contentType.Key = Guid.NewGuid(); + contentType.Level = 3; + contentType.Path = "-1,4,10"; + contentType.SortOrder = 5; + contentType.Trashed = false; + contentType.UpdateDate = DateTime.Now; + + ((IUmbracoEntity)contentType).AdditionalData.Add("test1", 123); + ((IUmbracoEntity)contentType).AdditionalData.Add("test2", "hello"); + + using (DisposableTimer.DebugDuration("STARTING PERF TEST")) + { + for (int j = 0; j < 1000; j++) + { + using (DisposableTimer.DebugDuration("Cloning content type")) + { + var clone = (ContentType)contentType.DeepClone(); + } + } + } + } + [Test] public void Can_Deep_Clone_Content_Type() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index e75eabff4f..26510f3b5d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -15,6 +15,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using umbraco.editorControls.tinyMCE3; using umbraco.interfaces; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories { @@ -331,6 +332,132 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 2, 1, out totalRecords, "Name", Direction.Ascending); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 2, out totalRecords, "Name", Direction.Ascending); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Descending); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, "Page 2"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(1)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + int totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, "Page"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + [Test] public void Can_Perform_GetAll_By_Param_Ids_On_ContentRepository() { diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs index fc5eebed5b..f9bbc04087 100644 --- a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -36,27 +36,27 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreNotEqual(dateTime.Date, ((DateTime)result).Date); } - // see the notes in the converter - // values such as "true" are NOT expected here - - //[TestCase("TRUE", true)] - //[TestCase("True", true)] - //[TestCase("true", true)] - [TestCase("1", true)] - //[TestCase("FALSE", false)] - //[TestCase("False", false)] - //[TestCase("false", false)] - [TestCase("0", false)] - [TestCase("", false)] - [TestCase("true", false)] + [TestCase("TRUE", true)] + [TestCase("True", true)] + [TestCase("true", true)] + [TestCase("1", true)] + [TestCase(1, true)] + [TestCase(true, true)] + [TestCase("FALSE", false)] + [TestCase("False", false)] [TestCase("false", false)] + [TestCase("0", false)] + [TestCase(0, false)] + [TestCase(false, false)] + [TestCase("", false)] + [TestCase(null, false)] [TestCase("blah", false)] - public void CanConvertYesNoPropertyEditor(string value, bool expected) - { - var converter = new YesNoValueConverter(); + public void CanConvertYesNoPropertyEditor(object value, bool expected) + { + var converter = new YesNoValueConverter(); var result = converter.ConvertDataToSource(null, value, false); // does not use type for conversion - Assert.AreEqual(expected, result); - } + Assert.AreEqual(expected, result); + } } } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 75a247f1c8..9e7535b0f3 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -39,6 +39,82 @@ namespace Umbraco.Tests.Services //TODO Add test to verify there is only ONE newest document/content in cmsDocument table after updating. //TODO Add test to delete specific version (with and without deleting prior versions) and versions by date. + [Test] + public void Count_All() + { + // Arrange + var contentService = ServiceContext.ContentService; + + // Act + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); + } + + // Assert + Assert.AreEqual(24, contentService.Count()); + } + + [Test] + public void Count_By_Content_Type() + { + // Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); + contentTypeService.Save(contentType); + + // Act + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, "umbBlah", 0); + } + + // Assert + Assert.AreEqual(20, contentService.Count(contentTypeAlias: "umbBlah")); + } + + [Test] + public void Count_Children() + { + // Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); + contentTypeService.Save(contentType); + var parent = contentService.CreateContentWithIdentity("Test", -1, "umbBlah", 0); + + // Act + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", parent, "umbBlah"); + } + + // Assert + Assert.AreEqual(20, contentService.CountChildren(parent.Id)); + } + + [Test] + public void Count_Descendants() + { + // Arrange + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbBlah", "test Doc Type"); + contentTypeService.Save(contentType); + var parent = contentService.CreateContentWithIdentity("Test", -1, "umbBlah", 0); + + // Act + IContent current = parent; + for (int i = 0; i < 20; i++) + { + current = contentService.CreateContentWithIdentity("Test", current, "umbBlah"); + } + + // Assert + Assert.AreEqual(20, contentService.CountDescendants(parent.Id)); + } + [Test] public void Tags_For_Entity_Are_Not_Exposed_Via_Tag_Api_When_Content_Is_Recycled() { diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index 9609110308..1ff9947a6f 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; @@ -376,6 +377,34 @@ namespace Umbraco.Tests.Services Assert.IsNull(ServiceContext.MemberService.GetByEmail("do@not.find")); } + [Test] + public void Get_Member_Name() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + IMember member = MockedMember.CreateSimpleMember(memberType, "Test Real Name", "test@test.com", "pass", "testUsername"); + ServiceContext.MemberService.Save(member); + + + Assert.AreEqual("Test Real Name", member.Name); + } + + [Test] + public void Get_Member_Name_In_Created_Event() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + + TypedEventHandler> callback = (sender, args) => + { + Assert.AreEqual("Test Real Name", args.Entity.Name); + }; + + MemberService.Created += callback; + var member = ServiceContext.MemberService.CreateMember("testUsername", "test@test.com", "Test Real Name", memberType); + MemberService.Created -= callback; + } + [Test] public void Get_By_Username() { diff --git a/src/Umbraco.Tests/Services/TagServiceTests.cs b/src/Umbraco.Tests/Services/TagServiceTests.cs new file mode 100644 index 0000000000..52e02d26da --- /dev/null +++ b/src/Umbraco.Tests/Services/TagServiceTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Services +{ + /// + /// Tests covering methods in the TagService class. + /// This is more of an integration test as it involves multiple layers + /// as well as configuration. + /// + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture, RequiresSTA] + public class TagServiceTests : BaseServiceTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + [Test] + public void TagList_Contains_NodeCount() + { + var contentService = ServiceContext.ContentService; + var contentTypeService = ServiceContext.ContentTypeService; + var tagService = ServiceContext.TagService; + var contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", DataTypeDatabaseType.Ntext) + { + Alias = "tags", + DataTypeDefinitionId = 1041 + }); + contentTypeService.Save(contentType); + + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetTags("tags", new[] { "cow", "pig", "goat" }, true); + contentService.Publish(content1); + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.SetTags("tags", new[] { "cow", "pig" }, true); + contentService.Publish(content2); + + var content3 = MockedContent.CreateSimpleContent(contentType, "Tagged content 3", -1); + content3.SetTags("tags", new[] { "cow" }, true); + contentService.Publish(content3); + + // Act + var tags = tagService.GetAllContentTags() + .OrderByDescending(x => x.NodeCount) + .ToList(); + + // Assert + Assert.AreEqual(3, tags.Count()); + Assert.AreEqual("cow", tags[0].Text); + Assert.AreEqual(3, tags[0].NodeCount); + Assert.AreEqual("pig", tags[1].Text); + Assert.AreEqual(2, tags[1].NodeCount); + Assert.AreEqual("goat", tags[2].Text); + Assert.AreEqual(1, tags[2].NodeCount); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs index c6ee114bb9..31bdb616b0 100644 --- a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs +++ b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Security; @@ -96,7 +97,9 @@ namespace Umbraco.Tests.TestHelpers var httpContextMock = new Mock(); httpContextMock.Setup(x => x.Cache).Returns(HttpRuntime.Cache); - httpContextMock.Setup(x => x.Items).Returns(new Dictionary()); + //note: foreach on Items should return DictionaryEntries! + //httpContextMock.Setup(x => x.Items).Returns(new Dictionary()); + httpContextMock.Setup(x => x.Items).Returns(new Hashtable()); httpContextMock.Setup(x => x.Request).Returns(requestMock.Object); httpContextMock.Setup(x => x.Server).Returns(serverMock.Object); httpContextMock.Setup(x => x.Response).Returns(responseMock.Object); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f2c7f45449..4caf247a2e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -86,7 +86,7 @@ False - ..\packages\Newtonsoft.Json.6.0.2\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll False @@ -311,6 +311,7 @@ + @@ -698,6 +699,9 @@ + + + xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\amd64\*.* "$(TargetDir)amd64\" /Y /F /E /D diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index 041197ba63..bfd68d9902 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -17,7 +17,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/autoscale.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/autoscale.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/detectfold.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/detectfold.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/detectfold.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/detectfold.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/fixnumber.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/fixnumber.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/fixnumber.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/fixnumber.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/hotkey.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/hotkey.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/hotkey.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/hotkey.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/localize.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/localize.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/localize.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/preventdefault.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/preventdefault.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/preventdefault.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/preventdefault.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/resizetocontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/resizetocontent.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/resizetocontent.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/resizetocontent.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/utill/selectonfocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/selectonfocus.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/utill/selectonfocus.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/util/selectonfocus.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js new file mode 100644 index 0000000000..74c007dfbc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js @@ -0,0 +1,18 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:noDirtyCheck +* @restrict A +* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) +**/ +function noDirtyCheck() { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + elm.focus(function () { + ctrl.$pristine = false; + }); + } + }; +} +angular.module('umbraco.directives').directive("noDirtyCheck", noDirtyCheck); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 2e5aa52e80..d20feb0379 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -12,7 +12,7 @@ * Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will * be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. **/ -function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService) { +function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { return { require: "form", restrict: "A", @@ -37,6 +37,12 @@ function valFormManager(serverValidationManager, $rootScope, $log, $timeout, not var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; + //This tracks if the user is currently saving a new item, we use this to determine + // if we should display the warning dialog that they are leaving the page - if a new item + // is being saved we never want to display that dialog, this will also cause problems when there + // are server side validation issues. + var isSavingNewItem = false; + //we should show validation if there are any msgs in the server validation collection if (serverValidationManager.items.length > 0) { element.addClass(className); @@ -45,6 +51,9 @@ function valFormManager(serverValidationManager, $rootScope, $log, $timeout, not //listen for the forms saving event scope.$on(savingEventName, function (ev, args) { element.addClass(className); + + //set the flag so we can check to see if we should display the error. + isSavingNewItem = $routeParams.create; }); //listen for the forms saved event @@ -60,7 +69,7 @@ function valFormManager(serverValidationManager, $rootScope, $log, $timeout, not //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but // the form has pending changes var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { - if (!formCtrl.$dirty) { + if (!formCtrl.$dirty || isSavingNewItem) { return; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index d96a60cd87..9b50b47882 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -23,7 +23,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro $http.get( umbRequestHelper.getApiUrl( "rteApiBaseUrl", - "GetConfiguration")), + "GetConfiguration"), { cache: true }), 'Failed to retrieve tinymce configuration'); }, @@ -94,7 +94,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro if(selectedElm.nodeName === 'IMG'){ var img = $(selectedElm); currentTarget = { - name: img.attr("alt"), + altText: img.attr("alt"), url: img.attr("src"), id: img.attr("rel") }; @@ -109,7 +109,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro if (img) { var data = { - alt: img.name, + alt: img.altText, src: (img.url) ? img.url : "nothing.jpg", rel: img.id, id: '__mcenew' diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 4b9fe44d0b..809f86d807 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -98,7 +98,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //this gets the image with the smallest height which equals the maximum we can scale up for this image block var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); + targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); @@ -109,7 +109,8 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (attemptedRowHeight < minDisplayHeight) { if (idealImages.length > 1) { - //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight + + //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); } @@ -135,7 +136,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me } else if (idealImages.length === 1) { //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally - // in the row so we'll just return the minDisplayHeight and it will just get centered on the row + // in the row. return { height: minDisplayHeight, imgCount: 1 }; } else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { @@ -156,7 +157,19 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); } + else if (targetHeight === maxRowHeight) { + + //This is going to happen when: + // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) + // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their + // maximum height (maxRowHeight) + // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough + // which is better than rendering a row that is shorter than the minimum since that could be quite small. + + return { height: targetHeight, imgCount: idealImages.length }; + } else { + //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); } @@ -178,9 +191,14 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me return newHeight; } - - //if it's not successful, return false - return null; + else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { + //if there is only one image, then return the target height + return targetHeight; + } + else { + //if it's not successful, return false + return null; + } }, /** builds an image grid row */ @@ -192,31 +210,29 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); var sizes = []; - for (var i = 0; i < imgs.length; i++) { + //loop through the images we know fit into the height + for (var i = 0; i < imageRowHeight.imgCount; i++) { //get the lower width to ensure it always fits var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - - //in this case, a single image will not fit into the row so we need to crop/center - // width the full width and the min display height - if (imageRowHeight.imgCount === 1) { - sizes.push({ - width: targetWidth, - //ensure that the height is rounded - height: Math.round(minDisplayHeight) - }); - row.images.push(imgs[i]); - break; - } if (currRowWidth + scaledWidth <= targetWidth) { currRowWidth += scaledWidth; sizes.push({ - width: scaledWidth, + width:scaledWidth, //ensure that the height is rounded height: Math.round(imageRowHeight.height) }); row.images.push(imgs[i]); - } + } + else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { + //the image is simply too wide, we'll crop/center it + sizes.push({ + width: maxRowWidth, + //ensure that the height is rounded + height: Math.round(imageRowHeight.height) + }); + row.images.push(imgs[i]); + } else { //the max width has been reached break; @@ -233,8 +249,11 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); } - ////set the row style - //row.style = { "width": maxRowWidth + "px" }; + if (row.images.length === 1) { + //if there's only one image on the row, set the container to max width + row.images[0].style.width = maxRowWidth + "px"; + } + return row; }, @@ -281,6 +300,11 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me imagesProcessed += row.images.length; } else { + + if (currImgs.length > 0) { + throw "Could not fill grid with all images, images remaining: " + currImgs.length; + } + //if there was nothing processed, exit break; } diff --git a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js index f90ecadd37..db3cc3e0bc 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js @@ -89,7 +89,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ //watch the value change but don't do the search on every change - that's far too many queries // we need to debounce - $scope.$watch("searchTerm", _.debounce(function () { + var debounced = _.debounce(function () { if ($scope.searchTerm) { $scope.isSearching = true; navigationService.showSearch(); @@ -102,7 +102,10 @@ function SearchController($scope, searchService, $log, $location, navigationServ navigationService.hideSearch(); $scope.selectedItem = undefined; } - }, 100)); + }, 300); + + + $scope.$watch("searchTerm", debounced); } //register it diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index e78754ff22..0e9ebc7526 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -104,6 +104,8 @@
+ + Validating your database connection... diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index ea0c1607db..5ecefa57e0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -17,7 +17,7 @@ label small, .guiDialogTiny { } label.control-label { - padding-top: 8px !important; + padding: 8px 10px 0 0 !important; } .umb-status-label{ @@ -25,7 +25,7 @@ label.control-label { } -.controls-row label{padding: 0px 10px 0px 10px; vertical-align: center} +.controls-row label{padding: 0 10px 0 10px; vertical-align: center} diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 2d76403c2e..f6101e109e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -75,6 +75,7 @@ padding: 15px 20px 10px 20px; margin-top: 30px; clear: both; + background: #fff; } .umb-dialog .umb-btn-toolbar .umb-control-group{ border: none; @@ -185,3 +186,4 @@ .umb-modal .breadcrumb input { height: 12px } + diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index ec9bf8f887..aa8eb57815 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -33,8 +33,12 @@ bottom: 90px; } -.umb-panel.editor-breadcrumb .umb-panel-body, .umb-panel.editor-breadcrumb .umb-bottom-bar,{ - bottom: 40px !Important; +.umb-panel.editor-breadcrumb .umb-panel-body, .umb-panel.editor-breadcrumb .umb-bottom-bar { + bottom: 30px !Important; +} + +.umb-tab-buttons.umb-bottom-bar { + bottom: 50px !Important; } .umb-panel-header .umb-headline, .umb-panel-header h1 { @@ -160,6 +164,7 @@ bottom: 0px; left: 100px; right: 20px; + z-index: 6010; }; @media (min-width: 1101px) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html index 4492b24d2c..d3f9a5f724 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html @@ -19,14 +19,10 @@ data-file-upload="options" data-file-upload-progress="" data-ng-class="{'fileupl ng-disabled="target.id"/> - - + - + ng-model="target.altText" />
@@ -98,8 +94,9 @@ data-file-upload="options" data-file-upload-progress="" data-ng-class="{'fileupl -

- There are no items show in the list. -

- - +
- + + + + + + + + + - + - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js index 98cd8218ef..c18b3c5610 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js @@ -37,26 +37,26 @@ angular.module('umbraco') function openDialog(index){ var dialogData = {}; - if(index){ + if(index !== null && $scope.renderModel[index]) { var macro = $scope.renderModel[index]; dialogData = {macroData: macro}; } dialogService.macroPicker({ - dialogData : dialogData, - callback: function(data) { + dialogData : dialogData, + callback: function(data) { - collectDetails(data); + collectDetails(data); - //update the raw syntax and the list... - if(index){ - $scope.renderModel[index] = data; - }else{ - $scope.renderModel.push(data); - } - } - }); - } + //update the raw syntax and the list... + if(index !== null && $scope.renderModel[index]) { + $scope.renderModel[index] = data; + } else { + $scope.renderModel.push(data); + } + } + }); + } diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js index b41ce4692e..d3d063521b 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/umb-photo-folder-helper.spec.js @@ -72,6 +72,26 @@ describe('umbPhotoFolderHelper tests', function () { expect(result.images.length).toBe(1); }); + + //SEE: http://issues.umbraco.org/issue/U4-5304 + it('When a row fits with width but its too short, we remove one and scale up, but that comes up too narrow, so we just render what we have', function () { + + var images = [ +{ "properties": [{ "value": "/test35.jpg", "alias": "umbracoFile" }, { "value": "1000", "alias": "umbracoWidth" }, { "value": "1041", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 1000, "originalHeight": 1041 }, +{ "properties": [{ "value": "/test36.jpg", "alias": "umbracoFile" }, { "value": "1000", "alias": "umbracoWidth" }, { "value": "2013", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 1000, "originalHeight": 2013 }, +{ "properties": [{ "value": "/test37.jpg", "alias": "umbracoFile" }, { "value": "840", "alias": "umbracoWidth" }, { "value": "360", "alias": "umbracoHeight" }], "contentTypeAlias": "Image", "name": "Test.jpg", "thumbnail": "/umbraco/UmbracoApi/Images/GetBigThumbnail?mediaId=1349", "originalWidth": 840, "originalHeight": 360 } + ]; + var maxRowHeight = 250; + var minDisplayHeight = 105; + var maxRowWidth = 400; + var idealImgPerRow = 3; + var margin = 5; + + var result = umbPhotoFolderHelper.buildRow(images, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); + + expect(result.images.length).toBe(2); + + }); }); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index dc20541fc8..935bb1db73 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -130,11 +130,12 @@ False - ..\packages\ImageProcessor.1.8.8.0\lib\ImageProcessor.dll + ..\packages\ImageProcessor.1.9.5.0\lib\ImageProcessor.dll False - ..\packages\ImageProcessor.Web.3.2.2.0\lib\net45\ImageProcessor.Web.dll + ..\packages\ImageProcessor.Web.3.3.0.0\lib\net45\ImageProcessor.Web.dll + True False @@ -166,8 +167,9 @@ False ..\packages\MySql.Data.6.6.5\lib\net40\MySql.Data.dll - - ..\packages\Newtonsoft.Json.6.0.2\lib\net45\Newtonsoft.Json.dll + + False + ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll System diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 94a5747a14..543b58d3c0 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -4,8 +4,8 @@ - - + + @@ -22,7 +22,7 @@ - + diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index 025f8433e5..6f467eb3b9 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -19,6 +19,7 @@ + Umbraco diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 945bb24abf..b957efd2bf 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -150,6 +150,7 @@ To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting Statistics Title (optional) + Alternative text (optional) Type Unpublish Last edited @@ -175,7 +176,7 @@ Browse your website - Hide - If umbraco isn't opening, you might need to allow popups from this site + If Umbraco isn't opening, you might need to allow popups from this site has opened in a new window Restart Visit @@ -203,7 +204,7 @@ Last Edited Link Internal link: - When using local links, insert "#" infront of link + When using local links, insert "#" in front of link Open in new window? Macro Settings This macro does not contain any properties you can edit @@ -437,12 +438,12 @@ proceed. ]]> next to continue the configuration wizard]]> The Default users’ password needs to be changed!]]> - The Default user has been disabled or has no access to umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> The password is changed! - umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is + Umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is changed to something unique.

@@ -450,29 +451,29 @@

]]>
Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this umbraco distribution consists of two different licenses, the open source MIT license for the framework and the umbraco freeware license that covers the UI. + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. Not installed yet. Affected files and folders - More information on setting up permissions for umbraco here + More information on setting up permissions for Umbraco here You need to grant ASP.NET modify permissions to the following files/folders Your permission settings are almost perfect!

- You can run umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of umbraco.]]>
+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> How to Resolve Click here to read the text version - video tutorial on setting up folder permissions for umbraco or read the text version.]]> + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> Your permission settings might be an issue!

- You can run umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of umbraco.]]>
- Your permission settings are not ready for umbraco! + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> + Your permission settings are not ready for Umbraco!

- In order to run umbraco, you'll need to update your permission settings.]]>
+ In order to run Umbraco, you'll need to update your permission settings.]]> Your permission settings are perfect!

- You are ready to run umbraco and install packages!]]>
+ You are ready to run Umbraco and install packages!]]> Resolving folder issue Follow this link for more information on problems with ASP.NET and creating folders Setting up folder permissions I want to start from scratch @@ -505,25 +506,25 @@ Step 1/5 Accept license Step 2/5: Database configuration Step 3/5: Validating File Permissions - Step 4/5: Check umbraco security + Step 4/5: Check Umbraco security Step 5/5: Umbraco is ready to get you started - Thank you for choosing umbraco + Thank you for choosing Umbraco Browse your new site You installed Runway, so why not see how your new website looks.]]> Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the umbraco terminology]]> +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> Umbraco %0% is installed and ready for use /web.config file and update the AppSetting key umbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to umbraco, + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, you can find plenty of resources on our getting started pages.]]>
Launch Umbraco -To manage your website, simply open the umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]>
Connection to database failed. Umbraco Version 3 Umbraco Version 4 Watch - umbraco %0% for a fresh install or upgrading from version 3.0. + Umbraco %0% for a fresh install or upgrading from version 3.0.

Press "next" to start the wizard.]]>
@@ -545,7 +546,7 @@ To manage your website, simply open the umbraco back office and start adding con Happy Caturday Log in below Session timed out - © 2001 - %0%
umbraco.org

]]>
+ © 2001 - %0%
Umbraco.com

]]>
Dashboard @@ -579,7 +580,7 @@ To manage your website, simply open the umbraco back office and start adding con Have a nice day! - Cheers from the umbraco robot + Cheers from the Umbraco robot ]]>
Hi %0%

@@ -606,7 +607,7 @@ To manage your website, simply open the umbraco back office and start adding con

Have a nice day!

- Cheers from the umbraco robot + Cheers from the Umbraco robot

]]>
[%0%] Notification about %1% performed on %2% Notifications @@ -614,7 +615,7 @@ To manage your website, simply open the umbraco back office and start adding con - button and locating the package. umbraco packages usually have a ".umb" or ".zip" extension. + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. ]]> Author Demonstration @@ -638,20 +639,20 @@ To manage your website, simply open the umbraco back office and start adding con Download update from the repository Upgrade package Upgrade instructions - There's an upgrade available for this package. You can download it directly from the umbraco package repository. + There's an upgrade available for this package. You can download it directly from the Umbraco package repository. Package version Package version history View package website Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. Paste as raw text without any formatting at all Paste, but remove formatting (Recommended) Role based protection - using umbraco's member groups.]]> + using Umbraco's member groups.]]> role-based authentication.]]> Error Page Used when people are logged on, but do not have access @@ -659,7 +660,7 @@ To manage your website, simply open the umbraco back office and start adding con %0% is now protected Protection removed from %0% Login Page - Choose the page that has the login formular + Choose the page that contains the login form Remove Protection Select the pages that contain login form and error messages Pick the roles who have access to this page @@ -668,16 +669,14 @@ To manage your website, simply open the umbraco back office and start adding con If you just want to setup simple protection using a single login and password - - - + ]]> %0% and subpages have been published Publish %0% and all its subpages ok to publish %0% and thereby making its content publicly available.

- You can publish this page and all its sub-pages by checking publish all children below. + You can publish this page and all it's sub-pages by checking publish all children below. ]]>
@@ -711,7 +710,7 @@ To manage your website, simply open the umbraco back office and start adding con Current version Red text will not be shown in the selected version. , green means added]]> Document has been rolled back - This displays the selected version as html, if you wish to see the difference between 2 versions at the same time, use the diff view + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view Rollback to Select version View @@ -733,6 +732,7 @@ To manage your website, simply open the umbraco back office and start adding con Translation Users Help + Analytics Default template @@ -826,9 +826,9 @@ To manage your website, simply open the umbraco back office and start adding con Insert content area placeholder Insert dictionary item Insert Macro - Insert umbraco page field + Insert Umbraco page field Master template - Quick Guide to umbraco template tags + Quick Guide to Umbraco template tags Template @@ -869,9 +869,9 @@ To manage your website, simply open the umbraco back office and start adding con ]]> close task Translation details - Download all translation tasks as xml - Download xml - Download xml DTD + Download all translation tasks as XML + Download XML + Download XML DTD Fields Include subpages [%0%] Translation task for %1% No translator users found. Please create a translator user before you start sending content to translation @@ -904,10 +904,10 @@ To manage your website, simply open the umbraco back office and start adding con Translate to Translation completed. You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the xml file might be corrupt + Translation failed, the XML file might be corrupt Translation options Translator - Upload translation xml + Upload translation XML Cache Browser @@ -938,6 +938,7 @@ To manage your website, simply open the umbraco back office and start adding con Stylesheets Templates XSLT Files + Analytics New update ready @@ -987,4 +988,4 @@ To manage your website, simply open the umbraco back office and start adding con Your recent history Session expires in - + diff --git a/src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.cs b/src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.cs index 056f17b157..ab017f3653 100644 --- a/src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.cs +++ b/src/Umbraco.Web.UI/umbraco/create/PartialViewMacro.ascx.cs @@ -9,17 +9,17 @@ using umbraco.presentation.create; namespace Umbraco.Web.UI.Umbraco.Create { - public partial class PartialViewMacro : UserControl - { - + public partial class PartialViewMacro : UserControl + { - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - DataBind(); - LoadTemplates(PartialViewTemplate); - } + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + DataBind(); + + LoadTemplates(PartialViewTemplate); + } private static void LoadTemplates(ListControl list) { @@ -45,44 +45,47 @@ namespace Umbraco.Web.UI.Umbraco.Create } } - protected void SubmitButton_Click(object sender, EventArgs e) - { - if (Page.IsValid) - { - //Seriously, need to overhaul create dialogs, this is rediculous: - // http://issues.umbraco.org/issue/U4-1373 + protected void SubmitButton_Click(object sender, EventArgs e) + { + if (Page.IsValid) + { + //Seriously, need to overhaul create dialogs, this is rediculous: + // http://issues.umbraco.org/issue/U4-1373 + + var createMacroVal = 0; + if (CreateMacroCheckBox.Checked) + createMacroVal = 1; - var createMacroVal = 0; - if (CreateMacroCheckBox.Checked) - createMacroVal = 1; - string returnUrl = dialogHandler_temp.Create(Request.GetItemAsString("nodeType"), createMacroVal, //apparently we need to pass this value to 'ParentID'... of course! :P then we'll extract it in PartialViewTasks to create it. PartialViewTemplate.SelectedValue + "|||" + FileName.Text); - - BasePage.Current.ClientTools - .ChangeContentFrameUrl(returnUrl) - .ChildNodeCreated() - .CloseModalWindow(); - } - } - protected void MacroExistsValidator_OnServerValidate(object source, ServerValidateEventArgs args) - { - if (CreateMacroCheckBox.Checked) - { + BasePage.Current.ClientTools + .ChangeContentFrameUrl(returnUrl) + .ChildNodeCreated() + .CloseModalWindow(); + } + } + + protected void MacroExistsValidator_OnServerValidate(object source, ServerValidateEventArgs args) + { + if (CreateMacroCheckBox.Checked) + { //TODO: Shouldn't this use our string functions to create the alias ? - var fileName = FileName.Text + ".cshtml"; - var name = fileName - .Substring(0, (fileName.LastIndexOf('.') + 1)).Trim('.') - .SplitPascalCasing().ToFirstUpperInvariant(); + var fileName = FileName.Text; + + var name = fileName.Contains(".") + ? fileName.Substring(0, (fileName.LastIndexOf('.') + 1)).Trim('.') + : fileName; + + name = name.SplitPascalCasing().ToFirstUpperInvariant(); var macro = ApplicationContext.Current.Services.MacroService.GetByAlias(name); if (macro != null) { args.IsValid = false; - } - } - } - } + } + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/sort.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/sort.aspx index b6d2014fea..7a6b7f6c7c 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/sort.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/sort.aspx @@ -24,7 +24,9 @@

<%= umbraco.ui.Text("sort", "sortPleaseWait") %>


- +
+ +
@@ -62,26 +58,38 @@
- +
+

There are no items show in the list.

+
- - {{result.name}}{{result.updateDate|date:'medium'}} + {{result.name}} + + {{result.updateDate|date:'medium'}} {{result.owner.name}} + + {{result.owner.name}}