diff --git a/build/Build.bat b/build/Build.bat index 2fc0dc79c8..107edbf7ea 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -1,5 +1,9 @@ @ECHO OFF -SET release=7.1.2 +IF NOT EXIST UmbracoVersion.txt ( + ECHO UmbracoVersion.txt missing! + GOTO :showerror +) +SET /p release= - Debug + Release _BuildOutput\ UmbracoCms$(DECIMAL_BUILD_NUMBER).zip UmbracoCms.AllBinaries$(DECIMAL_BUILD_NUMBER).zip @@ -325,7 +325,7 @@ Regex="CurrentComment { get { return "([a-zA-Z]+)?"" ReplacementText="CurrentComment { get { return "$(BUILD_COMMENT)""/> - diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index e7673f054e..e323abbb0d 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -1,4 +1,5 @@ @ECHO OFF +SET release=%1 ECHO Installing Npm NuGet Package SET nuGetFolder=%CD%\..\src\packages\ @@ -15,6 +16,8 @@ SET oldPath=%PATH% path=%npmPath%;%nodePath%;%PATH% +ECHO %path% + SET buildFolder=%CD% ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\ @@ -24,7 +27,7 @@ ECHO Do npm install and the grunt build of Belle call npm install call npm install -g grunt-cli call npm install -g bower -call grunt build --buildversion=7.1.2 +call grunt build --buildversion=%release% ECHO Reset path to what it was before path=%oldPath% diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index e70d12525d..b770d8cfdc 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -30,8 +30,8 @@ - - + + @@ -67,5 +67,6 @@ + \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 85431799e8..40e7c1b4da 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -1,6 +1,6 @@ - + UmbracoCms 7.0.0 Umbraco Cms diff --git a/build/NuSpecs/build/UmbracoCms.props b/build/NuSpecs/build/UmbracoCms.props index 5e11945a9d..1422a4cd76 100644 --- a/build/NuSpecs/build/UmbracoCms.props +++ b/build/NuSpecs/build/UmbracoCms.props @@ -1,12 +1,11 @@ - + AddUmbracoFilesToOutput; $(CopyAllFilesToSingleFolderForPackageDependsOn); - - + AddUmbracoFilesToOutput; $(CopyAllFilesToSingleFolderForPackageDependsOn); diff --git a/build/NuSpecs/build/UmbracoCms.targets b/build/NuSpecs/build/UmbracoCms.targets index 721f40c976..024d8af7ad 100644 --- a/build/NuSpecs/build/UmbracoCms.targets +++ b/build/NuSpecs/build/UmbracoCms.targets @@ -1,11 +1,8 @@  - - 7.1.2 - - ..\packages\UmbracoCms.$(UmbracoVersion)\UmbracoFiles\ + $(MSBuildThisFileDirectory)..\UmbracoFiles\ @@ -33,6 +30,9 @@ Config\Splashes + + data + umbraco diff --git a/build/NuSpecs/tools/install.core.ps1 b/build/NuSpecs/tools/install.core.ps1 new file mode 100644 index 0000000000..c712b363bc --- /dev/null +++ b/build/NuSpecs/tools/install.core.ps1 @@ -0,0 +1,24 @@ +param($rootPath, $toolsPath, $package, $project) + +if ($project) { + $dateTime = Get-Date -Format yyyyMMdd-HHmmss + $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + $copyLogsPath = Join-Path $backupPath "CopyLogs" + $projectDestinationPath = Split-Path $project.FullName -Parent + + # Create backup folder and logs folder if it doesn't exist yet + New-Item -ItemType Directory -Force -Path $backupPath + New-Item -ItemType Directory -Force -Path $copyLogsPath + + # After backing up, remove all dlls from bin folder in case dll files are included in the VS project + # See: http://issues.umbraco.org/issue/U4-4930 + $umbracoBinFolder = Join-Path $projectDestinationPath "bin" + if(Test-Path $umbracoBinFolder) { + $umbracoBinBackupPath = Join-Path $backupPath "bin" + + New-Item -ItemType Directory -Force -Path $umbracoBinBackupPath + + robocopy $umbracoBinFolder $umbracoBinBackupPath /e /LOG:$copyLogsPath\UmbracoBinBackup.log + Remove-Item $umbracoBinFolder\*.dll -Force -Confirm:$false + } +} \ No newline at end of file diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 8d25ea23bf..d7b1f5a217 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -3,9 +3,11 @@ param($rootPath, $toolsPath, $package, $project) if ($project) { $dateTime = Get-Date -Format yyyyMMdd-HHmmss $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + $copyLogsPath = Join-Path $backupPath "CopyLogs" - # Create backup folder if it doesn't exist yet + # Create backup folder and logs folder if it doesn't exist yet New-Item -ItemType Directory -Force -Path $backupPath + New-Item -ItemType Directory -Force -Path $copyLogsPath # Create a backup of original web.config $projectDestinationPath = Split-Path $project.FullName -Parent @@ -19,18 +21,26 @@ if ($project) { # Copy umbraco and umbraco_files from package to project folder # This is only done when these folders already exist because we # only want to do this for upgrades - $umbracoFolder = Join-Path $projectDestinationPath "Umbraco\" + $umbracoFolder = Join-Path $projectDestinationPath "Umbraco" if(Test-Path $umbracoFolder) { $umbracoFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco" - Copy-Item $umbracoFolder $backupPath -Force - robocopy $umbracoFolderSource $umbracoFolder /e /xf UI.xml + + $umbracoBackupPath = Join-Path $backupPath "Umbraco" + New-Item -ItemType Directory -Force -Path $umbracoBackupPath + + robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log + robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log } $umbracoClientFolder = Join-Path $projectDestinationPath "Umbraco_Client" if(Test-Path $umbracoClientFolder) { $umbracoClientFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco_Client" - Copy-Item $umbracoClientFolder $backupPath -Force - robocopy $umbracoFolderSource $umbracoClientFolder /e + + $umbracoClientBackupPath = Join-Path $backupPath "Umbraco_Client" + New-Item -ItemType Directory -Force -Path $umbracoClientBackupPath + + robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log + robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log } # Open readme.txt file diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt new file mode 100644 index 0000000000..ad955e95b4 --- /dev/null +++ b/build/UmbracoVersion.txt @@ -0,0 +1 @@ +7.1.3 \ No newline at end of file diff --git a/src/.nuget/NuGet.exe b/src/.nuget/NuGet.exe index 8f61340295..3ffdd33c61 100644 Binary files a/src/.nuget/NuGet.exe and b/src/.nuget/NuGet.exe differ diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index ebaca4ed81..3c15b52de9 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.1.2"); + private static readonly Version Version = new Version("7.1.3"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 525f75e709..8055b33ab8 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -15,43 +15,13 @@ namespace Umbraco.Core public static IEnumerable> InGroupsOf(this IEnumerable source, int groupSize) { if (source == null) - throw new NullReferenceException("source"); + throw new ArgumentNullException("source"); + if (groupSize <= 0) + throw new ArgumentException("Must be greater than zero.", "groupSize"); - // enumerate the source only once! - return new InGroupsEnumerator(source, groupSize).Groups(); - } - - // this class makes sure that the source is enumerated only ONCE - // which means that when it is enumerated, the actual groups content - // has to be evaluated at the same time, and stored in an array. - private class InGroupsEnumerator - { - private readonly IEnumerator _source; - private readonly int _count; - private bool _mightHaveNext; - - public InGroupsEnumerator(IEnumerable source, int count) - { - _source = source.GetEnumerator(); - _count = count; - _mightHaveNext = true; - } - - public IEnumerable> Groups() - { - while (_mightHaveNext && _source.MoveNext()) - yield return Group().ToArray(); // see note above - } - - private IEnumerable Group() - { - var c = 0; - do - { - yield return _source.Current; - } while (++c < _count && _source.MoveNext()); - _mightHaveNext = c == _count; - } + return source + .Select((x, i) => Tuple.Create(i / groupSize, x)) + .GroupBy(t => t.Item1, t => t.Item2); } /// The distinct by. diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 73b5ed34db..84cf2869e7 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -9,6 +9,8 @@ using System.Linq; using System.Web; using System.Xml; using System.Xml.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -539,7 +541,7 @@ namespace Umbraco.Core.Models return ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); } - + #region Tag methods ///// @@ -567,6 +569,21 @@ namespace Umbraco.Core.Models /// The group/category to assign the tags, the default value is "default" /// public static void SetTags(this IContentBase content, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") + { + content.SetTags(TagCacheStorageType.Csv, propertyTypeAlias, tags, replaceTags, tagGroup); + } + + /// + /// Sets tags for the property - will add tags to the tags table and set the property value to be the comma delimited value of the tags. + /// + /// The content item to assign the tags to + /// The tag storage type in cache (default is csv) + /// The property alias to assign the tags to + /// The tags to assign + /// True to replace the tags on the current property with the tags specified or false to merge them with the currently assigned ones + /// The group/category to assign the tags, the default value is "default" + /// + public static void SetTags(this IContentBase content, TagCacheStorageType storageType, string propertyTypeAlias, IEnumerable tags, bool replaceTags, string tagGroup = "default") { var property = content.Properties[propertyTypeAlias]; if (property == null) @@ -583,15 +600,39 @@ namespace Umbraco.Core.Models //ensure the property value is set to the same thing if (replaceTags) { - property.Value = string.Join(",", trimmedTags); + switch (storageType) + { + case TagCacheStorageType.Csv: + property.Value = string.Join(",", trimmedTags); + break; + case TagCacheStorageType.Json: + //json array + property.Value = JsonConvert.SerializeObject(trimmedTags); + break; + } + } else { - var currTags = property.Value.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + switch (storageType) + { + case TagCacheStorageType.Csv: + var currTags = property.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()); - property.Value = string.Join(",", trimmedTags.Union(currTags)); + property.Value = string.Join(",", trimmedTags.Union(currTags)); + break; + case TagCacheStorageType.Json: + var currJson = JsonConvert.DeserializeObject(property.Value.ToString()); + //need to append the new ones + foreach (var tag in trimmedTags) + { + currJson.Add(tag); + } + //json array + property.Value = JsonConvert.SerializeObject(currJson); + break; + } } - } /// @@ -664,7 +705,7 @@ namespace Umbraco.Core.Models { return ApplicationContext.Current.Services.PackagingService.Export(media, true, raiseEvents: false); } - + /// /// Creates the xml representation for the object /// @@ -687,10 +728,10 @@ 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/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index ff61f0f9a4..ec763d07fa 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -133,26 +133,6 @@ namespace Umbraco.Core.Models Key = Guid.NewGuid(); } - /// - /// Method to call when Entity is being updated - /// - /// Modified Date is set and a new Version guid is set - internal override void UpdatingEntity() - { - base.UpdatingEntity(); - } - - public override object DeepClone() - { - var clone = (ContentType)base.DeepClone(); - var propertyGroups = PropertyGroups.Select(x => (PropertyGroup)x.DeepClone()).ToList(); - clone.PropertyGroups = new PropertyGroupCollection(propertyGroups); - //set the property types that are not part of a group - clone.PropertyTypes = PropertyTypeCollection - .Where(x => x.PropertyGroupId == null) - .Select(x => (PropertyType)x.DeepClone()).ToList(); - return clone; - } /// /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 00e9c1e30c..5c09deadb8 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -331,11 +331,6 @@ namespace Umbraco.Core.Models get { return _additionalData; } } - /// - /// Some entities may expose additional data that other's might not, this custom data will be available in this collection - /// - public IDictionary AdditionalData { get; private set; } - /// /// Gets or sets a list of integer Ids for allowed ContentTypes /// @@ -356,7 +351,9 @@ namespace Umbraco.Core.Models /// /// List of PropertyGroups available on this ContentType /// - /// A PropertyGroup corresponds to a Tab in the UI + /// + /// A PropertyGroup corresponds to a Tab in the UI + /// [DataMember] public virtual PropertyGroupCollection PropertyGroups { @@ -372,7 +369,13 @@ namespace Umbraco.Core.Models /// List of PropertyTypes available on this ContentType. /// This list aggregates PropertyTypes across the PropertyGroups. /// + /// + /// Marked as DoNotClone because the result of this property is not the natural result of the data, it is + /// a union of data so when auto-cloning if the setter is used it will be setting the unnatural result of the + /// data. We manually clone this instead. + /// [IgnoreDataMember] + [DoNotClone] public virtual IEnumerable PropertyTypes { get @@ -388,6 +391,14 @@ namespace Umbraco.Core.Models } /// + /// Returns the property type collection containing types that are non-groups - used for tests + /// + internal IEnumerable NonGroupedPropertyTypes + { + get { return _propertyTypes; } + } + + /// /// A boolean flag indicating if a property type has been removed from this instance. /// /// @@ -584,5 +595,19 @@ namespace Umbraco.Core.Models propertyType.ResetDirtyProperties(); } } + + public override object DeepClone() + { + var clone = (ContentTypeBase)base.DeepClone(); + + //need to manually wire up the event handlers for the property type collections - we've ensured + // its ignored from the auto-clone process because its return values are unions, not raw and + // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 + + clone._propertyTypes = (PropertyTypeCollection)_propertyTypes.DeepClone(); + clone._propertyTypes.CollectionChanged += clone.PropertyTypesChanged; + + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index aaee03f963..d8ef10751b 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -2,9 +2,29 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Umbraco.Core.Models { + /// + /// Used to attribute properties that have a setter and are a reference type + /// that should be ignored for cloning when using the DeepCloneHelper + /// + /// + /// + /// This attribute must be used: + /// * when the property is backed by a field but the result of the property is the un-natural data stored in the field + /// + /// This attribute should not be used: + /// * when the property is virtual + /// * when the setter performs additional required logic other than just setting the underlying field + /// + /// + internal class DoNotCloneAttribute : Attribute + { + + } + public static class DeepCloneHelper { /// @@ -25,8 +45,10 @@ namespace Umbraco.Core.Models 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) + && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) //settable && x.CanWrite //non-indexed diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index b7d054c676..315e2697c0 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -240,52 +240,6 @@ namespace Umbraco.Core.Models.EntityBase DeepCloneHelper.DeepCloneRefProperties(this, clone); clone.ResetDirtyProperties(false); return clone; - - //Using data contract serializer - has issues - - //var s = Serialize(this); - //var d = Deserialize(s, this.GetType()); - //return d; - - //Using binary serializer - has issues - - //using (var memoryStream = new MemoryStream(10)) - //{ - //IFormatter formatter = new BinaryFormatter(); - //formatter.Serialize(memoryStream, this); - //memoryStream.Seek(0, SeekOrigin.Begin); - //return formatter.Deserialize(memoryStream); - //} } - - // serialize/deserialize with data contracts: - - //public static string Serialize(object obj) - //{ - // using (var memoryStream = new MemoryStream()) - // using (var reader = new StreamReader(memoryStream)) - // { - // var serializer = new DataContractSerializer(obj.GetType()); - // serializer.WriteObject(memoryStream, obj); - // memoryStream.Position = 0; - // return reader.ReadToEnd(); - // } - //} - - //public static object Deserialize(string xml, Type toType) - //{ - // using (Stream stream = new MemoryStream()) - // { - // using (var writer = new StreamWriter(stream, Encoding.UTF8)) - // { - // writer.Write(xml); - // //byte[] data = Encoding.UTF8.GetBytes(xml); - // //stream.Write(data, 0, data.Length); - // stream.Position = 0; - // var deserializer = new DataContractSerializer(toType); - // return deserializer.ReadObject(stream); - // } - // } - //} } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs index 9a357b3b38..8e95a1a7e2 100644 --- a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs +++ b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Models.EntityBase /// /// Tracks the properties that have changed /// - private readonly IDictionary _propertyChangedInfo = new Dictionary(); + private IDictionary _propertyChangedInfo = new Dictionary(); /// /// Tracks the properties that we're changed before the last commit (or last call to ResetDirtyProperties) @@ -86,7 +86,9 @@ namespace Umbraco.Core.Models.EntityBase /// public void ForgetPreviouslyDirtyProperties() { - _lastPropertyChangedInfo.Clear(); + //NOTE: We cannot .Clear() because when we memberwise clone this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _lastPropertyChangedInfo = new Dictionary(); } /// @@ -119,7 +121,9 @@ namespace Umbraco.Core.Models.EntityBase _lastPropertyChangedInfo = _propertyChangedInfo.ToDictionary(v => v.Key, v => v.Value); } - _propertyChangedInfo.Clear(); + //NOTE: We cannot .Clear() because when we memberwise clone this will be the SAME + // instance as the one on the clone, so we need to create a new instance. + _propertyChangedInfo = new Dictionary(); } /// diff --git a/src/Umbraco.Core/Models/IMacroProperty.cs b/src/Umbraco.Core/Models/IMacroProperty.cs index 551d60304c..a6c1d9ca5f 100644 --- a/src/Umbraco.Core/Models/IMacroProperty.cs +++ b/src/Umbraco.Core/Models/IMacroProperty.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Models /// /// Defines a Property for a Macro /// - public interface IMacroProperty : IValueObject + public interface IMacroProperty : IValueObject, IDeepCloneable, IRememberBeingDirty { [DataMember] int Id { get; set; } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index d81f49fc4f..eb8e0b7a66 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -109,9 +109,9 @@ namespace Umbraco.Core.Models private string _scriptAssembly; private string _scriptPath; private string _xslt; - private readonly MacroPropertyCollection _properties; - private readonly List _addedProperties; - private readonly List _removedProperties; + private MacroPropertyCollection _properties; + private List _addedProperties; + private List _removedProperties; private static readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); private static readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); @@ -138,12 +138,11 @@ namespace Umbraco.Core.Models var alias = prop.Alias; - //remove from the removed/added props (since people could add/remove all they want in one request) - _removedProperties.RemoveAll(s => s == alias); - _addedProperties.RemoveAll(s => s == alias); - - //add to the added props - _addedProperties.Add(alias); + if (_addedProperties.Contains(alias) == false) + { + //add to the added props + _addedProperties.Add(alias); + } } else if (e.Action == NotifyCollectionChangedAction.Remove) { @@ -153,12 +152,10 @@ namespace Umbraco.Core.Models var alias = prop.Alias; - //remove from the removed/added props (since people could add/remove all they want in one request) - _removedProperties.RemoveAll(s => s == alias); - _addedProperties.RemoveAll(s => s == alias); - - //add to the added props - _removedProperties.Add(alias); + if (_removedProperties.Contains(alias) == false) + { + _removedProperties.Add(alias); + } } } @@ -397,7 +394,20 @@ namespace Umbraco.Core.Models { get { return _properties; } } - - + + public override object DeepClone() + { + var clone = (Macro)base.DeepClone(); + + clone._addedProperties = new List(); + clone._removedProperties = new List(); + clone._properties = (MacroPropertyCollection)Properties.DeepClone(); + //re-assign the event handler + clone._properties.CollectionChanged += clone.PropertiesChanged; + + clone.ResetDirtyProperties(false); + + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MacroProperty.cs b/src/Umbraco.Core/Models/MacroProperty.cs index ef0a024973..17fef44a87 100644 --- a/src/Umbraco.Core/Models/MacroProperty.cs +++ b/src/Umbraco.Core/Models/MacroProperty.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class MacroProperty : TracksChangesEntityBase, IMacroProperty, IRememberBeingDirty + public class MacroProperty : TracksChangesEntityBase, IMacroProperty, IRememberBeingDirty, IDeepCloneable { public MacroProperty() { @@ -176,5 +176,37 @@ namespace Umbraco.Core.Models }, _editorAlias, PropertyTypeSelector); } } + + public object DeepClone() + { + //Memberwise clone on MacroProperty will work since it doesn't have any deep elements + // for any sub class this will work for standard properties as well that aren't complex object's themselves. + var clone = (MacroProperty)MemberwiseClone(); + //Automatically deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); + clone.ResetDirtyProperties(false); + return clone; + } + + protected bool Equals(MacroProperty other) + { + return string.Equals(_alias, other._alias) && _id == other._id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MacroProperty) obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((_alias != null ? _alias.GetHashCode() : 0)*397) ^ _id; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MacroPropertyCollection.cs b/src/Umbraco.Core/Models/MacroPropertyCollection.cs index 6d0c762154..de23d60e7c 100644 --- a/src/Umbraco.Core/Models/MacroPropertyCollection.cs +++ b/src/Umbraco.Core/Models/MacroPropertyCollection.cs @@ -8,13 +8,60 @@ namespace Umbraco.Core.Models /// /// A macro's property collection /// - public class MacroPropertyCollection : ObservableDictionary + public class MacroPropertyCollection : ObservableDictionary, IDeepCloneable { public MacroPropertyCollection() : base(property => property.Alias) { } + public object DeepClone() + { + var clone = new MacroPropertyCollection(); + foreach (var item in this) + { + clone.Add((IMacroProperty)item.DeepClone()); + } + return clone; + } + + /// + /// Used to update an existing macro property + /// + /// + /// + /// + /// + /// The existing property alias + /// + /// + public void UpdateProperty(string currentAlias, string name = null, int? sortOrder = null, string editorAlias = null, string newAlias = null) + { + var prop = this[currentAlias]; + if (prop == null) + { + throw new InvalidOperationException("No property exists with alias " + currentAlias); + } + + if (name.IsNullOrWhiteSpace() == false) + { + prop.Name = name; + } + if (sortOrder.HasValue) + { + prop.SortOrder = sortOrder.Value; + } + if (name.IsNullOrWhiteSpace() == false) + { + prop.EditorAlias = editorAlias; + } + + if (newAlias.IsNullOrWhiteSpace() == false && currentAlias != newAlias) + { + prop.Alias = newAlias; + ChangeKey(currentAlias, newAlias); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 9c710f4375..ba9b89d779 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -59,9 +59,9 @@ namespace Umbraco.Core.Models.Membership private IUserType _userType; private string _name; private Type _userTypeKey; - private readonly List _addedSections; - private readonly List _removedSections; - private readonly ObservableCollection _sectionCollection; + private List _addedSections; + private List _removedSections; + private ObservableCollection _sectionCollection; private int _sessionTimeout; private int _startContentId; private int _startMediaId; @@ -244,7 +244,10 @@ namespace Umbraco.Core.Models.Membership public void RemoveAllowedSection(string sectionAlias) { - _sectionCollection.Remove(sectionAlias); + if (_sectionCollection.Contains(sectionAlias)) + { + _sectionCollection.Remove(sectionAlias); + } } public void AddAllowedSection(string sectionAlias) @@ -423,24 +426,41 @@ namespace Umbraco.Core.Models.Membership if (e.Action == NotifyCollectionChangedAction.Add) { - //remove from the removed/added sections (since people could add/remove all they want in one request) - _removedSections.RemoveAll(s => s == e.NewItems.Cast().First()); - _addedSections.RemoveAll(s => s == e.NewItems.Cast().First()); + var item = e.NewItems.Cast().First(); - //add to the added sections - _addedSections.Add(e.NewItems.Cast().First()); + if (_addedSections.Contains(item) == false) + { + _addedSections.Add(item); + } } else if (e.Action == NotifyCollectionChangedAction.Remove) { - //remove from the removed/added sections (since people could add/remove all they want in one request) - _removedSections.RemoveAll(s => s == e.OldItems.Cast().First()); - _addedSections.RemoveAll(s => s == e.OldItems.Cast().First()); + var item = e.OldItems.Cast().First(); + + if (_removedSections.Contains(item) == false) + { + _removedSections.Add(item); + } - //add to the added sections - _removedSections.Add(e.OldItems.Cast().First()); } } - + + public override object DeepClone() + { + var clone = (User)base.DeepClone(); + + //need to create new collections otherwise they'll get copied by ref + clone._addedSections = new List(); + clone._removedSections = new List(); + clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); + //re-create the event handler + clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; + + clone.ResetDirtyProperties(false); + + return clone; + } + /// /// Internal class used to wrap the user in a profile /// diff --git a/src/Umbraco.Core/Models/Notification.cs b/src/Umbraco.Core/Models/Notification.cs index e5d3236b5e..351b60039e 100644 --- a/src/Umbraco.Core/Models/Notification.cs +++ b/src/Umbraco.Core/Models/Notification.cs @@ -5,7 +5,7 @@ using System.Text; namespace Umbraco.Core.Models { - internal class Notification + public class Notification { public Notification(int entityId, int userId, string action, Guid entityType) { diff --git a/src/Umbraco.Core/Models/PreValue.cs b/src/Umbraco.Core/Models/PreValue.cs index b12312130d..61d127f50a 100644 --- a/src/Umbraco.Core/Models/PreValue.cs +++ b/src/Umbraco.Core/Models/PreValue.cs @@ -3,7 +3,7 @@ /// /// Represents a stored pre-value field value /// - public class PreValue + public class PreValue : IDeepCloneable { public PreValue(int id, string value, int sortOrder) { @@ -37,5 +37,12 @@ /// The sort order stored for the pre-value field value /// public int SortOrder { get; private set; } + + public virtual object DeepClone() + { + //Memberwise clone on PreValue will work since it doesn't have any deep elements + var clone = (PreValue)MemberwiseClone(); + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PreValueCollection.cs b/src/Umbraco.Core/Models/PreValueCollection.cs index e6d864cd91..449eaaa3b7 100644 --- a/src/Umbraco.Core/Models/PreValueCollection.cs +++ b/src/Umbraco.Core/Models/PreValueCollection.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Models /// Most legacy property editors won't support the dictionary format but new property editors should always use the dictionary format. /// In order to get overrideable pre-values working we need a dictionary since we'll have to reference a pre-value by a key. /// - public class PreValueCollection + public class PreValueCollection : IDeepCloneable { private IDictionary _preValuesAsDictionary; private IEnumerable _preValuesAsArray; @@ -82,5 +82,21 @@ namespace Umbraco.Core.Models } return result; } + + public object DeepClone() + { + var clone = (PreValueCollection) MemberwiseClone(); + if (_preValuesAsArray != null) + { + clone._preValuesAsArray = _preValuesAsArray.Select(x => (PreValue)x.DeepClone()).ToArray(); + } + if (_preValuesAsDictionary != null) + { + clone._preValuesAsDictionary = _preValuesAsDictionary.ToDictionary(x => x.Key, x => (PreValue)x.Value.DeepClone()); + } + + + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyTagBehavior.cs b/src/Umbraco.Core/Models/PropertyTagBehavior.cs new file mode 100644 index 0000000000..0a435ebf95 --- /dev/null +++ b/src/Umbraco.Core/Models/PropertyTagBehavior.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Models +{ + internal enum PropertyTagBehavior + { + Replace, + Remove, + Merge + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyTags.cs b/src/Umbraco.Core/Models/PropertyTags.cs index d64a1c4edf..6075502131 100644 --- a/src/Umbraco.Core/Models/PropertyTags.cs +++ b/src/Umbraco.Core/Models/PropertyTags.cs @@ -33,11 +33,4 @@ namespace Umbraco.Core.Models public IEnumerable> Tags { get; set; } } - - internal enum PropertyTagBehavior - { - Replace, - Remove, - Merge - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 676eb05ef0..a0fa7932b5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -44,6 +44,11 @@ namespace Umbraco.Core.Models.PublishedContent InitializeIndexes(); } + // create detached content type - ie does not match anything in the DB + internal PublishedContentType(string alias, IEnumerable propertyTypes) + : this (0, alias, propertyTypes) + { } + private void InitializeIndexes() { for (var i = 0; i < _propertyTypes.Length; i++) diff --git a/src/Umbraco.Core/Models/TagCacheStorageType.cs b/src/Umbraco.Core/Models/TagCacheStorageType.cs new file mode 100644 index 0000000000..5078a44d85 --- /dev/null +++ b/src/Umbraco.Core/Models/TagCacheStorageType.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Models +{ + public enum TagCacheStorageType + { + Csv, + Json + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 92bdf3c790..23a6bcfe5b 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models @@ -47,13 +48,11 @@ namespace Umbraco.Core.Models public UmbracoEntity() { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); } public UmbracoEntity(bool trashed) { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); Trashed = trashed; } @@ -61,7 +60,6 @@ namespace Umbraco.Core.Models public UmbracoEntity(UInt64 trashed) { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); Trashed = trashed == 1; } @@ -287,15 +285,31 @@ namespace Umbraco.Core.Models } } + public override object DeepClone() + { + var clone = (UmbracoEntity) base.DeepClone(); + + //This ensures that any value in the dictionary that is deep cloneable is cloned too + foreach (var key in clone.AdditionalData.Keys.ToArray()) + { + var deepCloneable = clone.AdditionalData[key] as IDeepCloneable; + if (deepCloneable != null) + { + clone.AdditionalData[key] = deepCloneable.DeepClone(); + } + } + + return clone; + } + /// - /// Some entities may expose additional data that other's might not, this custom data will be available in this collection + /// A struction that can be contained in the additional data of an UmbracoEntity representing + /// a user defined property /// - public IList UmbracoProperties { get; set; } - - internal class UmbracoProperty : IDeepCloneable + public class EntityProperty : IDeepCloneable { public string PropertyEditorAlias { get; set; } - public string Value { get; set; } + public object Value { get; set; } public object DeepClone() { //Memberwise clone on Entity will work since it doesn't have any deep elements @@ -304,7 +318,7 @@ namespace Umbraco.Core.Models return clone; } - protected bool Equals(UmbracoProperty other) + protected bool Equals(EntityProperty other) { return PropertyEditorAlias.Equals(other.PropertyEditorAlias) && string.Equals(Value, other.Value); } @@ -314,7 +328,7 @@ namespace Umbraco.Core.Models if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((UmbracoProperty) obj); + return Equals((EntityProperty) obj); } public override int GetHashCode() diff --git a/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs b/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs index 016a9c8f11..f2223d338e 100644 --- a/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs +++ b/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs @@ -49,6 +49,7 @@ namespace Umbraco.Core.ObjectResolution /// Initializes a new instance of the class with an initial list /// If is per HttpRequest then there must be a current HttpContext. /// is per HttpRequest but the current HttpContext is null. + /// protected LazyManyObjectsResolverBase(IEnumerable> lazyTypeList, ObjectLifetimeScope scope = ObjectLifetimeScope.Application) : this(scope) { @@ -108,7 +109,7 @@ namespace Umbraco.Core.ObjectResolution /// Gets a value indicating whether the resolver has resolved types to create instances from. /// /// To be used in unit tests. - internal bool HasResolvedTypes + public bool HasResolvedTypes { get { diff --git a/src/Umbraco.Core/ObjectResolution/ResolverBase.cs b/src/Umbraco.Core/ObjectResolution/ResolverBase.cs index d6bf59351d..4b3b21082c 100644 --- a/src/Umbraco.Core/ObjectResolution/ResolverBase.cs +++ b/src/Umbraco.Core/ObjectResolution/ResolverBase.cs @@ -85,10 +85,9 @@ namespace Umbraco.Core.ObjectResolution } /// - /// Gets a value indicating whether a the singleton nstance has been set. + /// Gets a value indicating whether a the singleton instance has been set. /// - /// To be used in unit tests. - internal static bool HasCurrent + public static bool HasCurrent { get { @@ -102,9 +101,13 @@ namespace Umbraco.Core.ObjectResolution /// /// Resets the resolver singleton instance to null. /// - /// To be used in unit tests. - /// By default this is true because we always need to reset resolution before we reset a resolver, however in some insanely rare cases like unit testing you might not want to do this. - internal static void Reset(bool resetResolution = true) + /// + /// To be used in unit tests. DO NOT USE THIS DURING PRODUCTION. + /// + /// + /// By default this is true because we always need to reset resolution before we reset a resolver, however in some insanely rare cases like unit testing you might not want to do this. + /// + protected internal static void Reset(bool resetResolution = true) { //In order to reset a resolver, we always must reset the resolution diff --git a/src/Umbraco.Core/ObservableDictionary.cs b/src/Umbraco.Core/ObservableDictionary.cs index b665071386..7e988e2b05 100644 --- a/src/Umbraco.Core/ObservableDictionary.cs +++ b/src/Umbraco.Core/ObservableDictionary.cs @@ -135,6 +135,28 @@ namespace Umbraco.Core } + /// + /// Allows us to change the key of an item + /// + /// + /// + public virtual void ChangeKey(TKey currentKey, TKey newKey) + { + if (!Indecies.ContainsKey(currentKey)) + { + throw new InvalidOperationException("No item with the key " + currentKey + "was found in the collection"); + } + if (ContainsKey(newKey)) + { + throw new DuplicateKeyException(newKey.ToString()); + } + + var currentIndex = Indecies[currentKey]; + + Indecies.Remove(currentKey); + Indecies.Add(newKey, currentIndex); + } + internal class DuplicateKeyException : Exception { diff --git a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs index c2ca53a240..acacadb0cc 100644 --- a/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Persistence/Caching/RuntimeCacheProvider.cs @@ -128,9 +128,9 @@ namespace Umbraco.Core.Persistence.Caching public void Save(Type type, IEntity entity) { //IMPORTANT: we must clone to store, see: http://issues.umbraco.org/issue/U4-4259 - entity = (IEntity)entity.DeepClone(); + var clone = (IEntity)entity.DeepClone(); - var key = GetCompositeId(type, entity.Id); + var key = GetCompositeId(type, clone.Id); _keyTracker.TryAdd(key); @@ -139,11 +139,11 @@ namespace Umbraco.Core.Persistence.Caching if (_memoryCache != null) { - _memoryCache.Set(key, entity, new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(5) }); + _memoryCache.Set(key, clone, new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(5) }); } else { - HttpRuntime.Cache.Insert(key, entity, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(5)); + HttpRuntime.Cache.Insert(key, clone, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(5)); } } diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index db1a99bc45..f33ae1af2f 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -44,7 +44,6 @@ namespace Umbraco.Core.Persistence.Factories ContentTypeAlias = asDictionary.ContainsKey("alias") ? (d.alias ?? string.Empty) : string.Empty, ContentTypeIcon = asDictionary.ContainsKey("icon") ? (d.icon ?? string.Empty) : string.Empty, ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty, - UmbracoProperties = new List() }; var publishedVersion = default(Guid); @@ -87,7 +86,6 @@ namespace Umbraco.Core.Persistence.Factories ContentTypeAlias = dto.Alias ?? string.Empty, ContentTypeIcon = dto.Icon ?? string.Empty, ContentTypeThumbnail = dto.Thumbnail ?? string.Empty, - UmbracoProperties = new List() }; entity.IsPublished = dto.PublishedVersion != default(Guid) || (dto.NewestVersion != default(Guid) && dto.PublishedVersion == dto.NewestVersion); @@ -98,12 +96,13 @@ namespace Umbraco.Core.Persistence.Factories { foreach (var propertyDto in dto.UmbracoPropertyDtos) { - entity.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty - { - PropertyEditorAlias = - propertyDto.PropertyEditorAlias, - Value = propertyDto.UmbracoFile - }); + entity.AdditionalData[propertyDto.PropertyAlias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = propertyDto.PropertyEditorAlias, + Value = propertyDto.NTextValue.IsNullOrWhiteSpace() + ? propertyDto.NVarcharValue + : propertyDto.NTextValue.ConvertToJsonIfPossible() + }; } } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 51de2b2cab..305c630a71 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -72,7 +72,7 @@ namespace Umbraco.Core.Persistence.Factories { AppAlias = app }; - if (entity.Id != null) + if (entity.HasIdentity) { appDto.UserId = (int) entity.Id; } diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 9484121ad7..0e8ae8c3bc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -295,8 +295,8 @@ AND umbracoNode.id <> @id", var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); if (cached != null && cached.Any()) { - //return from the cache - return cached.First(); + //return from the cache, ensure it's a cloned result + return (PreValueCollection)cached.First().DeepClone(); } l.UpgradeToWriteLock(); diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 7336e36ab7..d0b81f1b8e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -66,15 +66,31 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); + var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); + + if (isMedia) + { + //for now treat media differently + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( + new UmbracoEntityRelator().Map, sql); - return entity; + return entities.FirstOrDefault(); + } + else + { + var nodeDto = _work.Database.FirstOrDefault(sql); + if (nodeDto == null) + return null; + + var factory = new UmbracoEntityFactory(); + var entity = factory.BuildEntityFromDynamic(nodeDto); + + return entity; + } + + } public virtual IUmbracoEntity Get(int id) @@ -94,16 +110,31 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; + var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); + + if (isMedia) + { + //for now treat media differently + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( + new UmbracoEntityRelator().Map, sql); - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); + return entities.FirstOrDefault(); + } + else + { + var nodeDto = _work.Database.FirstOrDefault(sql); + if (nodeDto == null) + return null; - return entity; + var factory = new UmbracoEntityFactory(); + var entity = factory.BuildEntityFromDynamic(nodeDto); + + return entity; + } + + } public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) @@ -119,8 +150,8 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, string.Empty, objectTypeId).Append(GetGroupBy(isContent, isMedia)); - + var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, string.Empty); + var factory = new UmbracoEntityFactory(); if (isMedia) @@ -166,24 +197,28 @@ namespace Umbraco.Core.Persistence.Repositories bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var wheres = string.Concat(" AND ", string.Join(" AND ", ((Query)query).WhereClauses())); + var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, wheres, objectTypeId); + var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().Append(GetGroupBy(isContent, isMedia)); + var entitySql = translator.Translate(); var factory = new UmbracoEntityFactory(); if (isMedia) { + var mediaSql = GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false)), wheres); + //treat media differently for now //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields var entities = _work.Database.Fetch( - new UmbracoEntityRelator().Map, sql); + new UmbracoEntityRelator().Map, mediaSql); return entities; } else { //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData - var dtos = _work.Database.Fetch(sql); + var dtos = _work.Database.Fetch(entitySql.Append(GetGroupBy(isContent, false))); return dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); } } @@ -193,6 +228,62 @@ namespace Umbraco.Core.Persistence.Repositories #region Sql Statements + protected Sql GetFullSqlForEntityType(Guid key, bool isContent, bool isMedia, Guid objectTypeId) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + protected Sql GetFullSqlForEntityType(int id, bool isContent, bool isMedia, Guid objectTypeId) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectTypeId, string additionalWhereClause) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, additionalWhereClause, objectTypeId); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + private Sql GetFullSqlForMedia(Sql entitySql, string additionWhereStatement = "") + { + //this will add any dataNvarchar property to the output which can be added to the additional properties + + var joinSql = new Sql() + .Select("contentNodeId, versionId, dataNvarchar, dataNtext, propertyEditorAlias, alias as propertyTypeAlias") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .InnerJoin() + .On(dto => dto.Id, dto => dto.PropertyTypeId) + .InnerJoin() + .On(dto => dto.DataTypeId, dto => dto.DataTypeId) + .Where("umbracoNode.nodeObjectType = @nodeObjectType" + additionWhereStatement, new {nodeObjectType = Constants.ObjectTypes.Media}); + + //We're going to create a query to query against the entity SQL + // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join + // the entitySql query, we have to join the wrapped query to get the ntext in the result + + var wrappedSql = new Sql("SELECT * FROM (") + .Append(entitySql) + .Append(new Sql(") tmpTbl LEFT JOIN (")) + .Append(joinSql) + .Append(new Sql(") as property ON id = property.contentNodeId")) + .OrderBy("sortOrder"); + + return wrappedSql; + } + protected virtual Sql GetBase(bool isContent, bool isMedia, string additionWhereStatement = "") { var columns = new List @@ -221,13 +312,9 @@ namespace Umbraco.Core.Persistence.Repositories columns.Add("contenttype.isContainer"); } - if (isMedia) - { - columns.Add("property.dataNvarchar as umbracoFile"); - columns.Add("property.propertyEditorAlias"); - } + //Creates an SQL query to return a single row for the entity - var sql = new Sql() + var entitySql = new Sql() .Select(columns.ToArray()) .From("umbracoNode umbracoNode") .LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); @@ -235,7 +322,7 @@ namespace Umbraco.Core.Persistence.Repositories if (isContent || isMedia) { - sql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") + entitySql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") .LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType") .LeftJoin( "(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1 GROUP BY nodeId, versionId) as published") @@ -245,18 +332,7 @@ namespace Umbraco.Core.Persistence.Repositories .On("umbracoNode.id = latest.nodeId"); } - if (isMedia) - { - sql.LeftJoin( - "(SELECT contentNodeId, versionId, dataNvarchar, propertyEditorAlias FROM cmsPropertyData " + - "INNER JOIN umbracoNode ON cmsPropertyData.contentNodeId = umbracoNode.id " + - "INNER JOIN cmsPropertyType ON cmsPropertyType.id = cmsPropertyData.propertytypeid " + - "INNER JOIN cmsDataType ON cmsPropertyType.dataTypeId = cmsDataType.nodeId "+ - "WHERE umbracoNode.nodeObjectType = '" + Constants.ObjectTypes.Media + "'" + additionWhereStatement + ") as property") - .On("umbracoNode.id = property.contentNodeId"); - } - - return sql; + return entitySql; } protected virtual Sql GetBaseWhere(Func baseQuery, bool isContent, bool isMedia, string additionWhereStatement, Guid nodeObjectType) @@ -298,7 +374,7 @@ namespace Umbraco.Core.Persistence.Repositories return sql; } - protected virtual Sql GetGroupBy(bool isContent, bool isMedia) + protected virtual Sql GetGroupBy(bool isContent, bool isMedia, bool includeSort = true) { var columns = new List { @@ -324,19 +400,18 @@ namespace Umbraco.Core.Persistence.Repositories columns.Add("contenttype.thumbnail"); columns.Add("contenttype.isContainer"); } + + var sql = new Sql() + .GroupBy(columns.ToArray()); - if (isMedia) + if (includeSort) { - columns.Add("property.dataNvarchar"); - columns.Add("property.propertyEditorAlias"); + sql = sql.OrderBy("umbracoNode.sortOrder"); } - var sql = new Sql() - .GroupBy(columns.ToArray()) - .OrderBy("umbracoNode.sortOrder"); return sql; } - + #endregion /// @@ -377,15 +452,21 @@ namespace Umbraco.Core.Persistence.Repositories [ResultColumn] public List UmbracoPropertyDtos { get; set; } } - + [ExplicitColumns] internal class UmbracoPropertyDto { [Column("propertyEditorAlias")] public string PropertyEditorAlias { get; set; } - [Column("umbracoFile")] - public string UmbracoFile { get; set; } + [Column("propertyTypeAlias")] + public string PropertyAlias { get; set; } + + [Column("dataNvarchar")] + public string NVarcharValue { get; set; } + + [Column("dataNtext")] + public string NTextValue { get; set; } } /// @@ -412,16 +493,18 @@ namespace Umbraco.Core.Persistence.Repositories // Is this the same UmbracoEntity as the current one we're processing if (Current != null && Current.Key == a.uniqueID) { - // Yes, just add this UmbracoProperty to the current UmbracoEntity's collection - if (Current.UmbracoProperties == null) + if (p != null && p.PropertyAlias.IsNullOrWhiteSpace() == false) { - Current.UmbracoProperties = new List(); - } - Current.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty + // Add this UmbracoProperty to the current additional data + Current.AdditionalData[p.PropertyAlias] = new UmbracoEntity.EntityProperty { PropertyEditorAlias = p.PropertyEditorAlias, - Value = p.UmbracoFile - }); + Value = p.NTextValue.IsNullOrWhiteSpace() + ? p.NVarcharValue + : p.NTextValue.ConvertToJsonIfPossible() + }; + } + // Return null to indicate we're not done with this UmbracoEntity yet return null; } @@ -436,15 +519,17 @@ namespace Umbraco.Core.Persistence.Repositories Current = _factory.BuildEntityFromDynamic(a); - //add the property/create the prop list if null - Current.UmbracoProperties = new List + if (p != null && p.PropertyAlias.IsNullOrWhiteSpace() == false) + { + //add the property/create the prop list if null + Current.AdditionalData[p.PropertyAlias] = new UmbracoEntity.EntityProperty { - new UmbracoEntity.UmbracoProperty - { - PropertyEditorAlias = p.PropertyEditorAlias, - Value = p.UmbracoFile - } + PropertyEditorAlias = p.PropertyEditorAlias, + Value = p.NTextValue.IsNullOrWhiteSpace() + ? p.NVarcharValue + : p.NTextValue.ConvertToJsonIfPossible() }; + } // Return the now populated previous UmbracoEntity (or null if first time through) return prev; diff --git a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs index 805177be9a..c8ffcbe68a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MacroRepository.cs @@ -165,10 +165,18 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(dto); - //update the sections if they've changed + //update the properties if they've changed var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties")) + if (macro.IsPropertyDirty("Properties") || macro.Properties.Any(x => x.IsDirty())) { + //now we need to delete any props that have been removed + foreach (var propAlias in macro.RemovedProperties) + { + //delete the property + Database.Delete("WHERE macro=@macroId AND macroPropertyAlias=@propAlias", + new { macroId = macro.Id, propAlias = propAlias }); + } + //for any that exist on the object, we need to determine if we need to update or insert foreach (var propDto in dto.MacroPropertyDtos) { @@ -180,17 +188,15 @@ namespace Umbraco.Core.Persistence.Repositories } else { - Database.Update(propDto); + //only update if it's dirty + if (macro.Properties[propDto.Alias].IsDirty()) + { + Database.Update(propDto); + } } } - //now we need to delete any props that have been removed - foreach (var propAlias in macro.RemovedProperties) - { - //delete the property - Database.Delete("WHERE macro=@macroId AND macroPropertyAlias=@propAlias", - new { macroId = macro.Id, propAlias = propAlias }); - } + } ((ICanBeDirty)entity).ResetDirtyProperties(); diff --git a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs index 1d11f8d3d4..44d6a6f369 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagsRepository.cs @@ -285,7 +285,7 @@ namespace Umbraco.Core.Persistence.Repositories if (group.IsNullOrWhiteSpace() == false) { sql = sql.Where(dto => dto.Group == group); - } + } var factory = new TagFactory(); @@ -329,7 +329,7 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE: There's some very clever logic in the umbraco.cms.businesslogic.Tags.Tag to insert tags where they don't exist, // and assign where they don't exist which we've borrowed here. The queries are pretty zany but work, otherwise we'll end up // with quite a few additional queries. - + //do all this in one transaction using (var trans = Database.GetTransaction()) { @@ -397,7 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories public void RemoveTagsFromProperty(int contentId, int propertyTypeId, IEnumerable tags) { var tagSetSql = GetTagSet(tags); - + var deleteSql = string.Concat("DELETE FROM cmsTagRelationship WHERE nodeId = ", contentId, " AND propertyTypeId = ", @@ -439,7 +439,11 @@ namespace Umbraco.Core.Persistence.Repositories /// private static string GetTagSet(IEnumerable tagsToInsert) { - var array = tagsToInsert.Select(tag => string.Format("select '{0}' as Tag, '{1}' as [Group]", tag.Text.Replace("'", "''"), tag.Group)).ToArray(); + var array = tagsToInsert + .Select(tag => + string.Format("select '{0}' as Tag, '{1}' as [Group]", + PetaPocoExtensions.EscapeAtSymbols(tag.Text.Replace("'", "''")), tag.Group)) + .ToArray(); return "(" + string.Join(" union ", array).Replace(" ", " ") + ") as TagSet"; } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index d7e9289211..0ed46c039c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -158,7 +158,7 @@ namespace Umbraco.Core.Persistence.Repositories { "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "UPDATE cmsDocument SET templateId = NULL WHERE nodeId = @Id", + "UPDATE cmsDocument SET templateId = NULL WHERE templateId = @Id", "DELETE FROM cmsDocumentType WHERE templateNodeId = @Id", "DELETE FROM cmsTemplate WHERE nodeId = @Id", "DELETE FROM umbracoNode WHERE id = @Id" diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 34775df456..67775cca54 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -210,9 +210,19 @@ namespace Umbraco.Core.Persistence.Repositories var user = (User)entity; if (user.IsPropertyDirty("AllowedSections")) { + //now we need to delete any applications that have been removed + foreach (var section in user.RemovedSections) + { + //we need to manually delete thsi record because it has a composite key + Database.Delete("WHERE app=@Section AND " + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("user") + "=@UserId", + new { Section = section, UserId = (int)user.Id }); + } + //for any that exist on the object, we need to determine if we need to update or insert + //NOTE: the User2AppDtos collection wil always be equal to the User.AllowedSections foreach (var sectionDto in userDto.User2AppDtos) { + //if something has been added then insert it if (user.AddedSections.Contains(sectionDto.AppAlias)) { //we need to insert since this was added @@ -226,13 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories } } - //now we need to delete any applications that have been removed - foreach (var section in user.RemovedSections) - { - //we need to manually delete thsi record because it has a composite key - Database.Delete("WHERE app=@Section AND " + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("user") + "=@UserId", - new { Section = section, UserId = (int)user.Id }); - } + } ((ICanBeDirty)entity).ResetDirtyProperties(); diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index 2f0f7fcec6..d03edc0fc6 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -44,9 +44,9 @@ namespace Umbraco.Core /// file is cached temporarily until app startup completes. /// /// - /// - internal PluginManager(ApplicationContext appContext, bool detectBinChanges = true) - : this(detectBinChanges) + /// + internal PluginManager(ApplicationContext appContext, bool detectChanges = true) + : this(detectChanges) { if (appContext == null) throw new ArgumentNullException("appContext"); _appContext = appContext; @@ -55,11 +55,11 @@ namespace Umbraco.Core /// /// Creates a new PluginManager /// - /// - /// If true will detect changes in the /bin folder and therefor load plugins from the + /// + /// If true will detect changes in the /bin folder, app_code, etc... 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 detectCodeChanges = true) + internal PluginManager(bool detectChanges = true) { _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); //create the folder if it doesn't exist @@ -68,29 +68,41 @@ namespace Umbraco.Core Directory.CreateDirectory(_tempFolder); } + var pluginListFile = GetPluginListFilePath(); + //this is a check for legacy changes, before we didn't store the TypeResolutionKind in the file which was a mistake, //so we need to detect if the old file is there without this attribute, if it is then we delete it if (DetectLegacyPluginListFile()) { - var filePath = GetPluginListFilePath(); - File.Delete(filePath); + File.Delete(pluginListFile); } - if (detectCodeChanges) + if (detectChanges) { //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; + RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; //if they have changed, we need to write the new file - if (HaveAssembliesChanged) + if (RequiresRescanning) { + //if the hash has changed, clear out the persisted list no matter what, this will force + // rescanning of all plugin types including lazy ones. + // http://issues.umbraco.org/issue/U4-4789 + File.Delete(pluginListFile); + WriteCachePluginsHash(); } } else { + + //if the hash has changed, clear out the persisted list no matter what, this will force + // rescanning of all plugin types including lazy ones. + // http://issues.umbraco.org/issue/U4-4789 + File.Delete(pluginListFile); + //always set to true if we're not detecting (generally only for testing) - HaveAssembliesChanged = true; + RequiresRescanning = true; } } @@ -130,9 +142,9 @@ namespace Umbraco.Core /// - /// Returns a bool if the assemblies in the /bin have changed since they were last hashed. + /// Returns a bool if the assemblies in the /bin, app_code, global.asax, etc... have changed since they were last hashed. /// - internal bool HaveAssembliesChanged { get; private set; } + internal bool RequiresRescanning { get; private set; } /// /// Returns the currently cached hash value of the scanned assemblies in the /bin folder. Returns 0 @@ -328,7 +340,7 @@ namespace Umbraco.Core /// /// Generally only used for resetting cache, for example during the install process /// - internal void ClearPluginCache() + public void ClearPluginCache() { var path = GetPluginListFilePath(); if (File.Exists(path)) @@ -670,7 +682,7 @@ namespace Umbraco.Core //we first need to look into our cache file (this has nothing to do with the 'cacheResult' parameter which caches in memory). //if assemblies have not changed and the cache file actually exists, then proceed to try to lookup by the cache file. - if (HaveAssembliesChanged == false && File.Exists(GetPluginListFilePath())) + if (RequiresRescanning == false && File.Exists(GetPluginListFilePath())) { var fileCacheResult = TryGetCachedPluginsFromFile(resolutionType); diff --git a/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs b/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs index 60f32c086d..3ff124d345 100644 --- a/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/SupportTagsAttribute.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Core.Models; namespace Umbraco.Core.PropertyEditors { @@ -30,6 +31,7 @@ namespace Umbraco.Core.PropertyEditors Delimiter = ","; ReplaceTags = true; TagGroup = "default"; + StorageType = TagCacheStorageType.Csv; } /// @@ -37,6 +39,11 @@ namespace Umbraco.Core.PropertyEditors /// public TagValueType ValueType { get; set; } + /// + /// Defines how to store the tags in cache (CSV or Json) + /// + public TagCacheStorageType StorageType { get; set; } + /// /// Defines a custom delimiter, the default is a comma /// diff --git a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs index 743cd08517..978a7bf2ba 100644 --- a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs +++ b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models.Editors; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; namespace Umbraco.Core.PropertyEditors { @@ -30,8 +31,14 @@ namespace Umbraco.Core.PropertyEditors Delimiter = tagsAttribute.Delimiter; ReplaceTags = tagsAttribute.ReplaceTags; TagGroup = tagsAttribute.TagGroup; + StorageType = TagCacheStorageType.Csv; } + /// + /// Defines how to store the tags in cache (CSV or Json) + /// + public virtual TagCacheStorageType StorageType { get; private set; } + /// /// Defines a custom delimiter, the default is a comma /// diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index a460f32e55..f5b41d0e34 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -86,6 +86,7 @@ namespace Umbraco.Core.Security public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound) { if (http == null) throw new ArgumentNullException("http"); + if (http.User == null) return null; //there's no user at all so no identity var identity = http.User.Identity as UmbracoBackOfficeIdentity; if (identity != null) return identity; if (authenticateRequestIfNotFound == false) return null; diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 03e721242c..f1bf4d1321 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -124,7 +124,9 @@ namespace Umbraco.Core.Services { var collection = repository.GetPreValuesCollectionByDataTypeId(id); //now convert the collection to a string list - var list = collection.FormatAsDictionary().Select(x => x.Value.Value).ToList(); + var list = collection.FormatAsDictionary() + .Select(x => x.Value.Value) + .ToList(); return list; } } diff --git a/src/Umbraco.Core/Services/INotificationService.cs b/src/Umbraco.Core/Services/INotificationService.cs index 4be6d17f0d..c067b2d615 100644 --- a/src/Umbraco.Core/Services/INotificationService.cs +++ b/src/Umbraco.Core/Services/INotificationService.cs @@ -13,7 +13,7 @@ using umbraco.interfaces; namespace Umbraco.Core.Services { - internal interface INotificationService : IService + public interface INotificationService : IService { /// /// Sends the notifications for the specified user regarding the specified node and action. diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index b59f672590..6dd9017924 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -19,7 +19,7 @@ using umbraco.interfaces; namespace Umbraco.Core.Services { - internal class NotificationService : INotificationService + public class NotificationService : INotificationService { private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly IUserService _userService; diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 7a9add5c2e..f358e41bf3 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -48,6 +48,10 @@ namespace Umbraco.Core.Services /// /// /// + /// + /// + /// + /// public ServiceContext( IContentService contentService, IMediaService mediaService, @@ -64,7 +68,8 @@ namespace Umbraco.Core.Services IUserService userService, ISectionService sectionService, IApplicationTreeService treeService, - ITagService tagService) + ITagService tagService, + INotificationService notificationService) { _tagService = new Lazy(() => tagService); _contentService = new Lazy(() => contentService); @@ -82,6 +87,7 @@ namespace Umbraco.Core.Services _treeService = new Lazy(() => treeService); _memberService = new Lazy(() => memberService); _userService = new Lazy(() => userService); + _notificationService = new Lazy(() => notificationService); } @@ -174,7 +180,7 @@ namespace Umbraco.Core.Services /// /// Gets the /// - internal INotificationService NotificationService + public INotificationService NotificationService { get { return _notificationService.Value; } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 395f9cea0c..723ff2efc9 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Xml; +using Newtonsoft.Json; using Umbraco.Core.Configuration; using System.Web.Security; using Umbraco.Core.Strings; @@ -52,6 +53,28 @@ namespace Umbraco.Core || (input.StartsWith("[") && input.EndsWith("]")); } + /// + /// Returns a JObject/JArray instance if the string can be converted to json, otherwise returns the string + /// + /// + /// + internal static object ConvertToJsonIfPossible(this string input) + { + if (input.DetectIsJson() == false) + { + return input; + } + try + { + var obj = JsonConvert.DeserializeObject(input); + return obj; + } + catch (Exception ex) + { + return input; + } + } + internal static string ReplaceNonAlphanumericChars(this string input, char replacement) { //any character that is not alphanumeric, convert to a hyphen @@ -190,7 +213,7 @@ namespace Umbraco.Core //this is from SqlMetal and just makes it a bit of fun to allow pluralisation public static string MakePluralName(this string name) { - if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("ss", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) + if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) { name = name + "es"; return name; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8b484914ec..6931fa1c84 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -349,12 +349,14 @@ + + diff --git a/src/Umbraco.Tests/MockTests.cs b/src/Umbraco.Tests/MockTests.cs index 9211321d55..f8810fe67f 100644 --- a/src/Umbraco.Tests/MockTests.cs +++ b/src/Umbraco.Tests/MockTests.cs @@ -55,7 +55,8 @@ namespace Umbraco.Tests new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object); + new Mock().Object, + new Mock().Object); Assert.Pass(); } @@ -99,7 +100,8 @@ namespace Umbraco.Tests new Mock().Object, new Mock().Object, new Mock().Object, - new Mock().Object), + new Mock().Object, + new Mock().Object), CacheHelper.CreateDisabledCacheHelper()); Assert.Pass(); diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 61eda9633d..1501cf09f3 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -272,6 +272,14 @@ namespace Umbraco.Tests.Models { Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(content, null)); } + + //need to ensure the event handlers are wired + + var asDirty = (ICanBeDirty)clone; + + Assert.IsFalse(asDirty.IsPropertyDirty("Properties")); + clone.Properties.Add(new Property(1, Guid.NewGuid(), new PropertyType("test", DataTypeDatabaseType.Ntext) {Alias = "blah"}, "blah")); + Assert.IsTrue(asDirty.IsPropertyDirty("Properties")); } [Test] diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index 8154b60ae2..bb61a84d82 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -4,13 +4,16 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Serialization; +using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Models { [TestFixture] - public class ContentTypeTests + public class ContentTypeTests : BaseUmbracoConfigurationTest { + + [Test] public void Can_Deep_Clone_Content_Type_Sort() { @@ -140,19 +143,22 @@ namespace Umbraco.Tests.Models Assert.AreNotSame(clone.AllowedTemplates.ElementAt(index), contentType.AllowedTemplates.ElementAt(index)); Assert.AreEqual(clone.AllowedTemplates.ElementAt(index), contentType.AllowedTemplates.ElementAt(index)); } + Assert.AreNotSame(clone.PropertyGroups, contentType.PropertyGroups); Assert.AreEqual(clone.PropertyGroups.Count, contentType.PropertyGroups.Count); for (var index = 0; index < contentType.PropertyGroups.Count; index++) { Assert.AreNotSame(clone.PropertyGroups[index], contentType.PropertyGroups[index]); Assert.AreEqual(clone.PropertyGroups[index], contentType.PropertyGroups[index]); } + Assert.AreNotSame(clone.PropertyTypes, contentType.PropertyTypes); Assert.AreEqual(clone.PropertyTypes.Count(), contentType.PropertyTypes.Count()); + Assert.AreEqual(0, ((ContentTypeBase)clone).NonGroupedPropertyTypes.Count()); for (var index = 0; index < contentType.PropertyTypes.Count(); index++) { Assert.AreNotSame(clone.PropertyTypes.ElementAt(index), contentType.PropertyTypes.ElementAt(index)); Assert.AreEqual(clone.PropertyTypes.ElementAt(index), contentType.PropertyTypes.ElementAt(index)); } - + Assert.AreEqual(clone.CreateDate, contentType.CreateDate); Assert.AreEqual(clone.CreatorId, contentType.CreatorId); Assert.AreEqual(clone.Key, contentType.Key); @@ -167,13 +173,24 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.Thumbnail, contentType.Thumbnail); Assert.AreEqual(clone.Icon, contentType.Icon); Assert.AreEqual(clone.IsContainer, contentType.IsContainer); - + //This double verifies by reflection var allProps = clone.GetType().GetProperties(); foreach (var propertyInfo in allProps) { Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(contentType, null)); } + + //need to ensure the event handlers are wired + + var asDirty = (ICanBeDirty)clone; + + Assert.IsFalse(asDirty.IsPropertyDirty("PropertyTypes")); + clone.AddPropertyType(new PropertyType("test", DataTypeDatabaseType.Nvarchar) { Alias = "blah" }); + Assert.IsTrue(asDirty.IsPropertyDirty("PropertyTypes")); + Assert.IsFalse(asDirty.IsPropertyDirty("PropertyGroups")); + clone.AddPropertyGroup("hello"); + Assert.IsTrue(asDirty.IsPropertyDirty("PropertyGroups")); } [Test] diff --git a/src/Umbraco.Tests/Models/MacroTests.cs b/src/Umbraco.Tests/Models/MacroTests.cs new file mode 100644 index 0000000000..b11814d622 --- /dev/null +++ b/src/Umbraco.Tests/Models/MacroTests.cs @@ -0,0 +1,64 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class MacroTests + { + [SetUp] + public void Init() + { + var config = SettingsForTests.GetDefault(); + SettingsForTests.ConfigureSettings(config); + } + + [Test] + public void Can_Deep_Clone() + { + var macro = new Macro(1, true, 3, "test", "Test", "blah", "blah", "xslt", false, true, true, "script"); + macro.Properties.Add(new MacroProperty(6, "rewq", "REWQ", 1, "asdfasdf")); + + var clone = (Macro)macro.DeepClone(); + + Assert.AreNotSame(clone, macro); + Assert.AreEqual(clone, macro); + Assert.AreEqual(clone.Id, macro.Id); + + Assert.AreEqual(clone.Properties.Count, macro.Properties.Count); + + for (int i = 0; i < clone.Properties.Count; i++) + { + Assert.AreEqual(clone.Properties[i], macro.Properties[i]); + Assert.AreNotSame(clone.Properties[i], macro.Properties[i]); + } + + Assert.AreNotSame(clone.Properties, macro.Properties); + Assert.AreNotSame(clone.AddedProperties, macro.AddedProperties); + Assert.AreNotSame(clone.RemovedProperties, macro.RemovedProperties); + + //This double verifies by reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(macro, null)); + } + + //need to ensure the event handlers are wired + + var asDirty = (ICanBeDirty)clone; + + Assert.IsFalse(asDirty.IsPropertyDirty("Properties")); + clone.Properties.Add(new MacroProperty(3, "asdf", "SDF", 3, "asdfasdf")); + Assert.IsTrue(asDirty.IsPropertyDirty("Properties")); + Assert.AreEqual(1, clone.AddedProperties.Count()); + clone.Properties.Remove("rewq"); + Assert.AreEqual(1, clone.RemovedProperties.Count()); + + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Models/PreValueCollectionTests.cs b/src/Umbraco.Tests/Models/PreValueCollectionTests.cs new file mode 100644 index 0000000000..49c9ab16bb --- /dev/null +++ b/src/Umbraco.Tests/Models/PreValueCollectionTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class PreValueCollectionTests + { + [Test] + public void Can_Deep_Clone() + { + var d = new PreValueCollection(new Dictionary + { + {"blah1", new PreValue(1, "test1", 1)}, + {"blah2", new PreValue(2, "test1", 3)}, + {"blah3", new PreValue(3, "test1", 2)} + }); + + var a = new PreValueCollection(new[] + { + new PreValue(1, "test1", 1), + new PreValue(2, "test1", 3), + new PreValue(3, "test1", 2) + }); + + var clone1 = (PreValueCollection)d.DeepClone(); + var clone2 = (PreValueCollection)a.DeepClone(); + + Action assert = (orig, clone) => + { + Assert.AreNotSame(orig, clone); + var oDic = orig.FormatAsDictionary(); + var cDic = clone.FormatAsDictionary(); + Assert.AreEqual(oDic.Keys.Count(), cDic.Keys.Count()); + foreach (var k in oDic.Keys) + { + Assert.AreNotSame(oDic[k], cDic[k]); + Assert.AreEqual(oDic[k].Id, cDic[k].Id); + Assert.AreEqual(oDic[k].SortOrder, cDic[k].SortOrder); + Assert.AreEqual(oDic[k].Value, cDic[k].Value); + } + }; + + assert(d, clone1); + assert(a, clone2); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Models/PropertyGroupTests.cs b/src/Umbraco.Tests/Models/PropertyGroupTests.cs index b6b15c83e8..413e94bfee 100644 --- a/src/Umbraco.Tests/Models/PropertyGroupTests.cs +++ b/src/Umbraco.Tests/Models/PropertyGroupTests.cs @@ -2,11 +2,12 @@ using System; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Serialization; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Models { [TestFixture] - public class PropertyGroupTests + public class PropertyGroupTests : BaseUmbracoConfigurationTest { [Test] public void Can_Deep_Clone() @@ -20,7 +21,7 @@ namespace Umbraco.Tests.Models Alias = "test", CreateDate = DateTime.Now, DataTypeDefinitionId = 5, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing", Key = Guid.NewGuid(), Mandatory = true, @@ -37,7 +38,7 @@ namespace Umbraco.Tests.Models Alias = "test2", CreateDate = DateTime.Now, DataTypeDefinitionId = 6, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing2", Key = Guid.NewGuid(), Mandatory = true, @@ -101,7 +102,7 @@ namespace Umbraco.Tests.Models Alias = "test", CreateDate = DateTime.Now, DataTypeDefinitionId = 5, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing", Key = Guid.NewGuid(), Mandatory = true, @@ -118,7 +119,7 @@ namespace Umbraco.Tests.Models Alias = "test2", CreateDate = DateTime.Now, DataTypeDefinitionId = 6, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing2", Key = Guid.NewGuid(), Mandatory = true, diff --git a/src/Umbraco.Tests/Models/PropertyTypeTests.cs b/src/Umbraco.Tests/Models/PropertyTypeTests.cs index e051791faf..a623fe6444 100644 --- a/src/Umbraco.Tests/Models/PropertyTypeTests.cs +++ b/src/Umbraco.Tests/Models/PropertyTypeTests.cs @@ -17,7 +17,7 @@ namespace Umbraco.Tests.Models Alias = "test", CreateDate = DateTime.Now, DataTypeDefinitionId = 5, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing", Key = Guid.NewGuid(), Mandatory = true, @@ -67,7 +67,7 @@ namespace Umbraco.Tests.Models Alias = "test", CreateDate = DateTime.Now, DataTypeDefinitionId = 5, - DataTypeId = Guid.NewGuid(), + PropertyEditorAlias = "propTest", Description = "testing", Key = Guid.NewGuid(), Mandatory = true, diff --git a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs index ac70364e67..52513d551d 100644 --- a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs +++ b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs @@ -47,12 +47,13 @@ namespace Umbraco.Tests.Models }; item.AdditionalData.Add("test1", 3); item.AdditionalData.Add("test2", "valuie"); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + + item.AdditionalData.Add("test3", new UmbracoEntity.EntityProperty() { Value = "test", PropertyEditorAlias = "TestPropertyEditor" }); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test4", new UmbracoEntity.EntityProperty() { Value = "test2", PropertyEditorAlias = "TestPropertyEditor2" @@ -82,12 +83,6 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.UpdateDate, item.UpdateDate); Assert.AreEqual(clone.AdditionalData.Count, item.AdditionalData.Count); Assert.AreEqual(clone.AdditionalData, item.AdditionalData); - Assert.AreEqual(clone.UmbracoProperties.Count, item.UmbracoProperties.Count); - for (var i = 0; i < clone.UmbracoProperties.Count; i++) - { - Assert.AreNotSame(clone.UmbracoProperties[i], item.UmbracoProperties[i]); - Assert.AreEqual(clone.UmbracoProperties[i], item.UmbracoProperties[i]); - } //This double verifies by reflection var allProps = clone.GetType().GetProperties(); @@ -125,12 +120,12 @@ namespace Umbraco.Tests.Models }; item.AdditionalData.Add("test1", 3); item.AdditionalData.Add("test2", "valuie"); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test3", new UmbracoEntity.EntityProperty() { Value = "test", PropertyEditorAlias = "TestPropertyEditor" }); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test4", new UmbracoEntity.EntityProperty() { Value = "test2", PropertyEditorAlias = "TestPropertyEditor2" diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs index 84b0fe317d..7fa9742870 100644 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ b/src/Umbraco.Tests/Models/UserTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NUnit.Framework; using Umbraco.Core.Models.Membership; using Umbraco.Core.Serialization; @@ -39,6 +40,8 @@ namespace Umbraco.Tests.Models Username = "username" }; + item.AddAllowedSection("test"); + var clone = (User)item.DeepClone(); Assert.AreNotSame(clone, item); @@ -46,6 +49,7 @@ namespace Umbraco.Tests.Models Assert.AreNotSame(clone.UserType, item.UserType); Assert.AreEqual(clone.UserType, item.UserType); + Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); //Verify normal properties with reflection var allProps = clone.GetType().GetProperties(); @@ -53,6 +57,17 @@ namespace Umbraco.Tests.Models { Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); } + + //ensure internal collections are differet + Assert.AreNotSame(item.AddedSections, clone.AddedSections); + Assert.AreNotSame(item.RemovedSections, clone.RemovedSections); + + //ensure event handlers are still wired on clone + clone.AddAllowedSection("blah"); + Assert.AreEqual(1, clone.AddedSections.Count()); + clone.RemoveAllowedSection("blah"); + Assert.AreEqual(1, clone.RemovedSections.Count()); + } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs index 5907b6c6be..377b9e57e7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs @@ -364,7 +364,58 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Update_Property_For_Macro() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = new MacroRepository(unitOfWork, NullCacheProvider.Current)) + { + var macro = repository.Get(1); + macro.Properties.Add(new MacroProperty("new1", "New1", 3, "test")); + repository.AddOrUpdate(macro); + unitOfWork.Commit(); + //Act + macro = repository.Get(1); + macro.Properties["new1"].Name = "this is a new name"; + repository.AddOrUpdate(macro); + unitOfWork.Commit(); + + + // Assert + var result = repository.Get(1); + Assert.AreEqual("new1", result.Properties.First().Alias); + Assert.AreEqual("this is a new name", result.Properties.First().Name); + + } + } + + [Test] + public void Can_Update_Macro_Property_Alias() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = new MacroRepository(unitOfWork, NullCacheProvider.Current)) + { + var macro = repository.Get(1); + macro.Properties.Add(new MacroProperty("new1", "New1", 3, "test")); + repository.AddOrUpdate(macro); + unitOfWork.Commit(); + + //Act + macro = repository.Get(1); + macro.Properties.UpdateProperty("new1", newAlias: "newAlias"); + repository.AddOrUpdate(macro); + unitOfWork.Commit(); + + // Assert + var result = repository.Get(1); + Assert.AreEqual("newAlias", result.Properties.First().Alias); + } + } [TearDown] public override void TearDown() diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index e2584a936c..a78ca65d58 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence; @@ -10,6 +11,7 @@ using Umbraco.Core.Persistence.Caching; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Persistence.Repositories { @@ -37,10 +39,13 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); // Act - var repository = RepositoryResolver.Current.ResolveByType(unitOfWork); + using (var repository = RepositoryResolver.Current.ResolveByType(unitOfWork)) + { + + // Assert + Assert.That(repository, Is.Not.Null); + } - // Assert - Assert.That(repository, Is.Not.Null); } [Test] @@ -51,10 +56,13 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); // Act - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + + // Assert + Assert.That(repository, Is.Not.Null); + } - // Assert - Assert.That(repository, Is.Not.Null); } [Test] @@ -63,16 +71,18 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = new PetaPocoUnitOfWorkProvider(); var unitOfWork = provider.GetUnitOfWork(); - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + // Act + var template = new Template("test-add-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; + repository.AddOrUpdate(template); + unitOfWork.Commit(); - // Act - var template = new Template("test-add-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; - repository.AddOrUpdate(template); - unitOfWork.Commit(); - - //Assert - Assert.That(repository.Get("test"), Is.Not.Null); - Assert.That(_masterPageFileSystem.FileExists("test.master"), Is.True); + //Assert + Assert.That(repository.Get("test"), Is.Not.Null); + Assert.That(_masterPageFileSystem.FileExists("test.master"), Is.True); + } + } [Test] @@ -81,22 +91,25 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = new PetaPocoUnitOfWorkProvider(); var unitOfWork = provider.GetUnitOfWork(); - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + // Act + var template = new Template("test-updated-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; + repository.AddOrUpdate(template); + unitOfWork.Commit(); - // Act - var template = new Template("test-updated-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; - repository.AddOrUpdate(template); - unitOfWork.Commit(); + template.Content = @"<%@ Master Language=""VB"" %>"; + repository.AddOrUpdate(template); + unitOfWork.Commit(); - template.Content = @"<%@ Master Language=""VB"" %>"; - repository.AddOrUpdate(template); - unitOfWork.Commit(); + var updated = repository.Get("test"); - var updated = repository.Get("test"); + // Assert + Assert.That(_masterPageFileSystem.FileExists("test.master"), Is.True); + Assert.That(updated.Content, Is.EqualTo(@"<%@ Master Language=""VB"" %>")); + } - // Assert - Assert.That(_masterPageFileSystem.FileExists("test.master"), Is.True); - Assert.That(updated.Content, Is.EqualTo(@"<%@ Master Language=""VB"" %>")); + } [Test] @@ -105,19 +118,64 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = new PetaPocoUnitOfWorkProvider(); var unitOfWork = provider.GetUnitOfWork(); - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + var template = new Template("test-add-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; + repository.AddOrUpdate(template); + unitOfWork.Commit(); - var template = new Template("test-add-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; - repository.AddOrUpdate(template); - unitOfWork.Commit(); + // Act + var templates = repository.Get("test"); + repository.Delete(templates); + unitOfWork.Commit(); - // Act - var templates = repository.Get("test"); - repository.Delete(templates); - unitOfWork.Commit(); + // Assert + Assert.IsNull(repository.Get("test")); + } - // Assert - Assert.IsNull(repository.Get("test")); + + } + + [Test] + public void Can_Perform_Delete_When_Assigned_To_Doc() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + + var templateRepository = new TemplateRepository(unitOfWork, NullCacheProvider.Current); + var tagRepository = new TagsRepository(unitOfWork, NullCacheProvider.Current); + var contentTypeRepository = new ContentTypeRepository(unitOfWork, NullCacheProvider.Current, templateRepository); + var contentRepo = new ContentRepository(unitOfWork, NullCacheProvider.Current, contentTypeRepository, templateRepository, tagRepository, CacheHelper.CreateDisabledCacheHelper()); + + using (contentRepo) + { + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); + var textpage = MockedContent.CreateSimpleContent(contentType); + contentTypeRepository.AddOrUpdate(contentType); + contentRepo.AddOrUpdate(textpage); + unitOfWork.Commit(); + + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + var template = new Template("test-add-masterpage.master", "test", "test") { Content = @"<%@ Master Language=""C#"" %>" }; + repository.AddOrUpdate(template); + unitOfWork.Commit(); + + textpage.Template = template; + contentRepo.AddOrUpdate(textpage); + unitOfWork.Commit(); + + // Act + var templates = repository.Get("test"); + repository.Delete(templates); + unitOfWork.Commit(); + + // Assert + Assert.IsNull(repository.Get("test")); + } + } + } [Test] @@ -126,27 +184,30 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = new PetaPocoUnitOfWorkProvider(); var unitOfWork = provider.GetUnitOfWork(); - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + var parent = new Template("test-parent-masterpage.master", "parent", "parent") { Content = @"<%@ Master Language=""C#"" %>" }; + var child = new Template("test-child-masterpage.master", "child", "child") { Content = @"<%@ Master Language=""C#"" %>" }; + var baby = new Template("test-baby-masterpage.master", "baby", "baby") { Content = @"<%@ Master Language=""C#"" %>" }; + child.MasterTemplateAlias = parent.Alias; + child.MasterTemplateId = new Lazy(() => parent.Id); + baby.MasterTemplateAlias = child.Alias; + baby.MasterTemplateId = new Lazy(() => child.Id); + repository.AddOrUpdate(parent); + repository.AddOrUpdate(child); + repository.AddOrUpdate(baby); + unitOfWork.Commit(); - var parent = new Template("test-parent-masterpage.master", "parent", "parent") { Content = @"<%@ Master Language=""C#"" %>" }; - var child = new Template("test-child-masterpage.master", "child", "child") { Content = @"<%@ Master Language=""C#"" %>" }; - var baby = new Template("test-baby-masterpage.master", "baby", "baby") { Content = @"<%@ Master Language=""C#"" %>" }; - child.MasterTemplateAlias = parent.Alias; - child.MasterTemplateId = new Lazy(() => parent.Id); - baby.MasterTemplateAlias = child.Alias; - baby.MasterTemplateId = new Lazy(() => child.Id); - repository.AddOrUpdate(parent); - repository.AddOrUpdate(child); - repository.AddOrUpdate(baby); - unitOfWork.Commit(); + // Act + var templates = repository.Get("parent"); + repository.Delete(templates); + unitOfWork.Commit(); - // Act - var templates = repository.Get("parent"); - repository.Delete(templates); - unitOfWork.Commit(); + // Assert + Assert.IsNull(repository.Get("test")); + } - // Assert - Assert.IsNull(repository.Get("test")); + } [Test] @@ -155,66 +216,69 @@ namespace Umbraco.Tests.Persistence.Repositories // Arrange var provider = new PetaPocoUnitOfWorkProvider(); var unitOfWork = provider.GetUnitOfWork(); - var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem); + using (var repository = new TemplateRepository(unitOfWork, NullCacheProvider.Current, _masterPageFileSystem, _viewsFileSystem)) + { + var parent = new Template("test-parent-masterpage.master", "parent", "parent") { Content = @"<%@ Master Language=""C#"" %>" }; + + var child1 = new Template("test-child1-masterpage.master", "child1", "child1") { Content = @"<%@ Master Language=""C#"" %>" }; + var toddler1 = new Template("test-toddler1-masterpage.master", "toddler1", "toddler1") { Content = @"<%@ Master Language=""C#"" %>" }; + var toddler2 = new Template("test-toddler2-masterpage.master", "toddler2", "toddler2") { Content = @"<%@ Master Language=""C#"" %>" }; + var baby1 = new Template("test-baby1-masterpage.master", "baby1", "baby1") { Content = @"<%@ Master Language=""C#"" %>" }; + + var child2 = new Template("test-child2-masterpage.master", "child2", "child2") { Content = @"<%@ Master Language=""C#"" %>" }; + var toddler3 = new Template("test-toddler3-masterpage.master", "toddler3", "toddler3") { Content = @"<%@ Master Language=""C#"" %>" }; + var toddler4 = new Template("test-toddler4-masterpage.master", "toddler4", "toddler4") { Content = @"<%@ Master Language=""C#"" %>" }; + var baby2 = new Template("test-baby2-masterpage.master", "baby2", "baby2") { Content = @"<%@ Master Language=""C#"" %>" }; + + + child1.MasterTemplateAlias = parent.Alias; + child1.MasterTemplateId = new Lazy(() => parent.Id); + child2.MasterTemplateAlias = parent.Alias; + child2.MasterTemplateId = new Lazy(() => parent.Id); + + toddler1.MasterTemplateAlias = child1.Alias; + toddler1.MasterTemplateId = new Lazy(() => child1.Id); + toddler2.MasterTemplateAlias = child1.Alias; + toddler2.MasterTemplateId = new Lazy(() => child1.Id); + + toddler3.MasterTemplateAlias = child2.Alias; + toddler3.MasterTemplateId = new Lazy(() => child2.Id); + toddler4.MasterTemplateAlias = child2.Alias; + toddler4.MasterTemplateId = new Lazy(() => child2.Id); + + baby1.MasterTemplateAlias = toddler2.Alias; + baby1.MasterTemplateId = new Lazy(() => toddler2.Id); + + baby2.MasterTemplateAlias = toddler4.Alias; + baby2.MasterTemplateId = new Lazy(() => toddler4.Id); + + repository.AddOrUpdate(parent); + repository.AddOrUpdate(child1); + repository.AddOrUpdate(child2); + repository.AddOrUpdate(toddler1); + repository.AddOrUpdate(toddler2); + repository.AddOrUpdate(toddler3); + repository.AddOrUpdate(toddler4); + repository.AddOrUpdate(baby1); + repository.AddOrUpdate(baby2); + unitOfWork.Commit(); + + // Act + var rootNode = repository.GetTemplateNode("parent"); + + // Assert + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "parent")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "child1")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "child2")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler1")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler2")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler3")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler4")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "baby1")); + Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "baby2")); + } - var parent = new Template("test-parent-masterpage.master", "parent", "parent") { Content = @"<%@ Master Language=""C#"" %>" }; - var child1 = new Template("test-child1-masterpage.master", "child1", "child1") { Content = @"<%@ Master Language=""C#"" %>" }; - var toddler1 = new Template("test-toddler1-masterpage.master", "toddler1", "toddler1") { Content = @"<%@ Master Language=""C#"" %>" }; - var toddler2 = new Template("test-toddler2-masterpage.master", "toddler2", "toddler2") { Content = @"<%@ Master Language=""C#"" %>" }; - var baby1 = new Template("test-baby1-masterpage.master", "baby1", "baby1") { Content = @"<%@ Master Language=""C#"" %>" }; - - var child2 = new Template("test-child2-masterpage.master", "child2", "child2") { Content = @"<%@ Master Language=""C#"" %>" }; - var toddler3 = new Template("test-toddler3-masterpage.master", "toddler3", "toddler3") { Content = @"<%@ Master Language=""C#"" %>" }; - var toddler4 = new Template("test-toddler4-masterpage.master", "toddler4", "toddler4") { Content = @"<%@ Master Language=""C#"" %>" }; - var baby2 = new Template("test-baby2-masterpage.master", "baby2", "baby2") { Content = @"<%@ Master Language=""C#"" %>" }; - - - child1.MasterTemplateAlias = parent.Alias; - child1.MasterTemplateId = new Lazy(() => parent.Id); - child2.MasterTemplateAlias = parent.Alias; - child2.MasterTemplateId = new Lazy(() => parent.Id); - - toddler1.MasterTemplateAlias = child1.Alias; - toddler1.MasterTemplateId = new Lazy(() => child1.Id); - toddler2.MasterTemplateAlias = child1.Alias; - toddler2.MasterTemplateId = new Lazy(() => child1.Id); - - toddler3.MasterTemplateAlias = child2.Alias; - toddler3.MasterTemplateId = new Lazy(() => child2.Id); - toddler4.MasterTemplateAlias = child2.Alias; - toddler4.MasterTemplateId = new Lazy(() => child2.Id); - - baby1.MasterTemplateAlias = toddler2.Alias; - baby1.MasterTemplateId = new Lazy(() => toddler2.Id); - - baby2.MasterTemplateAlias = toddler4.Alias; - baby2.MasterTemplateId = new Lazy(() => toddler4.Id); - - repository.AddOrUpdate(parent); - repository.AddOrUpdate(child1); - repository.AddOrUpdate(child2); - repository.AddOrUpdate(toddler1); - repository.AddOrUpdate(toddler2); - repository.AddOrUpdate(toddler3); - repository.AddOrUpdate(toddler4); - repository.AddOrUpdate(baby1); - repository.AddOrUpdate(baby2); - unitOfWork.Commit(); - - // Act - var rootNode = repository.GetTemplateNode("parent"); - - // Assert - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "parent")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "child1")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "child2")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler1")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler2")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler3")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "toddler4")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "baby1")); - Assert.IsNotNull(repository.FindTemplateInTree(rootNode, "baby2")); } //[Test] diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 82cc2c7560..9826e9fc3f 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.Document); + var entities = service.GetAll(UmbracoObjectTypes.Document).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var objectTypeId = new Guid(Constants.ObjectTypes.Document); - var entities = service.GetAll(objectTypeId); + var entities = service.GetAll(objectTypeId).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -57,7 +57,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(); + var entities = service.GetAll().ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -69,7 +69,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetChildren(-1, UmbracoObjectTypes.Document); + var entities = service.GetChildren(-1, UmbracoObjectTypes.Document).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -92,7 +92,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.DocumentType); + var entities = service.GetAll(UmbracoObjectTypes.DocumentType).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -104,7 +104,7 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var objectTypeId = new Guid(Constants.ObjectTypes.DocumentType); - var entities = service.GetAll(objectTypeId); + var entities = service.GetAll(objectTypeId).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -115,7 +115,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(); + var entities = service.GetAll().ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -126,16 +126,16 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.Media); + var entities = service.GetAll(UmbracoObjectTypes.Media).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(3)); - //Assert.That(entities.Any(x => ((UmbracoEntity)x).UmbracoFile != string.Empty), Is.True); + Assert.That( entities.Any( x => - ((UmbracoEntity) x).UmbracoProperties.Any( - y => y.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); + x.AdditionalData.Any(y => y.Value is UmbracoEntity.EntityProperty + && ((UmbracoEntity.EntityProperty)y.Value).PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); } private static bool _isSetup = false; diff --git a/src/Umbraco.Tests/Services/MacroServiceTests.cs b/src/Umbraco.Tests/Services/MacroServiceTests.cs index 5ef75d0b14..d88dd26dd4 100644 --- a/src/Umbraco.Tests/Services/MacroServiceTests.cs +++ b/src/Umbraco.Tests/Services/MacroServiceTests.cs @@ -150,6 +150,40 @@ namespace Umbraco.Tests.Services } + [Test] + public void Can_Add_And_Remove_Properties() + { + var macroService = ServiceContext.MacroService; + var macro = new Macro("test", "Test", scriptPath: "~/Views/MacroPartials/Test.cshtml", cacheDuration: 1234); + + //adds some properties + macro.Properties.Add(new MacroProperty("blah1", "Blah1", 0, "blah1")); + macro.Properties.Add(new MacroProperty("blah2", "Blah2", 0, "blah2")); + macro.Properties.Add(new MacroProperty("blah3", "Blah3", 0, "blah3")); + macro.Properties.Add(new MacroProperty("blah4", "Blah4", 0, "blah4")); + macroService.Save(macro); + + var result1 = macroService.GetById(macro.Id); + Assert.AreEqual(4, result1.Properties.Count()); + + //simulate clearing the sections + foreach (var s in result1.Properties.ToArray()) + { + result1.Properties.Remove(s.Alias); + } + //now just re-add a couple + result1.Properties.Add(new MacroProperty("blah3", "Blah3", 0, "blah3")); + result1.Properties.Add(new MacroProperty("blah4", "Blah4", 0, "blah4")); + macroService.Save(result1); + + //assert + + //re-get + result1 = macroService.GetById(result1.Id); + Assert.AreEqual(2, result1.Properties.Count()); + + } + //[Test] //public void Can_Get_Many_By_Alias() //{ diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index b1f0895022..f8c9429e80 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -390,6 +390,41 @@ namespace Umbraco.Tests.Services Assert.That(user.DefaultPermissions, Is.EqualTo(userType.Permissions)); } + [Test] + public void Can_Add_And_Remove_Sections_From_User() + { + var userType = ServiceContext.UserService.GetUserTypeByAlias("admin"); + + var user1 = ServiceContext.UserService.CreateUserWithIdentity("test1", "test1@test.com", userType); + + //adds some allowed sections + user1.AddAllowedSection("test1"); + user1.AddAllowedSection("test2"); + user1.AddAllowedSection("test3"); + user1.AddAllowedSection("test4"); + ServiceContext.UserService.Save(user1); + + var result1 = ServiceContext.UserService.GetUserById((int)user1.Id); + Assert.AreEqual(4, result1.AllowedSections.Count()); + + //simulate clearing the sections + foreach (var s in user1.AllowedSections) + { + result1.RemoveAllowedSection(s); + } + //now just re-add a couple + result1.AddAllowedSection("test3"); + result1.AddAllowedSection("test4"); + ServiceContext.UserService.Save(result1); + + //assert + + //re-get + result1 = ServiceContext.UserService.GetUserById((int)user1.Id); + Assert.AreEqual(2, result1.AllowedSections.Count()); + + } + [Test] public void Can_Remove_Section_From_All_Assigned_Users() { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index b36d94f3e1..379547049d 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -178,6 +178,7 @@ + @@ -190,6 +191,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index 9b786eda52..8aaf0e8b09 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -11,7 +11,7 @@ module.exports = function (grunt) { grunt.registerTask('watch-html', ['copy:views', 'copy:vs']); grunt.registerTask('watch-packages', ['copy:packages']); grunt.registerTask('watch-installer', ['concat:install', 'concat:installJs', 'copy:installer', 'copy:vs']); - grunt.registerTask('watch-tuning', ['copy:tuning', 'concat:tuningJs', 'concat:tuningLess']); + grunt.registerTask('watch-tuning', ['copy:tuning', 'concat:tuningJs', 'concat:tuningLess', 'copy:vs']); grunt.registerTask('watch-test', ['jshint:dev', 'karma:unit']); //triggered from grunt dev or grunt @@ -115,6 +115,7 @@ module.exports = function (grunt) { }, + // Copies over the files downloaded by bower bower: { files: [ { @@ -133,6 +134,7 @@ module.exports = function (grunt) { } ] }, + */ installer: { @@ -170,7 +172,8 @@ module.exports = function (grunt) { { dest: '<%= vsdir %>/assets', src: '**', expand: true, cwd: '<%= distdir %>/assets' }, { dest: '<%= vsdir %>/js', src: '**', expand: true, cwd: '<%= distdir %>/js' }, { dest: '<%= vsdir %>/lib', src: '**', expand: true, cwd: '<%= distdir %>/lib' }, - { dest: '<%= vsdir %>/views', src: '**', expand: true, cwd: '<%= distdir %>/views' } + { dest: '<%= vsdir %>/views', src: '**', expand: true, cwd: '<%= distdir %>/views' }, + { dest: '<%= vsdir %>/preview', src: '**', expand: true, cwd: '<%= distdir %>/preview' } ] }, diff --git a/src/Umbraco.Web.UI.Client/lib/typeahead/typeahead.bundle.min.js b/src/Umbraco.Web.UI.Client/lib/typeahead/typeahead.bundle.min.js new file mode 100644 index 0000000000..dff8ef56ec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/typeahead/typeahead.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * typeahead.js 0.10.2 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT + */ + +!function(a){var b={isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},noop:function(){}},c="0.10.2",d=function(){function a(a){return a.split(/\s+/)}function b(a){return a.split(/\W+/)}function c(a){return function(b){return function(c){return a(c[b])}}}return{nonword:b,whitespace:a,obj:{nonword:c(b),whitespace:c(a)}}}(),e=function(){function a(a){this.maxSize=a||100,this.size=0,this.hash={},this.list=new c}function c(){this.head=this.tail=null}function d(a,b){this.key=a,this.val=b,this.prev=this.next=null}return b.mixin(a.prototype,{set:function(a,b){var c,e=this.list.tail;this.size>=this.maxSize&&(this.list.remove(e),delete this.hash[e.key]),(c=this.hash[a])?(c.val=b,this.list.moveToFront(c)):(c=new d(a,b),this.list.add(c),this.hash[a]=c,this.size++)},get:function(a){var b=this.hash[a];return b?(this.list.moveToFront(b),b.val):void 0}}),b.mixin(c.prototype,{add:function(a){this.head&&(a.next=this.head,this.head.prev=a),this.head=a,this.tail=this.tail||a},remove:function(a){a.prev?a.prev.next=a.next:this.head=a.next,a.next?a.next.prev=a.prev:this.tail=a.prev},moveToFront:function(a){this.remove(a),this.add(a)}}),a}(),f=function(){function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+this.prefix)}function c(){return(new Date).getTime()}function d(a){return JSON.stringify(b.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return b.isNumber(g)?f.setItem(this._ttlKey(a),d(c()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return b.isNumber(d)&&c()>d?!0:!1}}:{get:b.noop,set:b.noop,remove:b.noop,clear:b.noop,isExpired:b.noop},b.mixin(a.prototype,g),a}(),g=function(){function c(b){b=b||{},this._send=b.transport?d(b.transport):a.ajax,this._get=b.rateLimiter?b.rateLimiter(this._get):this._get}function d(c){return function(d,e){function f(a){b.defer(function(){h.resolve(a)})}function g(a){b.defer(function(){h.reject(a)})}var h=a.Deferred();return c(d,e,f,g),h}}var f=0,g={},h=6,i=new e(10);return c.setMaxPendingRequests=function(a){h=a},c.resetCache=function(){i=new e(10)},b.mixin(c.prototype,{_get:function(a,b,c){function d(b){c&&c(null,b),i.set(a,b)}function e(){c&&c(!0)}function j(){f--,delete g[a],l.onDeckRequestArgs&&(l._get.apply(l,l.onDeckRequestArgs),l.onDeckRequestArgs=null)}var k,l=this;(k=g[a])?k.done(d).fail(e):h>f?(f++,g[a]=this._send(a,b).done(d).fail(e).always(j)):this.onDeckRequestArgs=[].slice.call(arguments,0)},get:function(a,c,d){var e;return b.isFunction(c)&&(d=c,c={}),(e=i.get(a))?b.defer(function(){d&&d(null,e)}):this._get(a,c,d),!!e}}),c}(),h=function(){function c(b){b=b||{},b.datumTokenizer&&b.queryTokenizer||a.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=b.datumTokenizer,this.queryTokenizer=b.queryTokenizer,this.reset()}function d(a){return a=b.filter(a,function(a){return!!a}),a=b.map(a,function(a){return a.toLowerCase()})}function e(){return{ids:[],children:{}}}function f(a){for(var b={},c=[],d=0;db[e]?e++:(f.push(a[d]),d++,e++);return f}return b.mixin(c.prototype,{bootstrap:function(a){this.datums=a.datums,this.trie=a.trie},add:function(a){var c=this;a=b.isArray(a)?a:[a],b.each(a,function(a){var f,g;f=c.datums.push(a)-1,g=d(c.datumTokenizer(a)),b.each(g,function(a){var b,d,g;for(b=c.trie,d=a.split("");g=d.shift();)b=b.children[g]||(b.children[g]=e()),b.ids.push(f)})})},get:function(a){var c,e,h=this;return c=d(this.queryTokenizer(a)),b.each(c,function(a){var b,c,d,f;if(e&&0===e.length)return!1;for(b=h.trie,c=a.split("");b&&(d=c.shift());)b=b.children[d];return b&&0===c.length?(f=b.ids.slice(0),void(e=e?g(e,f):f)):(e=[],!1)}),e?b.map(f(e),function(a){return h.datums[a]}):[]},reset:function(){this.datums=[],this.trie=e()},serialize:function(){return{datums:this.datums,trie:this.trie}}}),c}(),i=function(){function d(a){return a.local||null}function e(d){var e,f;return f={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(e=d.prefetch||null)&&(e=b.isString(e)?{url:e}:e,e=b.mixin(f,e),e.thumbprint=c+e.thumbprint,e.ajax.type=e.ajax.type||"GET",e.ajax.dataType=e.ajax.dataType||"json",!e.url&&a.error("prefetch requires url to be set")),e}function f(c){function d(a){return function(c){return b.debounce(c,a)}}function e(a){return function(c){return b.throttle(c,a)}}var f,g;return g={url:null,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(f=c.remote||null)&&(f=b.isString(f)?{url:f}:f,f=b.mixin(g,f),f.rateLimiter=/^throttle$/i.test(f.rateLimitBy)?e(f.rateLimitWait):d(f.rateLimitWait),f.ajax.type=f.ajax.type||"GET",f.ajax.dataType=f.ajax.dataType||"json",delete f.rateLimitBy,delete f.rateLimitWait,!f.url&&a.error("remote requires url to be set")),f}return{local:d,prefetch:e,remote:f}}();!function(c){function e(b){b&&(b.local||b.prefetch||b.remote)||a.error("one of local, prefetch, or remote is required"),this.limit=b.limit||5,this.sorter=j(b.sorter),this.dupDetector=b.dupDetector||k,this.local=i.local(b),this.prefetch=i.prefetch(b),this.remote=i.remote(b),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new h({datumTokenizer:b.datumTokenizer,queryTokenizer:b.queryTokenizer}),this.storage=this.cacheKey?new f(this.cacheKey):null}function j(a){function c(b){return b.sort(a)}function d(a){return a}return b.isFunction(a)?c:d}function k(){return!1}var l,m;return l=c.Bloodhound,m={data:"data",protocol:"protocol",thumbprint:"thumbprint"},c.Bloodhound=e,e.noConflict=function(){return c.Bloodhound=l,e},e.tokenizers=d,b.mixin(e.prototype,{_loadPrefetch:function(b){function c(a){f.clear(),f.add(b.filter?b.filter(a):a),f._saveToStorage(f.index.serialize(),b.thumbprint,b.ttl)}var d,e,f=this;return(d=this._readFromStorage(b.thumbprint))?(this.index.bootstrap(d),e=a.Deferred().resolve()):e=a.ajax(b.url,b.ajax).done(c),e},_getFromRemote:function(a,b){function c(a,c){b(a?[]:f.remote.filter?f.remote.filter(c):c)}var d,e,f=this;return a=a||"",e=encodeURIComponent(a),d=this.remote.replace?this.remote.replace(this.remote.url,a):this.remote.url.replace(this.remote.wildcard,e),this.transport.get(d,this.remote.ajax,c)},_saveToStorage:function(a,b,c){this.storage&&(this.storage.set(m.data,a,c),this.storage.set(m.protocol,location.protocol,c),this.storage.set(m.thumbprint,b,c))},_readFromStorage:function(a){var b,c={};return this.storage&&(c.data=this.storage.get(m.data),c.protocol=this.storage.get(m.protocol),c.thumbprint=this.storage.get(m.thumbprint)),b=c.thumbprint!==a||c.protocol!==location.protocol,c.data&&!b?c.data:null},_initialize:function(){function c(){e.add(b.isFunction(f)?f():f)}var d,e=this,f=this.local;return d=this.prefetch?this._loadPrefetch(this.prefetch):a.Deferred().resolve(),f&&d.done(c),this.transport=this.remote?new g(this.remote):null,this.initPromise=d.promise()},initialize:function(a){return!this.initPromise||a?this._initialize():this.initPromise},add:function(a){this.index.add(a)},get:function(a,c){function d(a){var d=f.slice(0);b.each(a,function(a){var c;return c=b.some(d,function(b){return e.dupDetector(a,b)}),!c&&d.push(a),d.length0||!this.transport)&&c&&c(f)},clear:function(){this.index.reset()},clearPrefetchCache:function(){this.storage&&this.storage.clear()},clearRemoteCache:function(){this.transport&&g.resetCache()},ttAdapter:function(){return b.bind(this.get,this)}}),e}(this);var j={wrapper:'',dropdown:'',dataset:'
',suggestions:'',suggestion:'
'},k={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},suggestions:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};b.isMsie()&&b.mixin(k.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),b.isMsie()&&b.isMsie()<=7&&b.mixin(k.input,{marginTop:"-1px"});var l=function(){function c(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d="typeahead:";return b.mixin(c.prototype,{trigger:function(a){var b=[].slice.call(arguments,1);this.$el.trigger(d+a,b)}}),c}(),m=function(){function a(a,b,c,d){var e;if(!c)return this;for(b=b.split(i),c=d?h(c,d):c,this._callbacks=this._callbacks||{};e=b.shift();)this._callbacks[e]=this._callbacks[e]||{sync:[],async:[]},this._callbacks[e][a].push(c);return this}function b(b,c,d){return a.call(this,"async",b,c,d)}function c(b,c,d){return a.call(this,"sync",b,c,d)}function d(a){var b;if(!this._callbacks)return this;for(a=a.split(i);b=a.shift();)delete this._callbacks[b];return this}function e(a){var b,c,d,e,g;if(!this._callbacks)return this;for(a=a.split(i),d=[].slice.call(arguments,1);(b=a.shift())&&(c=this._callbacks[b]);)e=f(c.sync,this,[b].concat(d)),g=f(c.async,this,[b].concat(d)),e()&&j(g);return this}function f(a,b,c){function d(){for(var d,e=0;!d&&e
/// [HttpGet] - public HttpResponseMessage IsAuthenticated() + public bool IsAuthenticated() { var attempt = UmbracoContext.Security.AuthorizeRequest(); if (attempt == ValidateRequestAttempt.Success) { - return Request.CreateResponse(HttpStatusCode.OK); - } - //return BadRequest (400), we don't want to return a 401 because that get's intercepted - // by our angular helper because it thinks that we need to re-perform the request once we are - // authorized and we don't want to return a 403 because angular will show a warning msg indicating - // that the user doesn't have access to perform this function, we just want to return a normal invalid msg. - return Request.CreateResponse(HttpStatusCode.BadRequest); + return true; + } + return false; } diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 52d60e566f..507c229122 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -531,8 +531,9 @@ namespace Umbraco.Web.Editors var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - return ids.Select(id => Mapper.Map(Services.EntityService.Get(id, objectType.Value))) + var result = ids.Select(id => Mapper.Map(Services.EntityService.Get(id, objectType.Value))) .WhereNotNull(); + return result; } //now we need to convert the unknown ones switch (entityType) diff --git a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs b/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs index 3f6fcc5c2c..7ef4ef206a 100644 --- a/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs +++ b/src/Umbraco.Web/Editors/EntityControllerConfigurationAttribute.cs @@ -1,3 +1,4 @@ +using System; using System.Web.Http.Controllers; using Umbraco.Web.WebApi; @@ -10,12 +11,10 @@ namespace Umbraco.Web.Editors /// NOTE: It is SOOOO important to remember that you cannot just assign this in the 'initialize' method of a webapi /// controller as it will assign it GLOBALLY which is what you def do not want to do. /// - internal class EntityControllerConfigurationAttribute : AngularJsonOnlyConfigurationAttribute, IControllerConfiguration + internal class EntityControllerConfigurationAttribute : Attribute, IControllerConfiguration { - public override void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { - base.Initialize(controllerSettings, controllerDescriptor); - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new EntityControllerActionSelector()); } } diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 39b872af6d..54938be76d 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -104,6 +104,8 @@ namespace Umbraco.Web.Editors return GetResized(imagePath, width, Convert.ToString(width)); } + //TODO: We should delegate this to ImageProcessing + /// /// Gets a resized image - if the requested max width is greater than the original image, only the original image will be returned. /// diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 383f78d28e..1c9486c67d 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -37,6 +37,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] + [OutgoingNoHyphenGuidFormat] public class MemberController : ContentControllerBase { /// @@ -58,17 +59,7 @@ namespace Umbraco.Web.Editors _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); } - private MembershipProvider _provider; - - /// - /// Ensure all GUIDs are formatted without hyphens - /// - /// - protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext) - { - base.Initialize(controllerContext); - controllerContext.SetOutgoingNoHyphenGuidFormat(); - } + private readonly MembershipProvider _provider; /// /// Returns the currently configured membership scenario for members in umbraco diff --git a/src/Umbraco.Web/Editors/TagExtractor.cs b/src/Umbraco.Web/Editors/TagExtractor.cs index f0de24d6e8..361e7700c9 100644 --- a/src/Umbraco.Web/Editors/TagExtractor.cs +++ b/src/Umbraco.Web/Editors/TagExtractor.cs @@ -43,15 +43,15 @@ namespace Umbraco.Web.Editors LogHelper.Error("Could not create custom " + attribute.TagPropertyDefinitionType + " tag definition", ex); throw; } - SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType); + SetPropertyTags(content, property, convertedPropertyValue, def.Delimiter, def.ReplaceTags, def.TagGroup, attribute.ValueType, def.StorageType); } else { - SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType); + SetPropertyTags(content, property, convertedPropertyValue, attribute.Delimiter, attribute.ReplaceTags, attribute.TagGroup, attribute.ValueType, attribute.StorageType); } } - public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType) + public static void SetPropertyTags(IContentBase content, Property property, object convertedPropertyValue, string delimiter, bool replaceTags, string tagGroup, TagValueType valueType, TagCacheStorageType storageType) { if (convertedPropertyValue == null) { @@ -62,14 +62,14 @@ namespace Umbraco.Web.Editors { case TagValueType.FromDelimitedValue: var tags = convertedPropertyValue.ToString().Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - content.SetTags(property.Alias, tags, replaceTags, tagGroup); + content.SetTags(storageType, property.Alias, tags, replaceTags, tagGroup); break; case TagValueType.CustomTagList: //for this to work the object value must be IENumerable var stringList = convertedPropertyValue as IEnumerable; if (stringList != null) { - content.SetTags(property.Alias, stringList, replaceTags, tagGroup); + content.SetTags(storageType, property.Alias, stringList, replaceTags, tagGroup); } break; } diff --git a/src/Umbraco.Web/Models/DetachedContent.cs b/src/Umbraco.Web/Models/DetachedContent.cs index e572de656c..df4effa078 100644 --- a/src/Umbraco.Web/Models/DetachedContent.cs +++ b/src/Umbraco.Web/Models/DetachedContent.cs @@ -10,21 +10,33 @@ namespace Umbraco.Web.Models { private readonly Dictionary _properties; + /// + /// Initialized a new instance of the class with properties. + /// + /// The properties + /// Properties must be detached or nested properties ie their property type must be detached or nested. + /// Such a detached content can be part of a published property value. public DetachedContent(IEnumerable properties) { _properties = properties.ToDictionary(x => x.PropertyTypeAlias, x => x, StringComparer.InvariantCultureIgnoreCase); } + // don't uncomment until you know what you are doing + /* public DetachedContent(IPublishedContent content) { _properties = content.Properties.ToDictionary(x => x.PropertyTypeAlias, x => x, StringComparer.InvariantCultureIgnoreCase); } + */ + // don't uncomment until you know what you are doing + // at the moment, I don't fully + /* public DetachedContent(IContent content, bool isPreviewing) { var publishedContentType = PublishedContentType.Get(PublishedItemType.Content, content.ContentType.Alias); _properties = PublishedProperty.MapProperties(publishedContentType.PropertyTypes, content.Properties, - (t, v) => PublishedProperty.GetDetached(t, v, isPreviewing)) + (t, v) => PublishedProperty.GetDetached(t.Detached(), v, isPreviewing)) .ToDictionary(x => x.PropertyTypeAlias, x => x, StringComparer.InvariantCultureIgnoreCase); } @@ -32,7 +44,7 @@ namespace Umbraco.Web.Models { var publishedContentType = PublishedContentType.Get(PublishedItemType.Media, media.ContentType.Alias); _properties = PublishedProperty.MapProperties(publishedContentType.PropertyTypes, media.Properties, - (t, v) => PublishedProperty.GetDetached(t, v, isPreviewing)) + (t, v) => PublishedProperty.GetDetached(t.Detached(), v, isPreviewing)) .ToDictionary(x => x.PropertyTypeAlias, x => x, StringComparer.InvariantCultureIgnoreCase); } @@ -40,9 +52,10 @@ namespace Umbraco.Web.Models { var publishedContentType = PublishedContentType.Get(PublishedItemType.Member, member.ContentType.Alias); _properties = PublishedProperty.MapProperties(publishedContentType.PropertyTypes, member.Properties, - (t, v) => PublishedProperty.GetDetached(t, v, isPreviewing)) + (t, v) => PublishedProperty.GetDetached(t.Detached(), v, isPreviewing)) .ToDictionary(x => x.PropertyTypeAlias, x => x, StringComparer.InvariantCultureIgnoreCase); } + */ public ICollection Properties { diff --git a/src/Umbraco.Web/NotificationServiceExtensions.cs b/src/Umbraco.Web/NotificationServiceExtensions.cs index 8404a99a0e..9e10531b78 100644 --- a/src/Umbraco.Web/NotificationServiceExtensions.cs +++ b/src/Umbraco.Web/NotificationServiceExtensions.cs @@ -2,7 +2,9 @@ using System; using System.Globalization; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using umbraco; using umbraco.BusinessLogic.Actions; @@ -10,28 +12,64 @@ using umbraco.interfaces; namespace Umbraco.Web { + //NOTE: all of these require an UmbracoContext because currently to send the notifications we need an HttpContext, this is based on legacy code + // for which probably requires updating so that these can be sent outside of the http context. + internal static class NotificationServiceExtensions { internal static void SendNotification(this INotificationService service, IUmbracoEntity entity, IAction action, ApplicationContext applicationContext) { - if (global::Umbraco.Web.UmbracoContext.Current == null) return; - service.SendNotification(entity, action, global::Umbraco.Web.UmbracoContext.Current); + if (UmbracoContext.Current == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } + service.SendNotification(entity, action, UmbracoContext.Current); } internal static void SendNotification(this INotificationService service, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext) { - if (umbracoContext == null) return; + if (umbracoContext == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } service.SendNotification(entity, action, umbracoContext, umbracoContext.Application); } internal static void SendNotification(this INotificationService service, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) { + if (umbracoContext == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Cannot send notifications, there is no current UmbracoContext"); + return; + } + + var user = umbracoContext.Security.CurrentUser; + + //if there is no current user, then use the admin + if (user == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "There is no current Umbraco user logged in, the notifications will be sent from the administrator"); + user = applicationContext.Services.UserService.GetUserById(0); + if (user == null) + { + LogHelper.Warn(typeof(NotificationServiceExtensions), "Noticiations can not be sent, no admin user with id 0 could be resolved"); + return; + } + } + service.SendNotification(user, entity, action, umbracoContext, applicationContext); + } + + internal static void SendNotification(this INotificationService service, IUser sender, IUmbracoEntity entity, IAction action, UmbracoContext umbracoContext, ApplicationContext applicationContext) + { + if (sender == null) throw new ArgumentNullException("sender"); if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - var user = umbracoContext.Security.CurrentUser; + applicationContext.Services.NotificationService.SendNotifications( - user, + sender, entity, action.Letter.ToString(CultureInfo.InvariantCulture), ui.Text("actions", action.Alias), diff --git a/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs b/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs index 2754af85f4..8e721366b8 100644 --- a/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs +++ b/src/Umbraco.Web/PropertyEditors/TagPropertyEditorTagDefinition.cs @@ -1,10 +1,13 @@ -using Umbraco.Core.Models.Editors; +using System; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { /// - /// Used to dynamically change the tag group based on the pre-values + /// Used to dynamically change the tag group and storage type based on the pre-values /// internal class TagPropertyEditorTagDefinition : TagPropertyDefinition { @@ -21,5 +24,16 @@ namespace Umbraco.Web.PropertyEditors return preVals.ContainsKey("group") ? preVals["group"].Value : "default"; } } + + public override TagCacheStorageType StorageType + { + get + { + var preVals = PropertySaving.PreValues.FormatAsDictionary(); + return preVals.ContainsKey("storageType") + ? Enum.Parse(preVals["storageType"].Value) + : TagCacheStorageType.Csv; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index dc42b74e0c..49fb5ddbc4 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [SupportTags(typeof(TagPropertyEditorTagDefinition))] + [SupportTags(typeof(TagPropertyEditorTagDefinition), ValueType = TagValueType.CustomTagList)] [PropertyEditor(Constants.PropertyEditors.TagsAlias, "Tags", "tags")] public class TagsPropertyEditor : PropertyEditor { @@ -12,7 +17,8 @@ namespace Umbraco.Web.PropertyEditors { _defaultPreVals = new Dictionary { - {"group", "default"} + {"group", "default"}, + {"storageType", TagCacheStorageType.Csv.ToString()} }; } @@ -27,11 +33,36 @@ namespace Umbraco.Web.PropertyEditors set { _defaultPreVals = value; } } + protected override PropertyValueEditor CreateValueEditor() + { + return new TagPropertyValueEditor(base.CreateValueEditor()); + } + protected override PreValueEditor CreatePreValueEditor() { return new TagPreValueEditor(); } + internal class TagPropertyValueEditor : PropertyValueEditorWrapper + { + public TagPropertyValueEditor(PropertyValueEditor wrapped) + : base(wrapped) + { + } + + /// + /// This needs to return IEnumerable{string} + /// + /// + /// + /// + public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) + { + var json = editorValue.Value as JArray; + return json == null ? null : json.Select(x => x.Value()); + } + } + internal class TagPreValueEditor : PreValueEditor { public TagPreValueEditor() @@ -43,11 +74,26 @@ namespace Umbraco.Web.PropertyEditors Name = "Tag group", View = "requiredfield" }); + + Fields.Add(new PreValueField(new ManifestPropertyValidator {Type = "Required"}) + { + Description = "Select whether to store the tags in cache as CSV (default) or as JSON. The only benefits of storage as JSON is that you are able to have commas in a tag value but this will require parsing the json in your views or using a property value converter", + Key = "storageType", + Name = "Storage Type", + View = "views/propertyeditors/tags/tags.prevalues.html" + }); } - public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, Core.Models.PreValueCollection persistedPreVals) + public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) { var result = base.ConvertDbToEditor(defaultPreVals, persistedPreVals); + + //This is required because we've added this pre-value so old installs that don't have it will need to have a default. + if (result.ContainsKey("storageType") == false || result["storageType"] == null || result["storageType"].ToString().IsNullOrWhiteSpace()) + { + result["storageType"] = TagCacheStorageType.Csv.ToString(); + } + return result; } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index dee2c47751..badbe39773 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -329,7 +329,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //lets check if the alias does not exist on the document. //NOTE: Examine will not index empty values and we do not output empty XML Elements to the cache - either of these situations // would mean that the property is missing from the collection whether we are getting the value from Examine or from the library media cache. - if (dd.Properties.All(x => x.PropertyTypeAlias != alias)) + if (dd.Properties.All(x => x.PropertyTypeAlias.InvariantEquals(alias) == false)) { return null; } diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 9f64b51e04..1efe0eed0d 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -159,12 +159,6 @@ namespace Umbraco.Web.Routing .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash)); if (domainAndUri != null) return domainAndUri; - // look for the first domain that the current url would be the base of - // ie current is www.example.com, look for domain www.example.com/foo/bar - domainAndUri = domainsAndUris - .FirstOrDefault(d => currentWithSlash.IsBaseOf(d.Uri.EndPathWithSlash())); - if (domainAndUri != null) return domainAndUri; - // if none matches, then try to run the filter to pick a domain if (filter != null) { diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 459e736e06..5e64f33a16 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -86,8 +86,8 @@ namespace Umbraco.Web.Security //This will occur if an email already exists! return Attempt.Fail(ex); } - - var member = GetCurrentMember(); + + var member = GetCurrentPersistedMember(); //NOTE: If changing the username is a requirement, than that needs to be done via the IMember directly since MembershipProvider's natively do // not support changing a username! @@ -206,6 +206,14 @@ namespace Umbraco.Web.Security return true; } + /// + /// Logs out the current member + /// + public void Logout() + { + FormsAuthentication.SignOut(); + } + #region Querying for front-end public IPublishedContent GetByProviderKey(object key) @@ -255,6 +263,35 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByEmail(email); return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, false)); } + + /// + /// Returns the currently logged in member as IPublishedContent + /// + /// + public IPublishedContent GetCurrentMember() + { + if (IsLoggedIn() == false) + { + return null; + } + var result = GetCurrentPersistedMember(); + var provider = MPE.GetMembersMembershipProvider(); + return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, true)); + } + + /// + /// Returns the currently logged in member id, -1 if they are not logged in + /// + /// + public int GetCurrentMemberId() + { + if (IsLoggedIn() == false) + { + return -1; + } + var result = GetCurrentMember(); + return result == null ? -1 : result.Id; + } #endregion @@ -276,7 +313,7 @@ namespace Umbraco.Web.Security if (provider.IsUmbracoMembershipProvider()) { var membershipUser = provider.GetCurrentUser(); - var member = GetCurrentMember(); + var member = GetCurrentPersistedMember(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying // to use the front-end! if (member == null) @@ -423,7 +460,7 @@ namespace Umbraco.Web.Security if (provider.IsUmbracoMembershipProvider()) { - var member = GetCurrentMember(); + var member = GetCurrentPersistedMember(); //this shouldn't happen but will if the member is deleted in the back office while the member is trying // to use the front-end! if (member == null) @@ -506,7 +543,7 @@ namespace Umbraco.Web.Security string username; if (provider.IsUmbracoMembershipProvider()) { - var member = GetCurrentMember(); + var member = GetCurrentPersistedMember(); username = member.Username; // If types defined, check member is of one of those types var allowTypesList = allowTypes as IList ?? allowTypes.ToList(); @@ -756,7 +793,7 @@ namespace Umbraco.Web.Security /// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity! /// /// - private IMember GetCurrentMember() + private IMember GetCurrentPersistedMember() { var provider = MPE.GetMembersMembershipProvider(); diff --git a/src/Umbraco.Web/Strategies/NotificationsHandler.cs b/src/Umbraco.Web/Strategies/NotificationsHandler.cs index dc77a7b745..3eb750c1fd 100644 --- a/src/Umbraco.Web/Strategies/NotificationsHandler.cs +++ b/src/Umbraco.Web/Strategies/NotificationsHandler.cs @@ -22,6 +22,10 @@ namespace Umbraco.Web.Strategies { base.ApplicationStarted(umbracoApplication, applicationContext); + ContentService.SentToPublish += (sender, args) => + applicationContext.Services.NotificationService.SendNotification( + args.Entity, ActionToPublish.Instance, applicationContext); + //Send notifications for the published action ContentService.Published += (sender, args) => args.PublishedEntities.ForEach( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 7d8f37768e..301e018e65 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -520,6 +520,7 @@ + diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index bb81aa96c7..9e7bca5fe5 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -437,8 +437,9 @@ namespace Umbraco.Web //currently assigned node. The PublishedContentRequest will be null if: // * we are rendering a partial view or child action // * we are rendering a view from a custom route - if (UmbracoContext.PublishedContentRequest == null + if ((UmbracoContext.PublishedContentRequest == null || UmbracoContext.PublishedContentRequest.PublishedContent.Id != currentPage.Id) + && currentPage.Id > 0) // in case we're rendering a detached content (id == 0) { item.NodeId = currentPage.Id.ToString(); } diff --git a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs index b867ae2f81..66a79376aa 100644 --- a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs +++ b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs @@ -32,39 +32,22 @@ namespace Umbraco.Web.WebApi /// public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) { - //Before we were calling the base method to do this however it was causing problems: - // http://issues.umbraco.org/issue/U4-4546 - // though I can't seem to figure out why the null ref exception was being thrown, it is very strange. - // This code is basically what the base class does and at least we can track/test exactly what is going on. if (type == null) throw new ArgumentNullException("type"); if (writeStream == null) throw new ArgumentNullException("writeStream"); - - var task = Task.Factory.StartNew(() => + + var effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers); + + using (var streamWriter = new StreamWriter(writeStream, effectiveEncoding)) { - var effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers); + //write the special encoding for angular json to the start + // (see: http://docs.angularjs.org/api/ng.$http) + streamWriter.Write(")]}',\n"); + streamWriter.Flush(); + return base.WriteToStreamAsync(type, value, writeStream, content, transportContext); + } - using (var streamWriter = new StreamWriter(writeStream, effectiveEncoding)) - using (var jsonTextWriter = new JsonTextWriter(streamWriter) - { - CloseOutput = false - }) - { - //write the special encoding for angular json to the start - // (see: http://docs.angularjs.org/api/ng.$http) - streamWriter.Write(")]}',\n"); - - if (Indent) - { - jsonTextWriter.Formatting = Formatting.Indented; - } - var jsonSerializer = JsonSerializer.Create(SerializerSettings); - jsonSerializer.Serialize(jsonTextWriter, value); - - jsonTextWriter.Flush(); - } - }); - return task; + } } diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs index a2f3373999..d8e185963a 100644 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs @@ -1,4 +1,8 @@ -using System.Web.Http.Filters; +using System; +using System.Linq; +using System.Net.Http.Formatting; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; using Umbraco.Core; namespace Umbraco.Web.WebApi.Filters @@ -6,9 +10,9 @@ namespace Umbraco.Web.WebApi.Filters /// /// Sets the json outgoing/serialized datetime format /// - internal sealed class OutgoingDateTimeFormatAttribute : ActionFilterAttribute + internal sealed class OutgoingDateTimeFormatAttribute : Attribute, IControllerConfiguration { - private readonly string _format; + private readonly string _format = "yyyy-MM-dd HH:mm:ss"; /// /// Specify a custom format @@ -23,24 +27,18 @@ namespace Umbraco.Web.WebApi.Filters /// /// Will use the standard ISO format /// - public OutgoingDateTimeFormatAttribute() - { + public OutgoingDateTimeFormatAttribute(){ } - public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { - base.OnActionExecuting(actionContext); - - if (_format.IsNullOrWhiteSpace()) + var jsonFormatter = controllerSettings.Formatters.OfType(); + foreach (var r in jsonFormatter) { - actionContext.ControllerContext.SetOutgoingDateTimeFormat(); + r.SerializerSettings.Converters.Add(new CustomDateTimeConvertor(_format)); } - else - { - actionContext.ControllerContext.SetOutgoingDateTimeFormat(_format); - } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs new file mode 100644 index 0000000000..897213a142 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Net.Http.Formatting; +using System.Web.Http.Controllers; + +namespace Umbraco.Web.WebApi.Filters +{ + internal sealed class OutgoingNoHyphenGuidFormatAttribute : Attribute, IControllerConfiguration + { + public OutgoingNoHyphenGuidFormatAttribute() + { + } + + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + var jsonFormatter = controllerSettings.Formatters.OfType(); + foreach (var r in jsonFormatter) + { + r.SerializerSettings.Converters.Add(new GuidNoHyphenConverter()); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs b/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs index a7e7160ae2..a3cb3b9d22 100644 --- a/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpControllerContextExtensions.cs @@ -55,61 +55,12 @@ namespace Umbraco.Web.WebApi /// private static async Task FilterContinuation(HttpActionContext actionContext, CancellationToken token, IList filters, int index) { - return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token, () => - { - Func nullResponse = () => null; - return (index + 1) == filters.Count - ? Task.Run(nullResponse) - : FilterContinuation(actionContext, token, filters, ++index); - }); + return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token, + () => (index + 1) == filters.Count + ? Task.FromResult(null) + : FilterContinuation(actionContext, token, filters, ++index)); } + - /// - /// Sets the JSON GUID format to not have hyphens - /// - /// - internal static void SetOutgoingNoHyphenGuidFormat(this HttpControllerContext controllerContext) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new GuidNoHyphenConverter()); - } - - - /// - /// Sets the JSON datetime format to be a custom one - /// - /// - /// - internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext, string format) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor(format)); - } - - /// - /// Sets the JSON datetime format to be our regular iso standard - /// - internal static void SetOutgoingDateTimeFormat(this HttpControllerContext controllerContext) - { - var jsonFormatter = controllerContext.Configuration.Formatters.JsonFormatter; - jsonFormatter.SerializerSettings.Converters.Add(new CustomDateTimeConvertor("yyyy-MM-dd HH:mm:ss")); - } - - ///// - ///// Removes the xml formatter so it only outputs angularized json (with the json vulnerability prefix added) - ///// - ///// - //internal static void EnsureJsonOutputOnly(this HttpControllerContext controllerContext) - //{ - // controllerContext.Configuration.Formatters = new MediaTypeFormatterCollection(); - - // //remove all json/xml formatters then add our custom one - // var toRemove = controllerContext.Configuration.Formatters.Where(t => (t is JsonMediaTypeFormatter) || (t is XmlMediaTypeFormatter)).ToList(); - // foreach (var r in toRemove) - // { - // controllerContext.Configuration.Formatters.Remove(r); - // } - // controllerContext.Configuration.Formatters.Add(new AngularJsonMediaTypeFormatter()); - //} } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebServices/CoreStringsController.cs b/src/Umbraco.Web/WebServices/CoreStringsController.cs index 6da704a520..de7bcd55d5 100644 --- a/src/Umbraco.Web/WebServices/CoreStringsController.cs +++ b/src/Umbraco.Web/WebServices/CoreStringsController.cs @@ -18,6 +18,7 @@ namespace Umbraco.Web.WebServices public class CoreStringsController : UmbracoAuthorizedController { [HttpGet] + [ValidateInput(false)] public JsonResult ToSafeAlias(string value, bool camelCase = true) { // always return a proper camel-cased alias diff --git a/src/Umbraco.Web/WebServices/FolderBrowserService.cs b/src/Umbraco.Web/WebServices/FolderBrowserService.cs index 2c8cff527d..bf5a3f1bd5 100644 --- a/src/Umbraco.Web/WebServices/FolderBrowserService.cs +++ b/src/Umbraco.Web/WebServices/FolderBrowserService.cs @@ -32,9 +32,14 @@ namespace Umbraco.Web.WebServices var entities = service.GetChildren(parentId, UmbracoObjectTypes.Media); foreach (UmbracoEntity entity in entities) { - var uploadFieldProperty = entity.UmbracoProperties.FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); - - var thumbnailUrl = uploadFieldProperty == null ? "" : ThumbnailProvidersResolver.Current.GetThumbnailUrl(uploadFieldProperty.Value); + var uploadFieldProperty = entity.AdditionalData + .Select(x => x.Value as UmbracoEntity.EntityProperty) + .Where(x => x != null) + .FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); + + //var uploadFieldProperty = entity.UmbracoProperties.FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); + + var thumbnailUrl = uploadFieldProperty == null ? "" : ThumbnailProvidersResolver.Current.GetThumbnailUrl((string)uploadFieldProperty.Value); var item = new { diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index d2a2aada5a..720e2ba9de 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -85,31 +85,32 @@ namespace umbraco var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } - else if (elements[_fieldName] != null && string.IsNullOrEmpty(elements[_fieldName].ToString()) == false) - { + else + { //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) - _fieldContent = elements[_fieldName].ToString().Trim(); + var elt = elements[_fieldName]; + if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) + _fieldContent = elt.ToString().Trim(); } //now we check if the value is still empty and if so we'll check useIfEmpty if (string.IsNullOrEmpty(_fieldContent)) { - //if useIfEmpty is true - if (string.IsNullOrEmpty(helper.FindAttribute(attributes, "useIfEmpty")) == false) + var altFieldName = helper.FindAttribute(attributes, "useIfEmpty"); + if (string.IsNullOrEmpty(altFieldName) == false) { - var altFieldName = helper.FindAttribute(attributes, "useIfEmpty"); - - //check for published content and get its value using that if (publishedContent != null && publishedContent.HasProperty(altFieldName)) { var pval = publishedContent.GetPropertyValue(altFieldName); var rval = pval == null ? string.Empty : pval.ToString(); _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } - else if (elements[altFieldName] != null && string.IsNullOrEmpty(elements[altFieldName].ToString()) == false) + else { //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) - _fieldContent = elements[altFieldName].ToString().Trim(); + var elt = elements[altFieldName]; + if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) + _fieldContent = elt.ToString().Trim(); } } } diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 2fd71b1458..f70898cfb6 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -342,6 +342,9 @@ namespace umbraco } catch (Exception e) { + LogHelper.WarnWithException( + "Error loading partial view macro (View: " + Model.ScriptName + ")", true, e); + renderFailed = true; Exceptions.Add(e); macroControl = handleError(e); @@ -1666,6 +1669,12 @@ namespace umbraco public static string MacroContentByHttp(int PageID, Guid PageVersion, Hashtable attributes) { + + if (SystemUtilities.GetCurrentTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) + { + return "Cannot render macro content in the rich text editor when the application is running in a Partial Trust environment"; + } + string tempAlias = (attributes["macroalias"] != null) ? attributes["macroalias"].ToString() : attributes["macroAlias"].ToString(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs index 0b48f7a2e1..f72457d867 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs @@ -21,28 +21,28 @@ namespace umbraco.cms.presentation.Trees { [Obsolete("This is no longer used and will be removed from the codebase in the future")] - public abstract class BaseMediaTree : BaseTree - { + public abstract class BaseMediaTree : BaseTree + { private User _user; - public BaseMediaTree(string application) - : base(application) - { - - } + public BaseMediaTree(string application) + : base(application) + { + + } + + /// + /// Returns the current User. This ensures that we don't instantiate a new User object + /// each time. + /// + protected User CurrentUser + { + get + { + return (_user == null ? (_user = UmbracoEnsuredPage.CurrentUser) : _user); + } + } - /// - /// Returns the current User. This ensures that we don't instantiate a new User object - /// each time. - /// - protected User CurrentUser - { - get - { - return (_user == null ? (_user = UmbracoEnsuredPage.CurrentUser) : _user); - } - } - public override void RenderJS(ref StringBuilder Javascript) { if (!string.IsNullOrEmpty(this.FunctionToCall)) @@ -54,7 +54,7 @@ namespace umbraco.cms.presentation.Trees else if (!this.IsDialog) { Javascript.Append( - @" + @" function openMedia(id) { " + ClientTools.Scripts.GetContentFrame() + ".location.href = 'editMedia.aspx?id=' + id;" + @" } @@ -98,12 +98,12 @@ function openMedia(id) { // to call so that is fine. var entities = Services.EntityService.GetChildren(m_id, UmbracoObjectTypes.Media).ToArray(); - + foreach (UmbracoEntity entity in entities) { var e = entity; var xNode = PerformNodeRender(e.Id, entity.Name, e.HasChildren, e.ContentTypeIcon, e.ContentTypeAlias, () => GetLinkValue(e)); - + OnBeforeNodeRender(ref tree, ref xNode, EventArgs.Empty); if (xNode != null) { @@ -111,7 +111,7 @@ function openMedia(id) { OnAfterNodeRender(ref tree, ref xNode, EventArgs.Empty); } } - } + } } private XmlTreeNode PerformNodeRender(int nodeId, string nodeName, bool hasChildren, string icon, string contentTypeAlias, Func getLinkValue) @@ -164,28 +164,28 @@ function openMedia(id) { return xNode; } - + /// - /// Returns the value for a link in WYSIWYG mode, by default only media items that have a - /// DataTypeUploadField are linkable, however, a custom tree can be created which overrides - /// this method, or another GUID for a custom data type can be added to the LinkableMediaDataTypes - /// list on application startup. - /// - /// - /// - /// + /// Returns the value for a link in WYSIWYG mode, by default only media items that have a + /// DataTypeUploadField are linkable, however, a custom tree can be created which overrides + /// this method, or another GUID for a custom data type can be added to the LinkableMediaDataTypes + /// list on application startup. + /// + /// + /// + /// public virtual string GetLinkValue(Media dd, string nodeLink) { var props = dd.GenericProperties; - foreach (Property p in props) - { - Guid currId = p.PropertyType.DataTypeDefinition.DataType.Id; - if (LinkableMediaDataTypes.Contains(currId) && string.IsNullOrEmpty(p.Value.ToString()) == false) - { - return p.Value.ToString(); - } - } + foreach (Property p in props) + { + Guid currId = p.PropertyType.DataTypeDefinition.DataType.Id; + if (LinkableMediaDataTypes.Contains(currId) && string.IsNullOrEmpty(p.Value.ToString()) == false) + { + return p.Value.ToString(); + } + } return ""; } @@ -200,29 +200,34 @@ function openMedia(id) { /// internal virtual string GetLinkValue(UmbracoEntity entity) { - foreach (var property in entity.UmbracoProperties) + foreach (var property in entity.AdditionalData + .Select(x => x.Value as UmbracoEntity.EntityProperty) + .Where(x => x != null)) { + + //required for backwards compatibility with v7 with changing the GUID -> alias var controlId = LegacyPropertyEditorIdToAliasConverter.GetLegacyIdFromAlias(property.PropertyEditorAlias, LegacyPropertyEditorIdToAliasConverter.NotFoundLegacyIdResponseBehavior.ReturnNull); if (controlId != null) { - if (LinkableMediaDataTypes.Contains(controlId.Value) && - string.IsNullOrEmpty(property.Value) == false) - return property.Value; - } + if (LinkableMediaDataTypes.Contains(controlId.Value) + && string.IsNullOrEmpty((string)property.Value) == false) + + return property.Value.ToString(); + } } return ""; } - /// - /// By default, any media type that is to be "linkable" in the WYSIWYG editor must contain - /// a DataTypeUploadField data type which will ouput the value for the link, however, if - /// a developer wants the WYSIWYG editor to link to a custom media type, they will either have - /// to create their own media tree and inherit from this one and override the GetLinkValue - /// or add another GUID to the LinkableMediaDataType list on application startup that matches - /// the GUID of a custom data type. The order of property types on the media item definition will determine the output value. - /// - public static List LinkableMediaDataTypes { get; protected set; } + /// + /// By default, any media type that is to be "linkable" in the WYSIWYG editor must contain + /// a DataTypeUploadField data type which will ouput the value for the link, however, if + /// a developer wants the WYSIWYG editor to link to a custom media type, they will either have + /// to create their own media tree and inherit from this one and override the GetLinkValue + /// or add another GUID to the LinkableMediaDataType list on application startup that matches + /// the GUID of a custom data type. The order of property types on the media item definition will determine the output value. + /// + public static List LinkableMediaDataTypes { get; protected set; } /// /// Returns true if we can use the EntityService to render the tree or revert to the original way @@ -245,5 +250,5 @@ function openMedia(id) { } } - } + } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs index 788d9a26f2..50fb8b912e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs @@ -39,9 +39,8 @@ namespace umbraco.cms.presentation.developer { _macro = Services.MacroService.GetById(Convert.ToInt32(Request.QueryString["macroID"])); - if (!IsPostBack) + if (IsPostBack == false) { - ClientTools .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) .SyncTree("-1,init," + _macro.Id.ToString(), false); @@ -77,57 +76,6 @@ namespace umbraco.cms.presentation.developer userControlList.Items.Insert(0, new ListItem("Browse usercontrols on server...", string.Empty)); } - else - { - Page.Validate(); - - ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree("-1,init," + _macro.Id.ToInvariantString(), true); //true forces the reload - - var tempMacroAssembly = macroAssembly.Text; - var tempMacroType = macroType.Text; - var tempCachePeriod = cachePeriod.Text; - if (tempCachePeriod == string.Empty) - tempCachePeriod = "0"; - if (tempMacroAssembly == string.Empty && macroUserControl.Text != string.Empty) - tempMacroType = macroUserControl.Text; - - SetMacroValuesFromPostBack(_macro, Convert.ToInt32(tempCachePeriod), tempMacroAssembly, tempMacroType); - - // Save elements - var sort = 0; - foreach (RepeaterItem item in macroProperties.Items) - { - var macroPropertyId = (HtmlInputHidden)item.FindControl("macroPropertyID"); - var macroElementName = (TextBox)item.FindControl("macroPropertyName"); - var macroElementAlias = (TextBox)item.FindControl("macroPropertyAlias"); - var macroElementType = (DropDownList)item.FindControl("macroPropertyType"); - - var prop = _macro.Properties.Single(x => x.Id == int.Parse(macroPropertyId.Value)); - prop.Alias = macroElementAlias.Text.Trim(); - prop.Name = macroElementName.Text.Trim(); - prop.EditorAlias = macroElementType.SelectedValue; - prop.SortOrder = sort; - sort++; - } - - Services.MacroService.Save(_macro); - - ClientTools.ShowSpeechBubble(speechBubbleIcon.save, "Macro saved", ""); - - // Check for assemblyBrowser - if (tempMacroType.IndexOf(".ascx", StringComparison.Ordinal) > 0) - assemblyBrowserUserControl.Controls.Add( - new LiteralControl("
")); - else if (tempMacroType != string.Empty && tempMacroAssembly != string.Empty) - assemblyBrowser.Controls.Add( - new LiteralControl("
")); - } } /// @@ -267,7 +215,17 @@ namespace umbraco.cms.presentation.developer } public void macroPropertyCreate(object sender, EventArgs e) - { + { + //enable add validators + var val1 = (RequiredFieldValidator)((Control)sender).Parent.FindControl("RequiredFieldValidator1"); + var val2 = (RequiredFieldValidator)((Control)sender).Parent.FindControl("RequiredFieldValidator4"); + var val3 = (RequiredFieldValidator)((Control)sender).Parent.FindControl("RequiredFieldValidator5"); + val1.Enabled = true; + val2.Enabled = true; + val3.Enabled = true; + + Page.Validate(); + if (Page.IsValid == false) { return; @@ -336,25 +294,90 @@ namespace umbraco.cms.presentation.developer } protected override void OnInit(EventArgs e) - { - // Tab setup - InfoTabPage = TabView1.NewTabPage("Macro Properties"); - InfoTabPage.Controls.Add(Pane1); - InfoTabPage.Controls.Add(Pane1_2); - InfoTabPage.Controls.Add(Pane1_3); - InfoTabPage.Controls.Add(Pane1_4); + { + base.OnInit(e); + EnsureChildControls(); + } - Parameters = TabView1.NewTabPage("Parameters"); - Parameters.Controls.Add(Panel2); + protected override void CreateChildControls() + { + base.CreateChildControls(); + + // Tab setup + InfoTabPage = TabView1.NewTabPage("Macro Properties"); + InfoTabPage.Controls.Add(Pane1); + InfoTabPage.Controls.Add(Pane1_2); + InfoTabPage.Controls.Add(Pane1_3); + InfoTabPage.Controls.Add(Pane1_4); + + Parameters = TabView1.NewTabPage("Parameters"); + Parameters.Controls.Add(Panel2); MenuButton save = TabView1.Menu.NewButton(); save.ButtonType = MenuButtonType.Primary; save.Text = ui.Text("save"); save.ID = "save"; - base.OnInit(e); - } + save.Click += Save_Click; + } - /// + void Save_Click(object sender, EventArgs e) + { + + Page.Validate(); + + ClientTools + .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) + .SyncTree("-1,init," + _macro.Id.ToInvariantString(), true); //true forces the reload + + var tempMacroAssembly = macroAssembly.Text; + var tempMacroType = macroType.Text; + var tempCachePeriod = cachePeriod.Text; + if (tempCachePeriod == string.Empty) + tempCachePeriod = "0"; + if (tempMacroAssembly == string.Empty && macroUserControl.Text != string.Empty) + tempMacroType = macroUserControl.Text; + + SetMacroValuesFromPostBack(_macro, Convert.ToInt32(tempCachePeriod), tempMacroAssembly, tempMacroType); + + // Save elements + var sort = 0; + foreach (RepeaterItem item in macroProperties.Items) + { + var macroPropertyId = (HtmlInputHidden)item.FindControl("macroPropertyID"); + var macroElementName = (TextBox)item.FindControl("macroPropertyName"); + var macroElementAlias = (TextBox)item.FindControl("macroPropertyAlias"); + var macroElementType = (DropDownList)item.FindControl("macroPropertyType"); + + var prop = _macro.Properties.Single(x => x.Id == int.Parse(macroPropertyId.Value)); + + _macro.Properties.UpdateProperty( + prop.Alias, + macroElementName.Text.Trim(), + sort, + macroElementType.SelectedValue, + macroElementAlias.Text.Trim()); + + sort++; + } + + Services.MacroService.Save(_macro); + + ClientTools.ShowSpeechBubble(speechBubbleIcon.save, "Macro saved", ""); + + // Check for assemblyBrowser + if (tempMacroType.IndexOf(".ascx", StringComparison.Ordinal) > 0) + assemblyBrowserUserControl.Controls.Add( + new LiteralControl("
")); + else if (tempMacroType != string.Empty && tempMacroAssembly != string.Empty) + assemblyBrowser.Controls.Add( + new LiteralControl("
")); + } + + /// /// TabView1 control. /// /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/nodeFactory/Node.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/nodeFactory/Node.cs index 954d36f699..104c30f827 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/nodeFactory/Node.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/nodeFactory/Node.cs @@ -551,7 +551,13 @@ namespace umbraco.NodeFactory { if (HttpContext.Current.Items["pageID"] == null) throw new InvalidOperationException("There is no current node."); - return (int)HttpContext.Current.Items["pageID"]; + + var intAttempt = HttpContext.Current.Items["pageID"].TryConvertTo(); + if (intAttempt == false) + { + throw new InvalidOperationException("The pageID value in the HttpContext.Items cannot be converted to an integer"); + } + return intAttempt.Result; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 9ec1c69546..2c71e529a4 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -491,10 +491,10 @@ namespace umbraco.cms.presentation.user } u.StartMediaId = mstartNode; - u.clearApplications(); + u.ClearApplications(); foreach (ListItem li in lapps.Items) { - if (li.Selected) u.addApplication(li.Value); + if (li.Selected) u.AddApplication(li.Value); } u.Save(); diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNode.cs b/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNode.cs index 050c8cf224..bb7144de75 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNode.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNode.cs @@ -887,7 +887,7 @@ namespace umbraco.MacroEngines //this is from SqlMetal and just makes it a bit of fun to allow pluralisation private static string MakePluralName(string name) { - if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("ss", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) + if ((name.EndsWith("x", StringComparison.OrdinalIgnoreCase) || name.EndsWith("ch", StringComparison.OrdinalIgnoreCase)) || (name.EndsWith("s", StringComparison.OrdinalIgnoreCase) || name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))) { name = name + "es"; return name; diff --git a/src/umbraco.businesslogic/UmbracoSettings.cs b/src/umbraco.businesslogic/UmbracoSettings.cs index 497226300d..ec68aa588d 100644 --- a/src/umbraco.businesslogic/UmbracoSettings.cs +++ b/src/umbraco.businesslogic/UmbracoSettings.cs @@ -15,7 +15,7 @@ namespace umbraco /// /// The UmbracoSettings Class contains general settings information for the entire Umbraco instance based on information from the /config/umbracoSettings.config file /// - [Obsolete("Use UmbracoConfiguration.Current.UmbracoSettings instead, it offers all settings in strongly typed formats. This class will be removed in future versions.")] + [Obsolete("Use UmbracoConfig.For.UmbracoSettings() instead, it offers all settings in strongly typed formats. This class will be removed in future versions.")] public class UmbracoSettings { [Obsolete("This hasn't been used since 4.1!")] diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index ffa6c790e0..798e4dc46c 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -689,9 +689,22 @@ namespace umbraco.BusinessLogic get { return _user.Id; } } + /// + /// Clears the list of applications the user has access to, ensure to call Save afterwords + /// + public void ClearApplications() + { + if (_lazyId.HasValue) SetupUser(_lazyId.Value); + foreach (var s in _user.AllowedSections.ToArray()) + { + _user.RemoveAllowedSection(s); + } + } + /// /// Clears the list of applications the user has access to. /// + [Obsolete("This method will implicitly cause a database save and will reset the current user's dirty property, do not use this method, use the ClearApplications method instead and then call Save() when you are done performing all user changes to persist the chagnes in one transaction")] public void clearApplications() { if (_lazyId.HasValue) SetupUser(_lazyId.Value); @@ -701,19 +714,32 @@ namespace umbraco.BusinessLogic _user.RemoveAllowedSection(s); } + //For backwards compatibility this requires an implicit save ApplicationContext.Current.Services.UserService.Save(_user); } + /// + /// Adds a application to the list of allowed applications, ensure to call Save() afterwords + /// + /// + public void AddApplication(string appAlias) + { + if (_lazyId.HasValue) SetupUser(_lazyId.Value); + _user.AddAllowedSection(appAlias); + } + /// /// Adds a application to the list of allowed applications /// /// The app alias. + [Obsolete("This method will implicitly cause a multiple database saves and will reset the current user's dirty property, do not use this method, use the AddApplication method instead and then call Save() when you are done performing all user changes to persist the chagnes in one transaction")] public void addApplication(string AppAlias) { if (_lazyId.HasValue) SetupUser(_lazyId.Value); _user.AddAllowedSection(AppAlias); + //For backwards compatibility this requires an implicit save ApplicationContext.Current.Services.UserService.Save(_user); }