diff --git a/.hgtags b/.hgtags index 2c22ddad48..8a944ec680 100644 --- a/.hgtags +++ b/.hgtags @@ -10,8 +10,5 @@ d03fcffb8834a9583a56813bb44b6abbd9f042cc Release-4.6.0 096f20bb0575d6199f20fcb3147b63554e765a74 Release-4.8.0 8f8a203857886b373148af29edd57460a42940be Release-4.8.1 de73e687ddf6086ed52b6554676c1632865d07f2 Release-4.9.0 -de73e687ddf6086ed52b6554676c1632865d07f2 Release-4.9.0 -0000000000000000000000000000000000000000 Release-4.9.0 -0000000000000000000000000000000000000000 Release-4.9.0 -09f5a69cf19ebf7ddd9501e5258bee1e6a125391 Release-4.9.0 +8d7d8609e2e4b971da99cd97f72132ce85ce3333 Release-4.9.1 f6da531fbb4c251ff61d314e2a7effb13c71e74a Release-4.10.0 diff --git a/src/Umbraco.Core/CustomBooleanTypeConverter.cs b/src/Umbraco.Core/CustomBooleanTypeConverter.cs index b7b30d1fa1..76c8f75076 100644 --- a/src/Umbraco.Core/CustomBooleanTypeConverter.cs +++ b/src/Umbraco.Core/CustomBooleanTypeConverter.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core { var str = (string)value; if (str == "1") return true; - if (str == "0") return false; + if (str == "0" || str == "") return false; } return base.ConvertFrom(context, culture, value); diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs new file mode 100644 index 0000000000..78f5eb6839 --- /dev/null +++ b/src/Umbraco.Core/HashCodeCombiner.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace Umbraco.Core +{ + /// + /// Used to create a hash code from multiple objects. + /// + /// + /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things + /// and is probably more stable in general, however we just need a quick easy class for this in order to create a unique + /// hash of plugins to see if they've changed. + /// + internal class HashCodeCombiner + { + private int _combinedHash; + + internal void AddInt(int i) + { + _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + } + + internal void AddObject(object o) + { + AddInt(o.GetHashCode()); + } + + internal void AddDateTime(DateTime d) + { + AddInt(d.GetHashCode()); + } + + internal void AddCaseInsensitiveString(string s) + { + if (s != null) + AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); + } + + internal void AddFile(FileInfo f) + { + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); + AddInt(f.Length.GetHashCode()); + } + + internal void AddFolder(DirectoryInfo d) + { + AddCaseInsensitiveString(d.FullName); + AddDateTime(d.CreationTimeUtc); + AddDateTime(d.LastWriteTimeUtc); + foreach (var f in d.GetFiles()) + { + AddFile(f); + } + foreach (var s in d.GetDirectories()) + { + AddFolder(s); + } + } + + /// + /// Returns the hex code of the combined hash code + /// + /// + internal string GetCombinedHashCode() + { + return _combinedHash.ToString("x", CultureInfo.InvariantCulture); + } + + } +} diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index e7f6b29c60..6828b0e353 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core.IO //all paths has a starting but no trailing / public class SystemDirectories { + //TODO: Why on earth is this even configurable? You cannot change the /Bin folder in ASP.Net public static string Bin { get diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index 0d86f41725..770363336b 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -1,10 +1,18 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using System.Web.Caching; +using System.Web.Compilation; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -22,16 +30,67 @@ namespace Umbraco.Core /// /// This class can expose extension methods to resolve custom plugins /// + /// Before this class resolves any plugins it checks if the hash has changed for the DLLs in the /bin folder, if it hasn't + /// it will use the cached resolved plugins that it has already found which means that no assembly scanning is necessary. This leads + /// to much faster startup times. /// internal class PluginManager { + private readonly ApplicationContext _appContext; - internal PluginManager() + /// + /// Creates a new PluginManager with an ApplicationContext instance which ensures that the plugin xml + /// file is cached temporarily until app startup completes. + /// + /// + /// + internal PluginManager(ApplicationContext appContext, bool detectBinChanges = true) + : this(detectBinChanges) { + if (appContext == null) throw new ArgumentNullException("appContext"); + _appContext = appContext; + } + + /// + /// Creates a new PluginManager + /// + /// + /// If true will detect changes in the /bin folder and therefor load plugins from the + /// cached plugins file if one is found. If false will never use the cache file for plugins + /// + internal PluginManager(bool detectBinChanges = true) + { + _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); + //create the folder if it doesn't exist + if (!Directory.Exists(_tempFolder)) + { + Directory.CreateDirectory(_tempFolder); + } + + if (detectBinChanges) + { + //first check if the cached hash is 0, if it is then we ne + //do the check if they've changed + HaveAssembliesChanged = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; + //if they have changed, we need to write the new file + if (HaveAssembliesChanged) + { + WriteCachePluginsHash(); + } + } + else + { + //always set to true if we're not detecting (generally only for testing) + HaveAssembliesChanged = true; + } + } static PluginManager _resolver; static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); + private readonly string _tempFolder; + private long _cachedAssembliesHash = -1; + private long _currentAssembliesHash = -1; /// /// We will ensure that no matter what, only one of these is created, this is to ensure that caching always takes place @@ -48,7 +107,9 @@ namespace Umbraco.Core if (_resolver == null) { l.UpgradeToWriteLock(); - _resolver = new PluginManager(); + _resolver = ApplicationContext.Current == null + ? new PluginManager() + : new PluginManager(ApplicationContext.Current); } return _resolver; } @@ -56,6 +117,215 @@ namespace Umbraco.Core set { _resolver = value; } } + #region Hash checking methods + + /// + /// Returns a bool if the assemblies in the /bin have changed since they were last hashed. + /// + internal bool HaveAssembliesChanged { get; private set; } + + /// + /// Returns the currently cached hash value of the scanned assemblies in the /bin folder. Returns 0 + /// if no cache is found. + /// + /// + internal long CachedAssembliesHash + { + get + { + if (_cachedAssembliesHash != -1) + return _cachedAssembliesHash; + + var filePath = Path.Combine(_tempFolder, "umbraco-plugins.hash"); + if (!File.Exists(filePath)) + return 0; + var hash = File.ReadAllText(filePath, Encoding.UTF8); + Int64 val; + if (Int64.TryParse(hash, out val)) + { + _cachedAssembliesHash = val; + return _cachedAssembliesHash; + } + //it could not parse for some reason so we'll return 0. + return 0; + } + } + + /// + /// Returns the current assemblies hash based on creating a hash from the assemblies in the /bin + /// + /// + internal long CurrentAssembliesHash + { + get + { + if (_currentAssembliesHash != -1) + return _currentAssembliesHash; + + _currentAssembliesHash = GetAssembliesHash(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)).GetFiles("*.dll")); + return _currentAssembliesHash; + } + } + + /// + /// Writes the assembly hash file + /// + private void WriteCachePluginsHash() + { + var filePath = Path.Combine(_tempFolder, "umbraco-plugins.hash"); + File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8); + } + + /// + /// Returns a unique hash for the combination of FileInfo objects passed in + /// + /// + /// + internal static long GetAssembliesHash(IEnumerable plugins) + { + using (DisposableTimer.TraceDuration("Determining hash of plugins on disk", "Hash determined")) + { + var hashCombiner = new HashCodeCombiner(); + //add each unique folder to the hash + foreach (var i in plugins.Select(x => x.Directory).DistinctBy(x => x.FullName)) + { + hashCombiner.AddFolder(i); + } + return ConvertPluginsHashFromHex(hashCombiner.GetCombinedHashCode()); + } + } + + /// + /// Converts the hash value of current plugins to long from string + /// + /// + /// + internal static long ConvertPluginsHashFromHex(string val) + { + long outVal; + if (Int64.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal)) + { + return outVal; + } + return 0; + } + + /// + /// Attempts to resolve the list of plugin + assemblies found in the runtime for the base type 'T' passed in. + /// If the cache file doesn't exist, fails to load, is corrupt or the type 'T' element is not found then + /// a false attempt is returned. + /// + /// + /// + internal Attempt> TryGetCachedPluginsFromFile() + { + var filePath = Path.Combine(_tempFolder, "umbraco-plugins.list"); + if (!File.Exists(filePath)) + return Attempt>.False; + + try + { + //we will load the xml document, if the app context exist, we will load it from the cache (which is only around for 5 minutes) + //while the app boots up, this should save some IO time on app startup when the app context is there (which is always unless in unit tests) + XDocument xml; + if (_appContext != null) + { + xml = _appContext.ApplicationCache.GetCacheItem("umbraco-plugins.list", + new TimeSpan(0, 0, 5, 0), + () => XDocument.Load(filePath)); + } + else + { + xml = XDocument.Load(filePath); + } + + + if (xml.Root == null) + return Attempt>.False; + + var typeElement = xml.Root.Elements() + .SingleOrDefault(x => + x.Name.LocalName == "baseType" + && ((string) x.Attribute("type")) == typeof (T).FullName); + if (typeElement == null) + return Attempt>.False; + + //return success + return new Attempt>( + true, + typeElement.Elements("add") + .Select(x => (string) x.Attribute("type"))); + } + catch (Exception) + { + //if the file is corrupted, etc... return false + return Attempt>.False; + } + } + + /// + /// Adds/Updates the type list for the base type 'T' in the cached file + /// + /// + /// + /// + /// THIS METHOD IS NOT THREAD SAFE + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + internal void UpdateCachedPluginsFile(IEnumerable typesFound) + { + var filePath = Path.Combine(_tempFolder, "umbraco-plugins.list"); + XDocument xml; + try + { + xml = XDocument.Load(filePath); + } + catch + { + //if there's an exception loading then this is somehow corrupt, we'll just replace it. + File.Delete(filePath); + //create the document and the root + xml = new XDocument(new XElement("plugins")); + } + if (xml.Root == null) + { + //if for some reason there is no root, create it + xml.Add(new XElement("plugins")); + } + //find the type 'T' element to add or update + var typeElement = xml.Root.Elements() + .SingleOrDefault(x => + x.Name.LocalName == "baseType" + && ((string)x.Attribute("type")) == typeof(T).FullName); + if (typeElement == null) + { + //create the type element + typeElement = new XElement("baseType", new XAttribute("type", typeof(T).FullName)); + //then add it to the root + xml.Root.Add(typeElement); + } + + + //now we have the type element, we need to clear any previous types as children and add/update it with new ones + typeElement.ReplaceNodes(typesFound + .Select(x => + new XElement("add", + new XAttribute("type", x.AssemblyQualifiedName)))); + //save the xml file + xml.Save(filePath); + } + + #endregion + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly HashSet _types = new HashSet(); private IEnumerable _assemblies; @@ -208,7 +478,7 @@ namespace Umbraco.Core using (DisposableTimer.TraceDuration( String.Format("Starting resolution types of {0}", typeof(T).FullName), String.Format("Completed resolution of types of {0}", typeof(T).FullName))) - { + { //check if the TypeList already exists, if so return it, if not we'll create it var typeList = _types.SingleOrDefault(x => x.IsTypeList(resolutionType)); //if we're not caching the result then proceed, or if the type list doesn't exist then proceed @@ -219,9 +489,47 @@ namespace Umbraco.Core typeList = new TypeList(resolutionType); - foreach (var t in finder()) + //we first need to look into our cache file (this has nothing to do with the 'cacheResult' parameter which caches in memory). + if (!HaveAssembliesChanged) { - typeList.AddType(t); + var fileCacheResult = TryGetCachedPluginsFromFile(); + if (fileCacheResult.Success) + { + var successfullyLoadedFromCache = true; + //we have a previous cache for this so we don't need to scan we just load what has been found in the file + foreach(var t in fileCacheResult.Result) + { + try + { + //we use the build manager to ensure we get all types loaded, this is slightly slower than + //Type.GetType but if the types in the assembly aren't loaded yet then we have problems with that. + var type = BuildManager.GetType(t, true); + typeList.AddType(type); + } + catch (Exception ex) + { + //if there are any exceptions loading types, we have to exist, this should never happen so + //we will need to revert to scanning for types. + successfullyLoadedFromCache = false; + LogHelper.Error("Could not load a cached plugin type: " + t + " now reverting to re-scanning assemblies for the base type: " + typeof (T).FullName, ex); + break; + } + } + if (!successfullyLoadedFromCache ) + { + //we need to manually load by scanning if loading from the file was not successful. + LoadViaScanningAndUpdateCacheFile(typeList, finder); + } + else + { + LogHelper.Debug("Loaded plugin types " + typeof(T).FullName + " from persisted cache"); + } + } + } + else + { + //we don't have a cache for this so proceed to look them up by scanning + LoadViaScanningAndUpdateCacheFile(typeList, finder); } //only add the cache if we are to cache the results @@ -236,6 +544,25 @@ namespace Umbraco.Core } } + /// + /// This method invokes the finder which scans the assemblies for the types and then loads the result into the type finder. + /// Once the results are loaded, we update the cached type xml file + /// + /// + /// + /// + /// THIS METHODS IS NOT THREAD SAFE + /// + private void LoadViaScanningAndUpdateCacheFile(TypeList typeList, Func> finder) + { + //we don't have a cache for this so proceed to look them up by scanning + foreach (var t in finder()) + { + typeList.AddType(t); + } + UpdateCachedPluginsFile(typeList.GetTypes()); + } + /// /// Generic method to find the specified type and cache the result /// diff --git a/src/Umbraco.Core/PropertyEditors/DatePickerPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/DatePickerPropertyEditorValueConverter.cs index 9c1f72e473..5d437f5d91 100644 --- a/src/Umbraco.Core/PropertyEditors/DatePickerPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/DatePickerPropertyEditorValueConverter.cs @@ -21,16 +21,7 @@ namespace Umbraco.Core.PropertyEditors /// public Attempt ConvertPropertyValue(object value) { - if (value == null) return new Attempt(false, null); - //if its already a DateTime - if (TypeHelper.IsTypeAssignableFrom(value)) - return new Attempt(true, value); - //convert to string - var asString = Convert.ToString(value); - DateTime dt; - return DateTime.TryParse(asString, out dt) - ? new Attempt(true, dt) - : new Attempt(false, null); + return value.TryConvertTo(typeof(DateTime)); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/YesNoPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/YesNoPropertyEditorValueConverter.cs index b44b00c882..3cac051c94 100644 --- a/src/Umbraco.Core/PropertyEditors/YesNoPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/YesNoPropertyEditorValueConverter.cs @@ -16,18 +16,7 @@ namespace Umbraco.Core.PropertyEditors /// public Attempt ConvertPropertyValue(object value) { - if (value == null) return new Attempt(false, null); - //if its already a bool - if (TypeHelper.IsTypeAssignableFrom(value)) - return new Attempt(true, value); - //convert to string - var asString = Convert.ToString(value); - bool asBool; - return bool.TryParse(asString, out asBool) - ? new Attempt(true, asBool) - : bool.TryParse(asString.Replace("1", "true").Replace("0", "false"), out asBool) //convert 0 or 1 to true or false - ? new Attempt(true, asBool) - : new Attempt(false, null); + return value.TryConvertTo(typeof(bool)); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index 489ddd944b..bda88f115e 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.Data; -using System.Linq; +using System.Net.Mime; using System.Web; -using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using umbraco.interfaces; @@ -14,6 +13,7 @@ namespace Umbraco.Core /// public static class PublishedContentExtensions { + #region GetProperty @@ -41,24 +41,33 @@ namespace Umbraco.Core #endregion #region GetPropertyValue - public static string GetPropertyValue(this IPublishedContent doc, string alias) + public static object GetPropertyValue(this IPublishedContent doc, string alias) { return doc.GetPropertyValue(alias, false); } - public static string GetPropertyValue(this IPublishedContent doc, string alias, string fallback) + public static object GetPropertyValue(this IPublishedContent doc, string alias, string fallback) { var prop = doc.GetPropertyValue(alias); - return !prop.IsNullOrWhiteSpace() ? prop : fallback; + return (prop != null && !Convert.ToString(prop).IsNullOrWhiteSpace()) ? prop : fallback; } - public static string GetPropertyValue(this IPublishedContent doc, string alias, bool recursive) + public static object GetPropertyValue(this IPublishedContent doc, string alias, bool recursive) { var p = doc.GetProperty(alias, recursive); - return p == null ? null : Convert.ToString(p.Value); + if (p == null) return null; + + //Here we need to put the value through the IPropertyEditorValueConverter's + //get the data type id for the current property + var dataType = PublishedContentHelper.GetDataType(doc.DocumentTypeAlias, alias); + //convert the string value to a known type + var converted = PublishedContentHelper.ConvertPropertyValue(p.Value, dataType, doc.DocumentTypeAlias, alias); + return converted.Success + ? converted.Result + : p.Value; } - public static string GetPropertyValue(this IPublishedContent doc, string alias, bool recursive, string fallback) + public static object GetPropertyValue(this IPublishedContent doc, string alias, bool recursive, string fallback) { var prop = doc.GetPropertyValue(alias, recursive); - return !prop.IsNullOrWhiteSpace() ? prop : fallback; + return (prop != null && !Convert.ToString(prop).IsNullOrWhiteSpace()) ? prop : fallback; } #endregion @@ -125,23 +134,51 @@ namespace Umbraco.Core /// then the default value of type T is returned. /// /// - /// + /// /// /// - public static T GetPropertyValue(this IPublishedContent prop, string alias) + public static T GetPropertyValue(this IPublishedContent doc, string alias) { - return prop.GetPropertyValue(alias, default(T)); + return doc.GetPropertyValue(alias, default(T)); + } + + public static T GetPropertyValue(this IPublishedContent prop, string alias, bool recursive, T ifCannotConvert) + { + var p = prop.GetProperty(alias, recursive); + if (p == null) + return ifCannotConvert; + + //before we try to convert it manually, lets see if the PropertyEditorValueConverter does this for us + //Here we need to put the value through the IPropertyEditorValueConverter's + //get the data type id for the current property + var dataType = PublishedContentHelper.GetDataType(prop.DocumentTypeAlias, alias); + //convert the value to a known type + var converted = PublishedContentHelper.ConvertPropertyValue(p.Value, dataType, prop.DocumentTypeAlias, alias); + if (converted.Success) + { + //if its successful, check if its the correct type and return it + if (converted.Result is T) + { + return (T)converted.Result; + } + //if that's not correct, try converting the converted type + var reConverted = converted.Result.TryConvertTo(); + if (reConverted.Success) + { + return reConverted.Result; + } + } + + //last, if all the above has failed, we'll just try converting the raw value straight to 'T' + var manualConverted = p.Value.TryConvertTo(); + if (manualConverted.Success) + return manualConverted.Result; + return ifCannotConvert; } public static T GetPropertyValue(this IPublishedContent prop, string alias, T ifCannotConvert) { - var p = prop.GetProperty(alias); - if (p == null) - return default(T); - var converted = p.Value.TryConvertTo(); - if (converted.Success) - return converted.Result; - return ifCannotConvert; + return prop.GetPropertyValue(alias, false, ifCannotConvert); } } diff --git a/src/Umbraco.Core/PublishedContentHelper.cs b/src/Umbraco.Core/PublishedContentHelper.cs new file mode 100644 index 0000000000..94fc293e4c --- /dev/null +++ b/src/Umbraco.Core/PublishedContentHelper.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dynamics; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core +{ + internal class PublishedContentHelper + { + /// + /// This callback is used only so we can set it dynamically because in the "Core" project currently we don't have + /// access to the business logic layer. + /// TODO: Once 6.0 is released we need to change this to use the new business logic layer that we can access from + /// this proejct. Until then this will return a Guid.Empty but the callback will need to be set in the WebBootManager + /// to work in the website. if people use this in a non-web aspect without the WebBootManager, the the IPropertyEditorValueConverters + /// will not be executed. + /// + internal static Func GetDataTypeCallback = (docTypeAlias, propertyAlias) => Guid.Empty; + + internal static Guid GetDataType(string docTypeAlias, string propertyAlias) + { + return GetDataTypeCallback(docTypeAlias, propertyAlias); + } + + /// + /// Converts the currentValue to a correctly typed value based on known registered converters, then based on known standards. + /// + /// + /// + /// + /// + /// + internal static Attempt ConvertPropertyValue(object currentValue, Guid dataType, string docTypeAlias, string propertyTypeAlias) + { + if (currentValue == null) return Attempt.False; + + //First lets check all registered converters for this data type. + var converters = PropertyEditorValueConvertersResolver.Current.Converters + .Where(x => x.IsConverterFor(dataType, docTypeAlias, propertyTypeAlias)) + .ToArray(); + + //try to convert the value with any of the converters: + foreach (var converted in converters + .Select(p => p.ConvertPropertyValue(currentValue)) + .Where(converted => converted.Success)) + { + return new Attempt(true, converted.Result); + } + + //if none of the converters worked, then we'll process this from what we know + + var sResult = Convert.ToString(currentValue).Trim(); + + //this will eat csv strings, so only do it if the decimal also includes a decimal seperator (according to the current culture) + if (sResult.Contains(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)) + { + decimal dResult; + if (decimal.TryParse(sResult, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.CurrentCulture, out dResult)) + { + return new Attempt(true, dResult); + } + } + //process string booleans as booleans + if (sResult.InvariantEquals("true")) + { + return new Attempt(true, true); + } + if (sResult.InvariantEquals("false")) + { + return new Attempt(true, false); + } + + //a really rough check to see if this may be valid xml + //TODO: This is legacy code, I'm sure there's a better and nicer way + if (sResult.StartsWith("<") && sResult.EndsWith(">") && sResult.Contains("/")) + { + try + { + var e = XElement.Parse(DynamicXml.StripDashesInElementOrAttributeNames(sResult), LoadOptions.None); + + //check that the document element is not one of the disallowed elements + //allows RTE to still return as html if it's valid xhtml + var documentElement = e.Name.LocalName; + + //TODO: See note against this setting, pretty sure we don't need this + if (!UmbracoSettings.NotDynamicXmlDocumentElements.Any( + tag => string.Equals(tag, documentElement, StringComparison.CurrentCultureIgnoreCase))) + { + return new Attempt(true, new DynamicXml(e)); + } + return Attempt.False; + } + catch (Exception) + { + return Attempt.False; + } + } + return Attempt.False; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5f4bde8b72..b88d8fb917 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -84,6 +84,7 @@ + @@ -399,6 +400,7 @@ + diff --git a/src/Umbraco.Tests/CacheRefresherFactoryTests.cs b/src/Umbraco.Tests/CacheRefresherFactoryTests.cs index e9a2c075fb..8fdef3f09a 100644 --- a/src/Umbraco.Tests/CacheRefresherFactoryTests.cs +++ b/src/Umbraco.Tests/CacheRefresherFactoryTests.cs @@ -17,7 +17,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/DataTypeFactoryTests.cs b/src/Umbraco.Tests/DataTypeFactoryTests.cs index d836b8f836..e0de0a4af6 100644 --- a/src/Umbraco.Tests/DataTypeFactoryTests.cs +++ b/src/Umbraco.Tests/DataTypeFactoryTests.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/DynamicDocument/DynamicPublishedContentTests.cs b/src/Umbraco.Tests/DynamicDocument/DynamicPublishedContentTests.cs index 4fd97c45d5..e9c0dfa3b3 100644 --- a/src/Umbraco.Tests/DynamicDocument/DynamicPublishedContentTests.cs +++ b/src/Umbraco.Tests/DynamicDocument/DynamicPublishedContentTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -34,7 +35,7 @@ namespace Umbraco.Tests.DynamicDocument }); //need to specify a custom callback for unit tests - DynamicPublishedContent.GetDataTypeCallback = (docTypeAlias, propertyAlias) => + PublishedContentHelper.GetDataTypeCallback = (docTypeAlias, propertyAlias) => { if (propertyAlias == "content") { diff --git a/src/Umbraco.Tests/DynamicDocument/PublishedContentTests.cs b/src/Umbraco.Tests/DynamicDocument/PublishedContentTests.cs index 12d9670501..3c55138a75 100644 --- a/src/Umbraco.Tests/DynamicDocument/PublishedContentTests.cs +++ b/src/Umbraco.Tests/DynamicDocument/PublishedContentTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Web; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; @@ -72,7 +73,7 @@ namespace Umbraco.Tests.DynamicDocument }); //need to specify a custom callback for unit tests - DynamicPublishedContent.GetDataTypeCallback = (docTypeAlias, propertyAlias) => + PublishedContentHelper.GetDataTypeCallback = (docTypeAlias, propertyAlias) => { if (propertyAlias == "content") { @@ -105,6 +106,20 @@ namespace Umbraco.Tests.DynamicDocument return doc; } + [Test] + public void Get_Property_Value_Uses_Converter() + { + var doc = GetNode(1173); + + var propVal = doc.GetPropertyValue("content"); + Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal.GetType())); + Assert.AreEqual("
This is some content
", propVal.ToString()); + + var propVal2 = doc.GetPropertyValue("content"); + Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal2.GetType())); + Assert.AreEqual("
This is some content
", propVal2.ToString()); + } + [Test] public void Complex_Linq() { @@ -113,7 +128,7 @@ namespace Umbraco.Tests.DynamicDocument var result = doc.Ancestors().OrderBy(x => x.Level) .Single() .Descendants() - .FirstOrDefault(x => x.GetPropertyValue("selectedNodes", "").Split(',').Contains("1173")); + .FirstOrDefault(x => x.GetPropertyValue("selectedNodes", "").Split(',').Contains("1173")); Assert.IsNotNull(result); } @@ -255,17 +270,7 @@ namespace Umbraco.Tests.DynamicDocument Assert.IsTrue(visible.IsVisible()); } - [Test] - public void Ensure_TinyMCE_Converted_Type_User_Property() - { - var doc = GetNode(1173); - - throw new NotImplementedException("We currently don't have an extension method to return the formatted value using IPropertyValueConverter! This currently only works in the dynamic implementation"); - - //Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(doc.GetPropertyValue().Content.GetType())); - //Assert.AreEqual("
This is some content
", doc.Content.ToString()); - } - + [Test] public void Ancestor_Or_Self() { @@ -326,8 +331,8 @@ namespace Umbraco.Tests.DynamicDocument Assert.IsNotNull(result); - Assert.AreEqual(6, result.Count()); - Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1174, 1176, 1175 })); + Assert.AreEqual(7, result.Count()); + Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1174, 1176, 1175, 4444 })); } [Test] diff --git a/src/Umbraco.Tests/HashCodeCombinerTests.cs b/src/Umbraco.Tests/HashCodeCombinerTests.cs new file mode 100644 index 0000000000..b1c808e7f8 --- /dev/null +++ b/src/Umbraco.Tests/HashCodeCombinerTests.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Reflection; +using NUnit.Framework; +using Umbraco.Core; + +namespace Umbraco.Tests +{ + [TestFixture] + public class HashCodeCombinerTests : PartialTrust.AbstractPartialTrustFixture + { + + private DirectoryInfo PrepareFolder() + { + var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner", Guid.NewGuid().ToString("N"))); + foreach (var f in dir.GetFiles()) + { + f.Delete(); + } + return dir; + } + + [Test] + public void HashCombiner_Test_String() + { + var combiner1 = new HashCodeCombiner(); + combiner1.AddCaseInsensitiveString("Hello"); + + var combiner2 = new HashCodeCombiner(); + combiner2.AddCaseInsensitiveString("hello"); + + Assert.AreEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + + combiner2.AddCaseInsensitiveString("world"); + + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + } + + [Test] + public void HashCombiner_Test_Int() + { + var combiner1 = new HashCodeCombiner(); + combiner1.AddInt(1234); + + var combiner2 = new HashCodeCombiner(); + combiner2.AddInt(1234); + + Assert.AreEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + + combiner2.AddInt(1); + + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + } + + [Test] + public void HashCombiner_Test_DateTime() + { + var dt = DateTime.Now; + var combiner1 = new HashCodeCombiner(); + combiner1.AddDateTime(dt); + + var combiner2 = new HashCodeCombiner(); + combiner2.AddDateTime(dt); + + Assert.AreEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + + combiner2.AddDateTime(DateTime.Now); + + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + } + + [Test] + public void HashCombiner_Test_File() + { + var dir = PrepareFolder(); + var file1Path = Path.Combine(dir.FullName, "hastest1.txt"); + File.Delete(file1Path); + using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt"))) + { + file1.WriteLine("hello"); + } + var file2Path = Path.Combine(dir.FullName, "hastest2.txt"); + File.Delete(file2Path); + using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt"))) + { + //even though files are the same, the dates are different + file2.WriteLine("hello"); + } + + var combiner1 = new HashCodeCombiner(); + combiner1.AddFile(new FileInfo(file1Path)); + + var combiner2 = new HashCodeCombiner(); + combiner2.AddFile(new FileInfo(file1Path)); + + var combiner3 = new HashCodeCombiner(); + combiner3.AddFile(new FileInfo(file2Path)); + + Assert.AreEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner3.GetCombinedHashCode()); + + combiner2.AddFile(new FileInfo(file2Path)); + + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + } + + [Test] + public void HashCombiner_Test_Folder() + { + var dir = PrepareFolder(); + var file1Path = Path.Combine(dir.FullName, "hastest1.txt"); + File.Delete(file1Path); + using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt"))) + { + file1.WriteLine("hello"); + } + + //first test the whole folder + var combiner1 = new HashCodeCombiner(); + combiner1.AddFolder(dir); + + var combiner2 = new HashCodeCombiner(); + combiner2.AddFolder(dir); + + Assert.AreEqual(combiner1.GetCombinedHashCode(), combiner2.GetCombinedHashCode()); + + //now add a file to the folder + + var file2Path = Path.Combine(dir.FullName, "hastest2.txt"); + File.Delete(file2Path); + using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt"))) + { + //even though files are the same, the dates are different + file2.WriteLine("hello"); + } + + var combiner3 = new HashCodeCombiner(); + combiner3.AddFolder(dir); + + Assert.AreNotEqual(combiner1.GetCombinedHashCode(), combiner3.GetCombinedHashCode()); + } + + public override void TestSetup() + { + + } + + public override void TestTearDown() + { + + } + } +} diff --git a/src/Umbraco.Tests/MacroEngineFactoryTests.cs b/src/Umbraco.Tests/MacroEngineFactoryTests.cs index 4a058e272e..f38a825f6f 100644 --- a/src/Umbraco.Tests/MacroEngineFactoryTests.cs +++ b/src/Umbraco.Tests/MacroEngineFactoryTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/MediaFactoryTests.cs b/src/Umbraco.Tests/MediaFactoryTests.cs index 58fd81d446..11bf2de2fe 100644 --- a/src/Umbraco.Tests/MediaFactoryTests.cs +++ b/src/Umbraco.Tests/MediaFactoryTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/ObjectExtensionsTests.cs b/src/Umbraco.Tests/ObjectExtensionsTests.cs index 022a84e144..8a786ded6f 100644 --- a/src/Umbraco.Tests/ObjectExtensionsTests.cs +++ b/src/Umbraco.Tests/ObjectExtensionsTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NUnit.Framework; using Umbraco.Core; @@ -67,7 +68,8 @@ namespace Umbraco.Tests {"FALSE", false}, {"False", false}, {"false", false}, - {"0", false} + {"0", false}, + {"", false} }; foreach (var testCase in testCases) @@ -79,6 +81,34 @@ namespace Umbraco.Tests } } + [Test] + [TestOnlyInFullTrust] + public virtual void CanConvertStringToDateTime() + { + var dateTime = new DateTime(2012, 11, 10, 13, 14, 15); + var testCases = new Dictionary + { + {"2012-11-10", true}, + {"2012/11/10", true}, + {"10/11/2012", true}, + {"11/10/2012", false}, + {"Sat 10, Nov 2012", true}, + {"Saturday 10, Nov 2012", true}, + {"Sat 10, November 2012", true}, + {"Saturday 10, November 2012", true}, + {"2012-11-10 13:14:15", true}, + {"", false} + }; + + foreach (var testCase in testCases) + { + var result = testCase.Key.TryConvertTo(); + + Assert.IsTrue(result.Success); + Assert.AreEqual(DateTime.Equals(dateTime.Date, result.Result.Date), testCase.Value); + } + } + /// /// Run once before each test in derived test fixtures. /// diff --git a/src/Umbraco.Tests/PluginManagerTests.cs b/src/Umbraco.Tests/PluginManagerTests.cs index 69fb1afa9a..f7262c8814 100644 --- a/src/Umbraco.Tests/PluginManagerTests.cs +++ b/src/Umbraco.Tests/PluginManagerTests.cs @@ -1,4 +1,10 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; using System.Linq; +using System.Reflection; +using System.Web.Compilation; using NUnit.Framework; using SqlCE4Umbraco; using Umbraco.Core; @@ -11,6 +17,7 @@ using umbraco.MacroEngines.Iron; using umbraco.businesslogic; using umbraco.cms.businesslogic; using umbraco.editorControls; +using umbraco.interfaces; using umbraco.uicontrols; using umbraco.cms; @@ -27,7 +34,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver //TODO: Should probably update this so it only searches this assembly and add custom types to be found @@ -54,6 +61,160 @@ namespace Umbraco.Tests }; } + private DirectoryInfo PrepareFolder() + { + var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "PluginManager", Guid.NewGuid().ToString("N"))); + foreach (var f in dir.GetFiles()) + { + f.Delete(); + } + return dir; + } + + //[Test] + //public void Scan_Vs_Load_Benchmark() + //{ + // var pluginManager = new PluginManager(false); + // var watch = new Stopwatch(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Type.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type3 = Type.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type4 = Type.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type5 = Type.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = BuildManager.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type3 = BuildManager.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type4 = BuildManager.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type5 = BuildManager.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var refreshers = pluginManager.ResolveTypes(false); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); + //} + + ////NOTE: This test shows that Type.GetType is 100% faster than Assembly.Load(..).GetType(...) so we'll use that :) + //[Test] + //public void Load_Type_Benchmark() + //{ + // var watch = new Stopwatch(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Type.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type3 = Type.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type4 = Type.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type5 = Type.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.macroCacheRefresh"); + // var type3 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.templateCacheRefresh"); + // var type4 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.presentation.cache.MediaLibraryRefreshers"); + // var type5 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.presentation.cache.pageRefresher"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = BuildManager.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type3 = BuildManager.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type4 = BuildManager.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type5 = BuildManager.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + //} + + [Test] + public void Create_Cached_Plugin_File() + { + var types = new[] {typeof (PluginManager), typeof (PluginManagerTests), typeof (UmbracoContext)}; + + var manager = new PluginManager(false); + //yes this is silly, none of these types inherit from string, but this is just to test the xml file format + manager.UpdateCachedPluginsFile(types); + + var plugins = manager.TryGetCachedPluginsFromFile(); + Assert.IsTrue(plugins.Success); + Assert.AreEqual(3, plugins.Result.Count()); + var shouldContain = types.Select(x => x.AssemblyQualifiedName); + //ensure they are all found + Assert.IsTrue(plugins.Result.ContainsAll(shouldContain)); + } + + [Test] + public void PluginHash_From_String() + { + var s = "hello my name is someone".GetHashCode().ToString("x", CultureInfo.InvariantCulture); + var output = PluginManager.ConvertPluginsHashFromHex(s); + Assert.AreNotEqual(0, output); + } + + [Test] + public void Get_Plugins_Hash() + { + //Arrange + var dir = PrepareFolder(); + var d1 = dir.CreateSubdirectory("1"); + var d2 = dir.CreateSubdirectory("2"); + var d3 = dir.CreateSubdirectory("3"); + var d4 = dir.CreateSubdirectory("4"); + var f1 = new FileInfo(Path.Combine(d1.FullName, "test1.dll")); + var f2 = new FileInfo(Path.Combine(d1.FullName, "test2.dll")); + var f3 = new FileInfo(Path.Combine(d2.FullName, "test1.dll")); + var f4 = new FileInfo(Path.Combine(d2.FullName, "test2.dll")); + var f5 = new FileInfo(Path.Combine(d3.FullName, "test1.dll")); + var f6 = new FileInfo(Path.Combine(d3.FullName, "test2.dll")); + var f7 = new FileInfo(Path.Combine(d4.FullName, "test1.dll")); + f1.CreateText().Close(); + f2.CreateText().Close(); + f3.CreateText().Close(); + f4.CreateText().Close(); + f5.CreateText().Close(); + f6.CreateText().Close(); + f7.CreateText().Close(); + var list1 = new[] { f1, f2, f3, f4, f5, f6 }; + var list2 = new[] { f1, f3, f5 }; + var list3 = new[] { f1, f3, f5, f7 }; + + //Act + var hash1 = PluginManager.GetAssembliesHash(list1); + var hash2 = PluginManager.GetAssembliesHash(list2); + var hash3 = PluginManager.GetAssembliesHash(list3); + + //Assert + + //both should be the same since we only create the hash based on the unique folder of the list passed in, yet + //all files will exist in those folders still + Assert.AreEqual(hash1, hash2); + Assert.AreNotEqual(hash1, hash3); + } + [Test] public void Ensure_Only_One_Type_List_Created() { diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs new file mode 100644 index 0000000000..c8616bb7c7 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class PropertyEditorValueConverterTests + { + [Test] + public void CanConvertDatePickerPropertyEditor() + { + var converter = new DatePickerPropertyEditorValueConverter(); + var dateTime = new DateTime(2012, 11, 10, 13, 14, 15); + var testCases = new Dictionary + { + {"2012-11-10", true}, + {"2012/11/10", true}, + {"10/11/2012", true}, + {"11/10/2012", false}, + {"Sat 10, Nov 2012", true}, + {"Saturday 10, Nov 2012", true}, + {"Sat 10, November 2012", true}, + {"Saturday 10, November 2012", true}, + {"2012-11-10 13:14:15", true}, + {"", false} + }; + + foreach (var testCase in testCases) + { + var result = converter.ConvertPropertyValue(testCase.Key); + + Assert.IsTrue(result.Success); + Assert.AreEqual(DateTime.Equals(dateTime.Date, ((DateTime)result.Result).Date), testCase.Value); + } + } + + [Test] + public void CanConvertYesNoPropertyEditor() + { + var converter = new YesNoPropertyEditorValueConverter(); + var testCases = new Dictionary + { + {"TRUE", true}, + {"True", true}, + {"true", true}, + {"1", true}, + {"FALSE", false}, + {"False", false}, + {"false", false}, + {"0", false}, + {"", false} + }; + + foreach (var testCase in testCases) + { + var result = converter.ConvertPropertyValue(testCase.Key); + + Assert.IsTrue(result.Success); + Assert.AreEqual(testCase.Value, result.Result); + } + } + } +} diff --git a/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs b/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs index d9bd05c7ce..34b834753b 100644 --- a/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs b/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs index eff9f82b53..6f7ae14ce9 100644 --- a/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs b/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs index b126fde180..a12890a36f 100644 --- a/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index a59f45a0d3..7d04ea49ef 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -92,9 +92,11 @@ + + @@ -255,6 +257,7 @@ Umbraco.Web + False diff --git a/src/Umbraco.Web/DefaultPublishedMediaStore.cs b/src/Umbraco.Web/DefaultPublishedMediaStore.cs index 2283c2a18f..6237912c9e 100644 --- a/src/Umbraco.Web/DefaultPublishedMediaStore.cs +++ b/src/Umbraco.Web/DefaultPublishedMediaStore.cs @@ -330,9 +330,9 @@ namespace Umbraco.Web ValidateAndSetProperty(valueDictionary, val => DocumentTypeAlias = val, "nodeTypeAlias", "__NodeTypeAlias"); ValidateAndSetProperty(valueDictionary, val => DocumentTypeId = int.Parse(val), "nodeType"); ValidateAndSetProperty(valueDictionary, val => WriterName = val, "writerName"); - ValidateAndSetProperty(valueDictionary, val => CreatorName = val, "creatorName"); + ValidateAndSetProperty(valueDictionary, val => CreatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132 ValidateAndSetProperty(valueDictionary, val => WriterId = int.Parse(val), "writerID"); - ValidateAndSetProperty(valueDictionary, val => CreatorId = int.Parse(val), "creatorID"); + ValidateAndSetProperty(valueDictionary, val => CreatorId = int.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132 ValidateAndSetProperty(valueDictionary, val => Path = val, "path", "__Path"); ValidateAndSetProperty(valueDictionary, val => CreateDate = DateTime.Parse(val), "createDate"); ValidateAndSetProperty(valueDictionary, val => UpdateDate = DateTime.Parse(val), "updateDate"); diff --git a/src/Umbraco.Web/Models/DynamicPublishedContent.cs b/src/Umbraco.Web/Models/DynamicPublishedContent.cs index 4e3c423863..990abd2296 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContent.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContent.cs @@ -22,12 +22,6 @@ namespace Umbraco.Web.Models /// public class DynamicPublishedContent : DynamicObject, IPublishedContent { - /// - /// This callback is used only so we can set it dynamically for use in unit tests - /// - internal static Func GetDataTypeCallback = (docTypeAlias, propertyAlias) => - ContentType.GetDataType(docTypeAlias, propertyAlias); - protected IPublishedContent PublishedContent { get; private set; } private DynamicPublishedContentList _cachedChildren; private readonly ConcurrentDictionary _cachedMemberOutput = new ConcurrentDictionary(); @@ -263,10 +257,10 @@ namespace Umbraco.Web.Models } //get the data type id for the current property - var dataType = GetDataType(userProperty.DocumentTypeAlias, userProperty.Alias); + var dataType = Umbraco.Core.PublishedContentHelper.GetDataType(userProperty.DocumentTypeAlias, userProperty.Alias); //convert the string value to a known type - var converted = ConvertPropertyValue(result, dataType, userProperty.DocumentTypeAlias, userProperty.Alias); + var converted = Umbraco.Core.PublishedContentHelper.ConvertPropertyValue(result, dataType, userProperty.DocumentTypeAlias, userProperty.Alias); if (converted.Success) { result = converted.Result; @@ -824,87 +818,7 @@ namespace Umbraco.Web.Models } #endregion - private static Guid GetDataType(string docTypeAlias, string propertyAlias) - { - return GetDataTypeCallback(docTypeAlias, propertyAlias); - } - - /// - /// Converts the currentValue to a correctly typed value based on known registered converters, then based on known standards. - /// - /// - /// - /// - /// - /// - private Attempt ConvertPropertyValue(object currentValue, Guid dataType, string docTypeAlias, string propertyTypeAlias) - { - if (currentValue == null) return Attempt.False; - - //First lets check all registered converters for this data type. - var converters = PropertyEditorValueConvertersResolver.Current.Converters - .Where(x => x.IsConverterFor(dataType, docTypeAlias, propertyTypeAlias)) - .ToArray(); - - //try to convert the value with any of the converters: - foreach (var converted in converters - .Select(p => p.ConvertPropertyValue(currentValue)) - .Where(converted => converted.Success)) - { - return new Attempt(true, converted.Result); - } - - //if none of the converters worked, then we'll process this from what we know - - var sResult = Convert.ToString(currentValue).Trim(); - - //this will eat csv strings, so only do it if the decimal also includes a decimal seperator (according to the current culture) - if (sResult.Contains(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)) - { - decimal dResult; - if (decimal.TryParse(sResult, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.CurrentCulture, out dResult)) - { - return new Attempt(true, dResult); - } - } - //process string booleans as booleans - if (sResult.InvariantEquals("true")) - { - return new Attempt(true, true); - } - if (sResult.InvariantEquals("false")) - { - return new Attempt(true, false); - } - - //a really rough check to see if this may be valid xml - //TODO: This is legacy code, I'm sure there's a better and nicer way - if (sResult.StartsWith("<") && sResult.EndsWith(">") && sResult.Contains("/")) - { - try - { - var e = XElement.Parse(DynamicXml.StripDashesInElementOrAttributeNames(sResult), LoadOptions.None); - - //check that the document element is not one of the disallowed elements - //allows RTE to still return as html if it's valid xhtml - var documentElement = e.Name.LocalName; - - //TODO: See note against this setting, pretty sure we don't need this - if (!UmbracoSettings.NotDynamicXmlDocumentElements.Any( - tag => string.Equals(tag, documentElement, StringComparison.CurrentCultureIgnoreCase))) - { - return new Attempt(true, new DynamicXml(e)); - } - return Attempt.False; - } - catch (Exception) - { - return Attempt.False; - } - } - return Attempt.False; - } - + #region Index/Position public int Position() { diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs b/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs index c1fe63605e..140f0d1de5 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs @@ -312,7 +312,15 @@ namespace Umbraco.Web.Routing { LogHelper.Debug("{0}Page is protected, check for access", () => tracePrefix); - var user = System.Web.Security.Membership.GetUser(); + System.Web.Security.MembershipUser user = null; + try + { + user = System.Web.Security.Membership.GetUser(); + } + catch (ArgumentException) + { + LogHelper.Debug("{0}Membership.GetUser returned ArgumentException", () => tracePrefix); + } if (user == null || !Member.IsLoggedOn()) { diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index bf722a4300..7c97ecce6b 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -48,6 +48,8 @@ namespace Umbraco.Web if (httpContext == null) throw new ArgumentNullException("httpContext"); if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + ObjectCreated = DateTime.Now; + HttpContext = httpContext; Application = applicationContext; RoutesCache = routesCache; @@ -100,6 +102,13 @@ namespace Umbraco.Web } } + /// + /// This is used internally for performance calculations, the ObjectCreated DateTime is set as soon as this + /// object is instantiated which in the web site is created during the BeginRequest phase. + /// We can then determine complete rendering time from that. + /// + internal DateTime ObjectCreated { get; private set; } + /// /// Gets the current ApplicationContext /// diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index e8b5c81562..4be9eba95f 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -260,8 +260,8 @@ namespace Umbraco.Web }; //this is here to figure out if this request is in the context of a partial - if (_umbracoContext.PublishedContentRequest.PublishedContent.Id != _currentPage.Id) - item.NodeId = _currentPage.Id.ToString(); + if (_umbracoContext.PublishedContentRequest.PublishedContent.Id != currentPage.Id) + item.NodeId = currentPage.Id.ToString(); var containerPage = new FormlessPage(); diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 24292936cf..cad2639599 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -37,7 +37,10 @@ namespace Umbraco.Web { // do not process if client-side request if (IsClientSideRequest(httpContext.Request.Url)) - return; + return; + + //write the trace output for diagnostics at the end of the request + httpContext.Trace.Write("UmbracoModule", "Umbraco request begins"); // ok, process @@ -94,6 +97,8 @@ namespace Umbraco.Web if (!EnsureUmbracoRoutablePage(umbracoContext, httpContext)) return; + httpContext.Trace.Write("UmbracoModule", "Umbraco request confirmed"); + // ok, process var uri = umbracoContext.OriginalRequestUrl; @@ -400,7 +405,7 @@ namespace Umbraco.Web { app.BeginRequest += (sender, e) => { - var httpContext = ((HttpApplication)sender).Context; + var httpContext = ((HttpApplication)sender).Context; BeginRequest(new HttpContextWrapper(httpContext)); }; @@ -417,7 +422,16 @@ namespace Umbraco.Web PersistXmlCache(new HttpContextWrapper(httpContext)); }; - // todo: initialize request errors handler + app.EndRequest += (sender, args) => + { + var httpContext = ((HttpApplication)sender).Context; + if (UmbracoContext.Current != null && UmbracoContext.Current.IsFrontEndUmbracoRequest) + { + //write the trace output for diagnostics at the end of the request + httpContext.Trace.Write("UmbracoModule", "Umbraco request completed"); + LogHelper.Debug("Total milliseconds for umbraco request to process: " + DateTime.Now.Subtract(UmbracoContext.Current.ObjectCreated).TotalMilliseconds); + } + }; } public void Dispose() diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 788330bc19..58d3c7fbab 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -15,6 +15,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Routing; using umbraco.businesslogic; +using umbraco.cms.businesslogic; namespace Umbraco.Web @@ -195,6 +196,10 @@ namespace Umbraco.Web { base.InitializeResolvers(); + //TODO: This needs to be removed in future versions (i.e. 6.0 when the PublishedContentHelper can access the business logic) + // see the TODO noted in the PublishedContentHelper. + PublishedContentHelper.GetDataTypeCallback = ContentType.GetDataType; + SurfaceControllerResolver.Current = new SurfaceControllerResolver( PluginManager.Current.ResolveSurfaceControllers()); diff --git a/src/umbraco.cms/businesslogic/template/Template.cs b/src/umbraco.cms/businesslogic/template/Template.cs index 936f45b6e5..32763615c9 100644 --- a/src/umbraco.cms/businesslogic/template/Template.cs +++ b/src/umbraco.cms/businesslogic/template/Template.cs @@ -263,11 +263,6 @@ namespace umbraco.cms.businesslogic.template FlushCache(); _design = value.Trim(Environment.NewLine.ToCharArray()); - // NH: Removing an generating the directive can mess up code behind - // We don't store the masterpage directive in the design value - // if (_design.StartsWith("<%@")) - // _design = _design.Substring(_design.IndexOf("%>") + 3).Trim(Environment.NewLine.ToCharArray()); - //we only switch to MVC View editing if the template has a view file, and MVC editing is enabled if (Umbraco.Core.Configuration.UmbracoSettings.DefaultRenderingEngine == RenderingEngine.Mvc && !MasterPageHelper.IsMasterPageSyntax(_design)) @@ -549,26 +544,7 @@ namespace umbraco.cms.businesslogic.template // NH: Changed this; if you delete a template we'll remove all references instead of // throwing an exception if (DocumentType.GetAllAsList().Where(x => x.allowedTemplates.Select(t => t.Id).Contains(this.Id)).Count() > 0) - { - // the uncommented code below have been refactored into removeAllReferences method that clears template - // from documenttypes, subtemplates and documents. RemoveAllReferences(); - /* - - // Added to remove template doctype relationship - SqlHelper.ExecuteNonQuery("delete from cmsDocumentType where templateNodeId =" + this.Id); - - // Need to update any other template that references this one as it's master to NULL - SqlHelper.ExecuteNonQuery("update cmsTemplate set [master] = NULL where [master] = " + this.Id); - */ - - // don't allow template deletion if it is in use - // (get all doc types and filter based on any that have the template id of this one) - /* - Log.Add(LogTypes.Error, this.Id, "Can't delete a template that is assigned to existing content"); - throw new InvalidOperationException("Can't delete a template that is assigned to existing content"); - */ - } DeleteEventArgs e = new DeleteEventArgs(); FireBeforeDelete(e); @@ -645,7 +621,7 @@ namespace umbraco.cms.businesslogic.template public string ConvertToMasterPageSyntax(string templateDesign) { - string masterPageContent = GetMasterContentElement(MasterTemplate) + "\n"; + string masterPageContent = GetMasterContentElement(MasterTemplate) + Environment.NewLine; masterPageContent += templateDesign; @@ -653,7 +629,9 @@ namespace umbraco.cms.businesslogic.template masterPageContent = EnsureMasterPageSyntax(masterPageContent); // append ending asp:content element - masterPageContent += "\n" + Environment.NewLine; + masterPageContent += Environment.NewLine + + "" + + Environment.NewLine; return masterPageContent; } @@ -683,98 +661,14 @@ namespace umbraco.cms.businesslogic.template public void ImportDesign(string design) { Design = design; - - /* - if (!isMasterPageSyntax(design)) - { - Design = ConvertToMasterPageSyntax(design); - } - else - { - Design = design; - }*/ - } public void SaveMasterPageFile(string masterPageContent) { //this will trigger the helper and store everything this.Design = masterPageContent; - - /* - - // Add header to master page if it doesn't exist - if (!masterPageContent.StartsWith("<%@")) - { - masterPageContent = getMasterPageHeader() + "\n" + masterPageContent; - } - else - { - // verify that the masterpage attribute is the same as the masterpage - string masterHeader = masterPageContent.Substring(0, masterPageContent.IndexOf("%>") + 2).Trim(Environment.NewLine.ToCharArray()); - // find the masterpagefile attribute - MatchCollection m = Regex.Matches(masterHeader, "(?\\S*)=\"(?[^\"]*)\"", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - foreach (Match attributeSet in m) - { - if (attributeSet.Groups["attributeName"].Value.ToLower() == "masterpagefile") - { - // validate the masterpagefile - string currentMasterPageFile = attributeSet.Groups["attributeValue"].Value; - string currentMasterTemplateFile = currentMasterTemplateFileName(); - if (currentMasterPageFile != currentMasterTemplateFile) - { - masterPageContent = - masterPageContent.Replace( - attributeSet.Groups["attributeName"].Value + "=\"" + currentMasterPageFile + "\"", - attributeSet.Groups["attributeName"].Value + "=\"" + currentMasterTemplateFile + "\""); - - } - } - } - - } - - //we have a Old Alias if the alias and therefor the masterpage file name has changed... - //so before we save the new masterfile, we'll clear the old one, so we don't up with - //Unused masterpage files - if (!string.IsNullOrEmpty(_oldAlias) && _oldAlias != _alias) - { - - //Ensure that child templates have the right master masterpage file name - if (HasChildren) - { - //store children array here because iterating over an Array property object is very inneficient. - var c = Children; - foreach (CMSNode cmn in c) - { - Template t = new Template(cmn.Id); - t.SaveAsMasterPage(); - t.Save(); - } - } - - //then kill the old file.. - string _oldFile = IOHelper.MapPath(SystemDirectories.Masterpages + "/" + _oldAlias.Replace(" ", "") + ".master"); - - if (System.IO.File.Exists(_oldFile)) - System.IO.File.Delete(_oldFile); - } - - // save the file in UTF-8 - - File.WriteAllText(MasterPageFile, masterPageContent, System.Text.Encoding.UTF8); - * */ } - - private string CurrentMasterTemplateFileName() - { - if (MasterTemplate != 0) - return SystemDirectories.Masterpages + "/" + new Template(MasterTemplate).Alias.Replace(" ", "") + ".master"; - else - return UmbracoMasterTemplate; - } - + private void GetAspNetMasterPageForm(ref string design) { Match formElement = Regex.Match(design, GetElementRegExp("?ASPNET_FORM", false), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); diff --git a/src/umbraco.editorControls/tags/DataEditor.cs b/src/umbraco.editorControls/tags/DataEditor.cs index ab80d352c4..4281e1f12a 100644 --- a/src/umbraco.editorControls/tags/DataEditor.cs +++ b/src/umbraco.editorControls/tags/DataEditor.cs @@ -3,21 +3,20 @@ using System.Collections; using System.Linq; using System.Web.UI; using System.Web.UI.WebControls; -using umbraco.BusinessLogic; using ClientDependency.Core.Controls; using ClientDependency.Core; -using umbraco.presentation; +using umbraco.interfaces; namespace umbraco.editorControls.tags { - public class DataEditor : System.Web.UI.UpdatePanel, umbraco.interfaces.IDataEditor, umbraco.interfaces.IUseTags + public class DataEditor : TextBox, IDataEditor, IUseTags { #region IDataEditor Members - cms.businesslogic.datatype.DefaultData _data; - string _group = ""; + private readonly cms.businesslogic.datatype.DefaultData _data; + readonly string _group = ""; - public DataEditor(umbraco.interfaces.IData Data, SortedList Prevalues) + public DataEditor(IData Data, SortedList Prevalues) { _data = (cms.businesslogic.datatype.DefaultData)Data; @@ -25,41 +24,22 @@ namespace umbraco.editorControls.tags _group = Prevalues["group"].ToString(); } - public TextBox tagBox = new TextBox(); - - public Control Editor { get { return this; } } public void Save() { - int _nodeID; - int.TryParse(_data.NodeId.ToString(), out _nodeID); - string allTags = ""; + int nodeId = _data.NodeId; //first clear out all items associated with this ID... - umbraco.cms.businesslogic.Tags.Tag.RemoveTagsFromNode(_nodeID, _group); + cms.businesslogic.Tags.Tag.RemoveTagsFromNode(nodeId, _group); - var items = tagBox.Text.Trim().Split(','); - foreach (var item in items) - { - var tagName = item.Trim(); - if(string.IsNullOrEmpty(tagName)) - continue; - - var tagId = cms.businesslogic.Tags.Tag.GetTagId(tagName, _group); - if(tagId == 0) - tagId = cms.businesslogic.Tags.Tag.AddTag(tagName, _group); - - if (tagId > 0) - { - umbraco.cms.businesslogic.Tags.Tag.AssociateTagToNode(_nodeID, tagId); - } - } + UpdateOrAddTags(nodeId); //and just in case, we'll save the tags as plain text on the node itself... - _data.Value = tagBox.Text; + _data.Value = this.Text; } + public bool ShowLabel { get { return true; } @@ -74,35 +54,54 @@ namespace umbraco.editorControls.tags [Obsolete("use the umbraco.cms.businesslogic.Tags.Tag class instead")] public int getTagId(string tag, string group) { - return umbraco.cms.businesslogic.Tags.Tag.GetTagId(tag, group); + return cms.businesslogic.Tags.Tag.GetTagId(tag, group); } - protected override void OnInit(EventArgs e) { base.OnInit(e); - //ClientDependencyLoader.Instance.RegisterDependency("Application/JQuery/jquery.autocomplete.js", "UmbracoClient", ClientDependencyType.Javascript); + // register all dependencies - only registered once ClientDependencyLoader.Instance.RegisterDependency("ui/ui-lightness/jquery-ui.custom.css", "UmbracoClient", ClientDependencyType.Css); ClientDependencyLoader.Instance.RegisterDependency("css/umbracoGui.css", "UmbracoRoot", ClientDependencyType.Css); - ClientDependencyLoader.Instance.RegisterDependency("tags/css/jquery.tagsinput.css", "UmbracoClient", ClientDependencyType.Css); ClientDependencyLoader.Instance.RegisterDependency("tags/js/jquery.tagsinput.min.js", "UmbracoClient", ClientDependencyType.Javascript); - - string _alias = ((umbraco.cms.businesslogic.datatype.DefaultData)_data).PropertyId.ToString(); + var pageId = GetPageId(); + if (!Page.IsPostBack && !String.IsNullOrEmpty(pageId) && string.IsNullOrWhiteSpace(this.Text)) + { + var tags = cms.businesslogic.Tags.Tag.GetTags(int.Parse(pageId), _group); + this.Text = string.Join(",", tags.Select(x => x.TagCaption)); + } - //making sure that we have a ID for context - string pageId = UmbracoContext.Current.Request["id"]; + var tagsAutoCompleteScript = TagsAutocompleteScript(pageId); + var tagsAutoCompleteInitScript = string.Format("jQuery(document).ready(function(){{{0}}});", tagsAutoCompleteScript); - if (pageId != null) pageId = pageId.Trim(); + // register it as a startup script + Page.ClientScript.RegisterStartupScript(GetType(), ClientID + "_tagsinit", tagsAutoCompleteInitScript, true); + } + private string TagsAutocompleteScript(string pageId) + { + var tagsAutoCompleteScript = string.Format("jQuery('#{0}').tagsInput({{ width: '400px', defaultText: 'Add a tag', minChars: 2, autocomplete_url: '{1}/webservices/TagsAutoCompleteHandler.ashx?group={2}&id={3}&rnd={4}&format=json' }});", + this.ClientID, + Umbraco.Core.IO.IOHelper.ResolveUrl(Umbraco.Core.IO.SystemDirectories.Umbraco), + _group, + pageId, + DateTime.Now.Ticks); + + return tagsAutoCompleteScript; + } + + private static string GetPageId() + { + var pageId = Umbraco.Web.UmbracoContext.Current.HttpContext.Request["id"]; if (string.IsNullOrEmpty(pageId)) { // we need an empty try/catch as Node.GetCurrent() will throw an exception if we're outside of Umbraco Context try { - NodeFactory.Node currentNode = NodeFactory.Node.GetCurrent(); + var currentNode = NodeFactory.Node.GetCurrent(); if (currentNode != null) { pageId = currentNode.Id.ToString(); @@ -112,38 +111,28 @@ namespace umbraco.editorControls.tags { } } + if (pageId != null) pageId = pageId.Trim(); + return pageId; + } - tagBox.ID = "tagBox2_" + _alias; - tagBox.CssClass = "umbEditorTextField umbTagBox"; - - if (!String.IsNullOrEmpty(pageId)) + private void UpdateOrAddTags(int nodeId) + { + var items = this.Text.Trim().Split(','); + foreach (var item in items) { - var tags = umbraco.cms.businesslogic.Tags.Tag.GetTags(int.Parse(pageId), _group); + var tagName = item.Trim(); + if (string.IsNullOrEmpty(tagName)) + continue; - tagBox.Text = string.Join(",", tags.Select(x => x.TagCaption)); + var tagId = cms.businesslogic.Tags.Tag.GetTagId(tagName, _group); + if (tagId == 0) + tagId = cms.businesslogic.Tags.Tag.AddTag(tagName, _group); + + if (tagId > 0) + { + cms.businesslogic.Tags.Tag.AssociateTagToNode(nodeId, tagId); + } } - - this.ContentTemplateContainer.Controls.Add(tagBox); - - string tagsAutoCompleteScript = - "jQuery('#" + tagBox.ClientID + "').tagsInput({ width: '400px', defaultText: 'Add a tag', minChars: 2, autocomplete_url: '" + - umbraco.IO.IOHelper.ResolveUrl(umbraco.IO.SystemDirectories.Umbraco) - + "/webservices/TagsAutoCompleteHandler.ashx?group=" + _group + "&id=" + pageId + "&rnd=" + - DateTime.Now.Ticks + "&format=json' });"; - - - string tagsAutoCompleteInitScript = - "jQuery(document).ready(function(){" - + tagsAutoCompleteScript - + "});"; - - Page.ClientScript.RegisterStartupScript(GetType(), ClientID + "_tagsinit", tagsAutoCompleteInitScript, true); - - if (Page.IsPostBack) - { - ScriptManager.RegisterClientScriptBlock(this, GetType(), ClientID + "_tags", tagsAutoCompleteScript, true); - } - } #endregion @@ -155,50 +144,35 @@ namespace umbraco.editorControls.tags get { return _group; } } - #endregion - - #region IUseTags Members - - public void RemoveTag(int nodeId, string tag) { - library.RemoveTagFromNode(nodeId, tag, _group); + cms.businesslogic.Tags.Tag.RemoveTagFromNode(nodeId, tag, _group); } - public System.Collections.Generic.List GetTagsFromNode(int nodeId) + public System.Collections.Generic.List GetTagsFromNode(int nodeId) { - return library.GetTagsFromNodeAsITags(nodeId); + return cms.businesslogic.Tags.Tag.GetTags(nodeId).Cast().ToList(); } - public System.Collections.Generic.List GetAllTags() + public System.Collections.Generic.List GetAllTags() { - return library.GetTagsFromGroupAsITags(_group); + return cms.businesslogic.Tags.Tag.GetTags(_group).Cast().ToList(); } - #endregion - - #region IUseTags Members - - public void AddTag(string tag) { - library.AddTag(tag, _group); + cms.businesslogic.Tags.Tag.AddTag(tag, _group); } public void AddTagToNode(int nodeId, string tag) { - library.addTagsToNode(nodeId, tag, _group); + cms.businesslogic.Tags.Tag.AddTagsToNode(nodeId, tag, _group); } - #endregion - - #region IUseTags Members - - public void RemoveTagsFromNode(int nodeId) { - foreach (umbraco.interfaces.ITag t in library.GetTagsFromNodeAsITags(nodeId)) - library.RemoveTagFromNode(nodeId, t.TagCaption, t.Group); + foreach (var tag in cms.businesslogic.Tags.Tag.GetTags(nodeId).Cast().ToList()) + cms.businesslogic.Tags.Tag.RemoveTagFromNode(nodeId, tag.TagCaption, tag.Group); } #endregion diff --git a/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs b/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs index 232caab2ed..96c9af703d 100644 --- a/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs +++ b/src/umbraco.editorControls/tinyMCE3/TinyMCE.cs @@ -324,6 +324,7 @@ namespace umbraco.editorControls.tinyMCE3 public virtual void Save() { string parsedString = base.Text.Trim(); + string parsedStringForTinyMce = parsedString; if (parsedString != string.Empty) { parsedString = replaceMacroTags(parsedString).Trim(); @@ -370,19 +371,22 @@ namespace umbraco.editorControls.tinyMCE3 parsedString = parsedString.Replace(helper.GetBaseUrl(HttpContext.Current) + "/#", "#"); parsedString = parsedString.Replace(helper.GetBaseUrl(HttpContext.Current), ""); } + // if a paragraph is empty, remove it + if (parsedString.ToLower() == "

") + parsedString = ""; + + // save string after all parsing is done, but before CDATA replacement - to put back into TinyMCE + parsedStringForTinyMce = parsedString; //Allow CDATA nested into RTE without exceptions // GE 2012-01-18 parsedString = parsedString.Replace("").Replace("]]>", ""); - - // if text is an empty parargraph, make it all empty - if (parsedString.ToLower() == "

") - parsedString = ""; } + _data.Value = parsedString; // update internal webcontrol value with parsed result - base.Text = parsedString; + base.Text = parsedStringForTinyMce; } #endregion