diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 662f0b666a..e46b9f2d3c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.7.2")] -[assembly: AssemblyInformationalVersion("7.7.2")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.7.4")] +[assembly: AssemblyInformationalVersion("7.7.4")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 02f3322ec9..395502d1b2 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -538,24 +538,41 @@ namespace Umbraco.Core.Configuration internal static bool ContentCacheXmlStoredInCodeGen { - get { return ContentCacheXmlStorageLocation == ContentXmlStorage.AspNetTemp; } + get { return LocalTempStorageLocation == LocalTempStorage.AspNetTemp; } } - - internal static ContentXmlStorage ContentCacheXmlStorageLocation + + /// + /// This is the location type to store temporary files such as cache files or other localized files for a given machine + /// + /// + /// Currently used for the xml cache file and the plugin cache files + /// + internal static LocalTempStorage LocalTempStorageLocation { get - { + { + //there's a bunch of backwards compat config checks here.... + + //This is the current one + if (ConfigurationManager.AppSettings.ContainsKey("umbracoLocalTempStorage")) + { + return Enum.Parse(ConfigurationManager.AppSettings["umbracoLocalTempStorage"]); + } + + //This one is old if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLStorage")) { - return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]); - } + return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]); + } + + //This one is older if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp")) { return bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"]) - ? ContentXmlStorage.AspNetTemp - : ContentXmlStorage.Default; + ? LocalTempStorage.AspNetTemp + : LocalTempStorage.Default; } - return ContentXmlStorage.Default; + return LocalTempStorage.Default; } } diff --git a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs b/src/Umbraco.Core/Configuration/LocalTempStorage.cs similarity index 75% rename from src/Umbraco.Core/Configuration/ContentXmlStorage.cs rename to src/Umbraco.Core/Configuration/LocalTempStorage.cs index 7cbbc70675..d41f7d1925 100644 --- a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs +++ b/src/Umbraco.Core/Configuration/LocalTempStorage.cs @@ -1,6 +1,6 @@ namespace Umbraco.Core.Configuration { - internal enum ContentXmlStorage + internal enum LocalTempStorage { Default, AspNetTemp, diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 3bd45dabf2..43bfbc64a0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.7.2"); + private static readonly Version Version = new Version("7.7.4"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index d017505b4c..a58d983667 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -13,11 +13,28 @@ using Umbraco.Core.Configuration; namespace Umbraco.Core.IO { public static class IOHelper - { + { + /// + /// Gets or sets a value forcing Umbraco to consider it is non-hosted. + /// + /// This should always be false, unless unit testing. + public static bool ForceNotHosted { get; set; } + private static string _rootDir = ""; // static compiled regex for faster performance private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + /// + /// Gets a value indicating whether Umbraco is hosted. + /// + public static bool IsHosted + { + get + { + return ForceNotHosted == false && (HttpContext.Current != null || HostingEnvironment.IsHosted); + } + } public static char DirSepChar { @@ -72,14 +89,14 @@ namespace Umbraco.Core.IO internal static string ResolveUrlsFromTextString(string text) { if (UmbracoConfig.For.UmbracoSettings().Content.ResolveUrlsFromTextString) - { + { using (DisposableTimer.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) { // find all relative urls (ie. urls that contain ~) var tags = ResolveUrlPattern.Matches(text); - + foreach (Match tag in tags) - { + { string url = ""; if (tag.Groups[1].Success) url = tag.Groups[1].Value; @@ -97,7 +114,8 @@ namespace Umbraco.Core.IO public static string MapPath(string path, bool useHttpContext) { - if (path == null) throw new ArgumentNullException("path"); + if (path == null) throw new ArgumentNullException("path"); + useHttpContext = useHttpContext && IsHosted; // Check if the path is already mapped if ((path.Length >= 2 && path[1] == Path.VolumeSeparatorChar) @@ -303,7 +321,7 @@ namespace Umbraco.Core.IO var debugFolder = Path.Combine(binFolder, "debug"); if (Directory.Exists(debugFolder)) return debugFolder; -#endif +#endif var releaseFolder = Path.Combine(binFolder, "release"); if (Directory.Exists(releaseFolder)) return releaseFolder; diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs index 437ddd3ef7..ccf8ea5b2c 100644 --- a/src/Umbraco.Core/IO/SystemFiles.cs +++ b/src/Umbraco.Core/IO/SystemFiles.cs @@ -73,19 +73,19 @@ namespace Umbraco.Core.IO { get { - switch (GlobalSettings.ContentCacheXmlStorageLocation) + switch (GlobalSettings.LocalTempStorageLocation) { - case ContentXmlStorage.AspNetTemp: + case LocalTempStorage.AspNetTemp: return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config"); - case ContentXmlStorage.EnvironmentTemp: + case LocalTempStorage.EnvironmentTemp: var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); - var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoXml", - //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back - // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not - // utilizing an old path + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path appDomainHash); return Path.Combine(cachePath, "umbraco.config"); - case ContentXmlStorage.Default: + case LocalTempStorage.Default: return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config"); default: throw new ArgumentOutOfRangeException(); diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 012e9b6cc2..7042c63ec1 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Membership /// [Serializable] [DataContract(IsReference = true)] - internal class UserGroup : Entity, IUserGroup, IReadOnlyUserGroup + public class UserGroup : Entity, IUserGroup, IReadOnlyUserGroup { private int? _startContentId; private int? _startMediaId; diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index f90163d181..aa318de287 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using System.Web; using System.Web.Compilation; using Umbraco.Core.Cache; using Umbraco.Core.IO; @@ -15,6 +16,7 @@ using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; using umbraco.interfaces; +using Umbraco.Core.Configuration; using File = System.IO.File; namespace Umbraco.Core @@ -39,7 +41,8 @@ namespace Umbraco.Core private readonly IServiceProvider _serviceProvider; private readonly IRuntimeCacheProvider _runtimeCache; private readonly ProfilingLogger _logger; - private readonly string _tempFolder; + private readonly Lazy _pluginListFilePath = new Lazy(GetPluginListFilePath); + private readonly Lazy _pluginHashFilePath = new Lazy(GetPluginHashFilePath); private readonly object _typesLock = new object(); private readonly Dictionary _types = new Dictionary(); @@ -65,14 +68,7 @@ namespace Umbraco.Core _serviceProvider = serviceProvider; _runtimeCache = runtimeCache; _logger = logger; - - // the temp folder where the cache file lives - _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); - if (Directory.Exists(_tempFolder) == false) - Directory.CreateDirectory(_tempFolder); - - var pluginListFile = GetPluginListFilePath(); - + if (detectChanges) { //first check if the cached hash is string.Empty, if it is then we need @@ -84,7 +80,8 @@ namespace Umbraco.Core // 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); + if(File.Exists(_pluginListFilePath.Value)) + File.Delete(_pluginListFilePath.Value); WriteCachePluginsHash(); } @@ -94,7 +91,8 @@ namespace Umbraco.Core // 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); + if (File.Exists(_pluginListFilePath.Value)) + File.Delete(_pluginListFilePath.Value); // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; @@ -187,11 +185,10 @@ namespace Umbraco.Core { if (_cachedAssembliesHash != null) return _cachedAssembliesHash; + + if (File.Exists(_pluginHashFilePath.Value) == false) return string.Empty; - var filePath = GetPluginHashFilePath(); - if (File.Exists(filePath) == false) return string.Empty; - - var hash = File.ReadAllText(filePath, Encoding.UTF8); + var hash = File.ReadAllText(_pluginHashFilePath.Value, Encoding.UTF8); _cachedAssembliesHash = hash; return _cachedAssembliesHash; @@ -229,9 +226,8 @@ namespace Umbraco.Core /// Writes the assembly hash file. /// private void WriteCachePluginsHash() - { - var filePath = GetPluginHashFilePath(); - File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8); + { + File.WriteAllText(_pluginHashFilePath.Value, CurrentAssembliesHash, Encoding.UTF8); } /// @@ -274,7 +270,7 @@ namespace Umbraco.Core } } } - return generator.GenerateHash(); + return generator.GenerateHash(); } } } @@ -314,7 +310,7 @@ namespace Umbraco.Core uniqInfos.Add(fileOrFolder.FullName); generator.AddFileSystemItem(fileOrFolder); } - return generator.GenerateHash(); + return generator.GenerateHash(); } } } @@ -347,12 +343,11 @@ namespace Umbraco.Core { return ReadCache(); } - catch + catch (Exception ex) { try { - var filePath = GetPluginListFilePath(); - File.Delete(filePath); + File.Delete(_pluginListFilePath.Value); } catch { @@ -366,12 +361,11 @@ namespace Umbraco.Core internal Dictionary, IEnumerable> ReadCache() { var cache = new Dictionary, IEnumerable>(); - - var filePath = GetPluginListFilePath(); - if (File.Exists(filePath) == false) + + if (File.Exists(_pluginListFilePath.Value) == false) return cache; - using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) + using (var stream = GetFileStream(_pluginListFilePath.Value, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) { while (true) @@ -414,38 +408,86 @@ namespace Umbraco.Core /// Generally only used for resetting cache, for example during the install process. public void ClearPluginCache() { - var path = GetPluginListFilePath(); - if (File.Exists(path)) - File.Delete(path); - - path = GetPluginHashFilePath(); - if (File.Exists(path)) - File.Delete(path); + if (File.Exists(_pluginListFilePath.Value)) + File.Delete(_pluginListFilePath.Value); + + if (File.Exists(_pluginHashFilePath.Value)) + File.Delete(_pluginHashFilePath.Value); _runtimeCache.ClearCacheItem(CacheKey); } - private string GetPluginListFilePath() + private static string GetPluginListFilePath() { - var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list"; - return Path.Combine(_tempFolder, filename); + string pluginListFilePath; + switch (GlobalSettings.LocalTempStorageLocation) + { + case LocalTempStorage.AspNetTemp: + pluginListFilePath = Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco-plugins.list"); + break; + case LocalTempStorage.EnvironmentTemp: + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + pluginListFilePath = Path.Combine(cachePath, "umbraco-plugins.list"); + break; + case LocalTempStorage.Default: + default: + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); + pluginListFilePath = Path.Combine(tempFolder, "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list"); + break; + } + + //ensure the folder exists + var folder = Path.GetDirectoryName(pluginListFilePath); + if (folder == null) + throw new InvalidOperationException("The folder could not be determined for the file " + pluginListFilePath); + if (Directory.Exists(folder) == false) + Directory.CreateDirectory(folder); + + return pluginListFilePath; } - private string GetPluginHashFilePath() + private static string GetPluginHashFilePath() { - var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash"; - return Path.Combine(_tempFolder, filename); + string pluginHashFilePath; + switch (GlobalSettings.LocalTempStorageLocation) + { + case LocalTempStorage.AspNetTemp: + pluginHashFilePath = Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco-plugins.hash"); + break; + case LocalTempStorage.EnvironmentTemp: + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + pluginHashFilePath = Path.Combine(cachePath, "umbraco-plugins.hash"); + break; + case LocalTempStorage.Default: + default: + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); + pluginHashFilePath = Path.Combine(tempFolder, "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash"); + break; + } + + //ensure the folder exists + var folder = Path.GetDirectoryName(pluginHashFilePath); + if (folder == null) + throw new InvalidOperationException("The folder could not be determined for the file " + pluginHashFilePath); + if (Directory.Exists(folder) == false) + Directory.CreateDirectory(folder); + + return pluginHashFilePath; } internal void WriteCache() { - // be absolutely sure - if (Directory.Exists(_tempFolder) == false) - Directory.CreateDirectory(_tempFolder); - - var filePath = GetPluginListFilePath(); - - using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) + using (var stream = GetFileStream(_pluginListFilePath.Value, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) using (var writer = new StreamWriter(stream)) { foreach (var typeList in _types.Values) @@ -461,8 +503,7 @@ namespace Umbraco.Core internal void UpdateCache() { - // note - // at the moment we write the cache to disk every time we update it. ideally we defer the writing + // TODO: at the moment we write the cache to disk every time we update it. ideally we defer the writing // since all the updates are going to happen in a row when Umbraco starts. that being said, the // file is small enough, so it is not a priority. WriteCache(); @@ -475,13 +516,13 @@ namespace Umbraco.Core while (true) { try - { + { return new FileStream(path, fileMode, fileAccess, fileShare); } - catch + catch (Exception ex) { if (--attempts == 0) - throw; + throw; LogHelper.Debug(string.Format("Attempted to get filestream for file {0} failed, {1} attempts left, pausing for {2} milliseconds", path, attempts, pauseMilliseconds)); Thread.Sleep(pauseMilliseconds); @@ -691,7 +732,7 @@ namespace Umbraco.Core // else proceed, typeList = new TypeList(baseType, attributeType); - var scan = RequiresRescanning || File.Exists(GetPluginListFilePath()) == false; + var scan = RequiresRescanning || File.Exists(_pluginListFilePath.Value) == false; if (scan) { diff --git a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs index d773eed2e9..0c8bec8f2e 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs @@ -178,29 +178,25 @@ namespace Umbraco.Core.PropertyEditors return result; } - private void ConvertItemsToJsonIfDetected(IDictionary result) + protected void ConvertItemsToJsonIfDetected(IDictionary result) { - //now we're going to try to see if any of the values are JSON, if they are we'll convert them to real JSON objects - // so they can be consumed as real json in angular! + // convert values that are Json to true Json objects that can be consumed by Angular var keys = result.Keys.ToArray(); for (var i = 0; i < keys.Length; i++) { - if (result[keys[i]] is string) + if ((result[keys[i]] is string) == false) continue; + + var asString = result[keys[i]].ToString(); + if (asString.DetectIsJson() == false) continue; + + try { - var asString = result[keys[i]].ToString(); - if (asString.DetectIsJson()) - { - try - { - var json = JsonConvert.DeserializeObject(asString); - result[keys[i]] = json; - } - catch - { - //swallow this exception, we thought it was json but it really isn't so continue returning a string - } - } + result[keys[i]] = JsonConvert.DeserializeObject(asString); + } + catch + { + // swallow this exception, we thought it was Json but it really isn't so continue returning a string } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs index 987640716b..b0d2e0809d 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ColorPickerValueConverter.cs @@ -1,13 +1,14 @@ using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { [DefaultPropertyValueConverter] - [PropertyValueType(typeof(string))] - [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] - public class ColorPickerValueConverter : PropertyValueConverterBase + public class ColorPickerValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta { public override bool IsConverter(PublishedPropertyType propertyType) { @@ -18,11 +19,60 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters return false; } + public Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return UseLabel(propertyType) ? typeof(PickedColor) : typeof(string); + } + + public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + return PropertyCacheLevel.Content; + } + + private bool UseLabel(PublishedPropertyType propertyType) + { + var preValues = ApplicationContext.Current.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(propertyType.DataTypeId); + PreValue preValue; + return preValues.PreValuesAsDictionary.TryGetValue("useLabel", out preValue) && preValue.Value == "1"; + } + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) { - // make sure it's a string - return source == null ? string.Empty : source.ToString(); + var useLabel = UseLabel(propertyType); + + if (source == null) return useLabel ? null : string.Empty; + + var ssource = source.ToString(); + if (ssource.DetectIsJson()) + { + try + { + var jo = JsonConvert.DeserializeObject(ssource); + if (useLabel) return new PickedColor(jo["value"].ToString(), jo["label"].ToString()); + return jo["value"].ToString(); + } + catch { /* not json finally */ } + } + + if (useLabel) return new PickedColor(ssource, ssource); + return ssource; + } + + public class PickedColor + { + public PickedColor(string color, string label) + { + Color = color; + Label = label; + } + + public string Color { get; private set; } + public string Label { get; private set; } + + public override string ToString() + { + return Color; + } } - } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 029d1c3546..85599767d6 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -43,8 +43,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters var gridConfig = UmbracoConfig.For.GridConfig( ApplicationContext.Current.ProfilingLogger.Logger, ApplicationContext.Current.ApplicationCache.RuntimeCache, - new DirectoryInfo(HttpContext.Current.Server.MapPath(SystemDirectories.AppPlugins)), - new DirectoryInfo(HttpContext.Current.Server.MapPath(SystemDirectories.Config)), + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)), + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)), HttpContext.Current.IsDebuggingEnabled); var sections = GetArray(obj, "sections"); diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 12cb465b50..6f1fc03281 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using umbraco.interfaces; +using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Sync @@ -39,6 +40,7 @@ namespace Umbraco.Core.Sync private bool _syncing; private bool _released; private readonly ProfilingLogger _profilingLogger; + private readonly Lazy _distCacheFilePath = new Lazy(GetDistCacheFilePath); protected DatabaseServerMessengerOptions Options { get; private set; } protected ApplicationContext ApplicationContext { get { return _appContext; } } @@ -460,10 +462,9 @@ namespace Umbraco.Core.Sync /// private void ReadLastSynced() { - var path = SyncFilePath; - if (File.Exists(path) == false) return; + if (File.Exists(_distCacheFilePath.Value) == false) return; - var content = File.ReadAllText(path); + var content = File.ReadAllText(_distCacheFilePath.Value); int last; if (int.TryParse(content, out last)) _lastId = last; @@ -478,7 +479,7 @@ namespace Umbraco.Core.Sync /// private void SaveLastSynced(int id) { - File.WriteAllText(SyncFilePath, id.ToString(CultureInfo.InvariantCulture)); + File.WriteAllText(_distCacheFilePath.Value, id.ToString(CultureInfo.InvariantCulture)); _lastId = id; } @@ -498,20 +499,40 @@ namespace Umbraco.Core.Sync + "/D" + AppDomain.CurrentDomain.Id // eg 22 + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique - /// - /// Gets the sync file path for the local server. - /// - /// The sync file path for the local server. - private static string SyncFilePath + private static string GetDistCacheFilePath() { - get - { - var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache/" + NetworkHelper.FileSafeMachineName); - if (Directory.Exists(tempFolder) == false) - Directory.CreateDirectory(tempFolder); + var fileName = HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"; - return Path.Combine(tempFolder, HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"); + string distCacheFilePath; + switch (GlobalSettings.LocalTempStorageLocation) + { + case LocalTempStorage.AspNetTemp: + distCacheFilePath = Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData", fileName); + break; + case LocalTempStorage.EnvironmentTemp: + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + distCacheFilePath = Path.Combine(cachePath, fileName); + break; + case LocalTempStorage.Default: + default: + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache"); + distCacheFilePath = Path.Combine(tempFolder, fileName); + break; } + + //ensure the folder exists + var folder = Path.GetDirectoryName(distCacheFilePath); + if (folder == null) + throw new InvalidOperationException("The folder could not be determined for the file " + distCacheFilePath); + if (Directory.Exists(folder) == false) + Directory.CreateDirectory(folder); + + return distCacheFilePath; } #endregion diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index ab2b24392e..16dbd49e73 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.IO; using System.Linq; using System.Reflection; using System.Security; using System.Text; -using System.Web; using System.Web.Compilation; -using System.Web.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -21,6 +20,38 @@ namespace Umbraco.Core { private static volatile HashSet _localFilteredAssemblyCache; private static readonly object LocalFilteredAssemblyCacheLocker = new object(); + private static readonly List NotifiedLoadExceptionAssemblies = new List(); + private static string[] _assembliesAcceptingLoadExceptions; + + private static string[] AssembliesAcceptingLoadExceptions + { + get + { + if (_assembliesAcceptingLoadExceptions != null) + return _assembliesAcceptingLoadExceptions; + + var s = ConfigurationManager.AppSettings["Umbraco.AssembliesAcceptingLoadExceptions"]; + return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s) + ? new string[0] + : s.Split(',').Select(x => x.Trim()).ToArray(); + } + } + + private static bool AcceptsLoadExceptions(Assembly a) + { + if (AssembliesAcceptingLoadExceptions.Length == 0) + return false; + if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*") + return true; + var name = a.GetName().Name; // simple name of the assembly + return AssembliesAcceptingLoadExceptions.Any(pattern => + { + if (pattern.Length > name.Length) return false; // pattern longer than name + if (pattern.Length == name.Length) return pattern.InvariantEquals(name); // same length, must be identical + if (pattern[pattern.Length] != '.') return false; // pattern is shorter than name, must end with dot + return name.StartsWith(pattern); // and name must start with pattern + }); + } /// /// lazily load a reference to all assemblies and only local assemblies. @@ -45,7 +76,7 @@ namespace Umbraco.Core HashSet assemblies = null; try { - var isHosted = HttpContext.Current != null || HostingEnvironment.IsHosted; + var isHosted = IOHelper.IsHosted; try { @@ -528,9 +559,22 @@ namespace Umbraco.Core AppendCouldNotLoad(sb, a, getAll); foreach (var loaderException in rex.LoaderExceptions.WhereNotNull()) AppendLoaderException(sb, loaderException); + + var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); - // rethrow with new message - throw new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString()); + // rethrow with new message, unless accepted + if (AcceptsLoadExceptions(a) == false) throw ex; + + // log a warning, and return what we can + lock (NotifiedLoadExceptionAssemblies) + { + if (NotifiedLoadExceptionAssemblies.Contains(a.FullName) == false) + { + NotifiedLoadExceptionAssemblies.Add(a.FullName); + LogHelper.WarnWithException(typeof(TypeFinder), "Could not load all types from " + a.GetName().Name + ".", ex); + } + } + return rex.Types.WhereNotNull().ToArray(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c9f3259181..5b49e40c47 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -198,7 +198,7 @@ - + diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 4244188697..aa9fc35524 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -20,7 +20,7 @@ using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Models { [TestFixture] - public class ContentTests : BaseUmbracoConfigurationTest + public class ContentTests : BaseUmbracoApplicationTest { [SetUp] public void Init() diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 6db3ee793b..6bc45914a0 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -388,13 +388,12 @@ namespace Umbraco.Tests.TestHelpers return ctx; } - protected FakeHttpContextFactory GetHttpContextFactory(string url, RouteData routeData = null) + protected virtual FakeHttpContextFactory GetHttpContextFactory(string url, RouteData routeData = null) { var factory = routeData != null ? new FakeHttpContextFactory(url, routeData) : new FakeHttpContextFactory(url); - //set the state helper StateHelper.HttpContext = factory.HttpContext; diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 49895ed453..6444638928 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -118,25 +118,12 @@ namespace Umbraco.Tests.TestHelpers }); } - /// - /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan - /// all of the assemblies. Inheritors can override this if plugin manager resetting is required, generally needs - /// to be set to true if the SetupPluginManager has been overridden. - /// - protected virtual bool PluginManagerResetRequired - { - get { return false; } - } - /// /// Inheritors can resset the plugin manager if they choose to on teardown /// protected virtual void ResetPluginManager() { - if (PluginManagerResetRequired) - { - PluginManager.Current = null; - } + PluginManager.Current = null; } protected virtual CacheHelper CreateCacheHelper() @@ -185,26 +172,23 @@ namespace Umbraco.Tests.TestHelpers /// protected virtual void SetupPluginManager() { - if (PluginManager.Current == null || PluginManagerResetRequired) + PluginManager.Current = new PluginManager( + new ActivatorServiceProvider(), + CacheHelper.RuntimeCache, ProfilingLogger, false) { - PluginManager.Current = new PluginManager( - new ActivatorServiceProvider(), - CacheHelper.RuntimeCache, ProfilingLogger, false) + AssembliesToScan = new[] { - AssembliesToScan = new[] - { - Assembly.Load("Umbraco.Core"), - Assembly.Load("umbraco"), - Assembly.Load("Umbraco.Tests"), - Assembly.Load("businesslogic"), - Assembly.Load("cms"), - Assembly.Load("controls"), - Assembly.Load("umbraco.editorControls"), - Assembly.Load("umbraco.MacroEngines"), - Assembly.Load("umbraco.providers"), - } - }; - } + Assembly.Load("Umbraco.Core"), + Assembly.Load("umbraco"), + Assembly.Load("Umbraco.Tests"), + Assembly.Load("businesslogic"), + Assembly.Load("cms"), + Assembly.Load("controls"), + Assembly.Load("umbraco.editorControls"), + Assembly.Load("umbraco.MacroEngines"), + Assembly.Load("umbraco.providers"), + } + }; } /// diff --git a/src/Umbraco.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs index 75f4536fd4..82608a5513 100644 --- a/src/Umbraco.Tests/UdiTests.cs +++ b/src/Umbraco.Tests/UdiTests.cs @@ -7,11 +7,12 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Deploy; using Umbraco.Core.Serialization; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests { [TestFixture] - public class UdiTests + public class UdiTests : BaseUmbracoApplicationTest { [Test] public void StringUdiCtorTest() diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs index c84d578d14..cdca8645f3 100644 --- a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs @@ -22,5 +22,40 @@ namespace Umbraco.Tests.Web.Mvc var expected = "

hello world

hello world
hello world
hello world
hello world

"; Assert.AreEqual(expected, output); } + + [Test] + public void TruncateWithElipsis() + { + var output = _htmlStringUtilities.Truncate("hello world", 5, true, false).ToString(); + var expected = "hello…"; + Assert.AreEqual(expected, output); + } + + [Test] + public void TruncateWithoutElipsis() + { + var output = _htmlStringUtilities.Truncate("hello world", 5, false, false).ToString(); + var expected = "hello"; + Assert.AreEqual(expected, output); + } + + [Test] + public void TruncateShorterWordThanHellip() + { + //http://issues.umbraco.org/issue/U4-10478 + var output = _htmlStringUtilities.Truncate("hi", 5, true, false).ToString(); + var expected = "hi"; + Assert.AreEqual(expected, output); + } + + [Test] + public void TruncateAndRemoveSpaceBetweenHellipAndWord() + { + var output = _htmlStringUtilities.Truncate("hello world", 6 /* hello plus space */, true, false).ToString(); + var expected = "hello…"; + Assert.AreEqual(expected, output); + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js index d23fdcbf84..cc45df2ce2 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js @@ -7,12 +7,22 @@ var app = angular.module("Umbraco.canvasdesigner", ['colorpicker', 'ui.slider', .controller("Umbraco.canvasdesignerController", function ($scope, $http, $window, $timeout, $location, dialogService) { + var isInit = $location.search().init; + if (isInit === "true") { + //do not continue, this is the first load of this new window, if this is passed in it means it's been + //initialized by the content editor and then the content editor will actually re-load this window without + //this flag. This is a required trick to get around chrome popup mgr. We don't want to double load preview.aspx + //since that will double prepare the preview documents + return; + } + $scope.isOpen = false; $scope.frameLoaded = false; $scope.enableCanvasdesigner = 0; $scope.googleFontFamilies = {}; - $scope.pageId = $location.search().id; - $scope.pageUrl = "../dialogs/Preview.aspx?id=" + $location.search().id; + var pageId = $location.search().id; + $scope.pageId = pageId; + $scope.pageUrl = "../dialogs/Preview.aspx?id=" + pageId; $scope.valueAreLoaded = false; $scope.devices = [ { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index a0218c266a..402885a1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -230,9 +230,9 @@ if (!$scope.busy) { // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. + // without the initial scoped request. This trick will fix that. // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + var previewWindow = $window.open('preview/?init=true&id=' + content.id, 'umbpreview'); // Build the correct path so both /#/ and #/ work. var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + content.id; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js index 7fad3e8a74..7dd2f0d7a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js @@ -66,9 +66,13 @@ Use this directive to render an avatar. function getNameInitials(name) { if(name) { - var initials = name.match(/\b\w/g) || []; - initials = ((initials.shift() || '') + (initials.pop() || '')).toUpperCase(); - return initials; + var names = name.split(' '), + initials = names[0].substring(0, 1); + + if (names.length > 1) { + initials += names[names.length - 1].substring(0, 1); + } + return initials.toUpperCase(); } return null; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js new file mode 100644 index 0000000000..d75b9e2de0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js @@ -0,0 +1,26 @@ +(function () { + 'use strict'; + + // comes from https://codepen.io/jakob-e/pen/eNBQaP + // works fine with Angular 1.6.5 - alas not with 1.1.5 - binding issue + + function PasswordToggleDirective($compile) { + + var directive = { + restrict: 'A', + scope: {}, + link: function(scope, elem, attrs) { + scope.tgl = function () { elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); } + var lnk = angular.element("Toggle"); + $compile(lnk)(scope); + elem.wrap("
").after(lnk); + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js index a84be208ba..16330f5493 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js @@ -2,12 +2,12 @@ * @ngdoc service * @name umbraco.services.assetsService * - * @requires $q + * @requires $q * @requires angularHelper - * + * * @description * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers. - * + * * ##usage * To use, simply inject the assetsService into any controller that needs it, and make * sure the umbraco.services module is accesible - which it should be by default. @@ -18,7 +18,7 @@ * //this code executes when the dependencies are done loading * }); * }); - * + * * * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout * @@ -38,13 +38,14 @@ * //loadcss cannot determine when the css is done loading, so this will trigger instantly * }); * }); - * + * */ angular.module('umbraco.services') .factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) { var initAssetsLoaded = false; - var appendRnd = function (url) { + + function appendRnd (url) { //if we don't have a global umbraco obj yet, the app is bootstrapping if (!Umbraco.Sys.ServerVariables.application) { return url; @@ -77,7 +78,8 @@ angular.module('umbraco.services') return this.loadedAssets[path]; } }, - /** + + /** Internal method. This is called when the application is loading and the user is already authenticated, or once the user is authenticated. There's a few assets the need to be loaded for the application to function but these assets require authentication to load. */ @@ -108,10 +110,10 @@ angular.module('umbraco.services') * * @description * Injects a file as a stylesheet into the document head - * + * * @param {String} path path to the css file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the stylesheet element + * @param {Object} keyvalue collection of attributes to pass to the stylesheet element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ @@ -149,10 +151,10 @@ angular.module('umbraco.services') * * @description * Injects a file as a javascript into the document - * + * * @param {String} path path to the js file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the script element + * @param {Object} keyvalue collection of attributes to pass to the script element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ @@ -192,8 +194,8 @@ angular.module('umbraco.services') * @methodOf umbraco.services.assetsService * * @description - * Injects a collection of files, this can be ONLY js files - * + * Injects a collection of css and js files + * * * @param {Array} pathArray string array of paths to the files to load * @param {Scope} scope optional scope to pass into the loader @@ -206,61 +208,72 @@ angular.module('umbraco.services') throw "pathArray must be an array"; } + // Check to see if there's anything to load, resolve promise if not var nonEmpty = _.reject(pathArray, function (item) { return item === undefined || item === ""; - }); + }); - - //don't load anything if there's nothing to load - if (nonEmpty.length > 0) { - var promises = []; - var assets = []; - - //compile a list of promises - //blocking - _.each(nonEmpty, function (path) { - - path = convertVirtualPath(path); - - var asset = service._getAssetPromise(path); - //if not previously loaded, add to list of promises - if (asset.state !== "loaded") { - if (asset.state === "new") { - asset.state = "loading"; - assets.push(asset); - } - - //we need to always push to the promises collection to monitor correct - //execution - promises.push(asset.deferred.promise); - } - }); - - - //gives a central monitoring of all assets to load - promise = $q.all(promises); - - _.each(assets, function (asset) { - LazyLoad.js(appendRnd(asset.path), function () { - asset.state = "loaded"; - if (!scope) { - asset.deferred.resolve(true); - } - else { - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - }); - } - else { - //return and resolve + if (nonEmpty.length === 0) { var deferred = $q.defer(); promise = deferred.promise; deferred.resolve(true); + return promise; } + //compile a list of promises + //blocking + var promises = []; + var assets = []; + _.each(nonEmpty, function (path) { + path = convertVirtualPath(path); + var asset = service._getAssetPromise(path); + //if not previously loaded, add to list of promises + if (asset.state !== "loaded") { + if (asset.state === "new") { + asset.state = "loading"; + assets.push(asset); + } + + //we need to always push to the promises collection to monitor correct + //execution + promises.push(asset.deferred.promise); + } + }); + + //gives a central monitoring of all assets to load + promise = $q.all(promises); + + // Split into css and js asset arrays, and use LazyLoad on each array + var cssAssets = _.filter(assets, + function (asset) { + return asset.path.match(/(\.css$|\.css\?)/ig); + }); + var jsAssets = _.filter(assets, + function (asset) { + return asset.path.match(/(\.js$|\.js\?)/ig); + }); + + function assetLoaded(asset) { + asset.state = "loaded"; + if (!scope) { + asset.deferred.resolve(true); + return; + } + angularHelper.safeApply(scope, + function () { + asset.deferred.resolve(true); + }); + } + + if (cssAssets.length > 0) { + var cssPaths = _.map(cssAssets, function (asset) { return appendRnd(asset.path) }); + LazyLoad.css(cssPaths, function() { _.each(cssAssets, assetLoaded); }); + } + + if (jsAssets.length > 0) { + var jsPaths = _.map(jsAssets, function (asset) { return appendRnd(asset.path) }); + LazyLoad.js(jsPaths, function () { _.each(jsAssets, assetLoaded); }); + } return promise; } diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index e76ffc682c..1eaf285119 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -261,6 +261,9 @@ input[type="submit"].btn { *padding-top: 1px; *padding-bottom: 1px; } + + // Safari defaults to 1px for input. Ref U4-7721. + margin: 0px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index d25744a108..47ed97fde2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -35,3 +35,10 @@ margin-right: 5px; color: @gray-7; } + +input.umb-breadcrumbs__add-ancestor { + height: 25px; + margin-top: -2px; + margin-left: 3px; + width: 100px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 3551d9d31a..2e5d1ef77d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -636,9 +636,19 @@ clear: both; } -.umb-grid .mce-btn button { - padding: 8px 6px; - line-height: inherit; +.umb-grid .mce-btn { + button { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 6px; + line-height: inherit; + } + + &:not(.mce-menubtn) { + button { + padding-right: 6px; + } + } } .umb-grid .mce-toolbar { diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 924f9b20bb..2fe214bf07 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -66,7 +66,7 @@ margin-bottom: auto; } -.login-overlay .form input[type="text"], +.login-overlay .form input[type="text"], .login-overlay .form input[type="password"], .login-overlay .form input[type="email"] { height: 36px; @@ -114,8 +114,44 @@ line-height: 36px; } -.login-overlay .text-error, -.login-overlay .text-info +.login-overlay .text-error, +.login-overlay .text-info { font-weight:bold; -} +} + +.password-toggle { + position: relative; + display: block; + user-select: none; + + input::-ms-clear, input::-ms-reveal { + display: none; + } + + a { + opacity: .5; + cursor: pointer; + display: inline-block; + position: absolute; + height: 1px; + width: 45px; + height: 75%; + font-size: 0; + background-repeat: no-repeat; + background-size: 50%; + background-position: center; + top: 0; + margin-left: -45px; + z-index: 1; + -webkit-tap-highlight-color: transparent; + } + + [type="text"] + a { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M29.6.4C29 0 28 0 27.4.4L21 6.8c-1.4-.5-3-.8-5-.8C9 6 3 10 0 16c1.3 2.6 3 4.8 5.4 6.5l-5 5c-.5.5-.5 1.5 0 2 .3.4.7.5 1 .5s1 0 1.2-.4l27-27C30 2 30 1 29.6.4zM13 10c1.3 0 2.4 1 2.8 2L12 15.8c-1-.4-2-1.5-2-2.8 0-1.7 1.3-3 3-3zm-9.6 6c1.2-2 2.8-3.5 4.7-4.7l.7-.2c-.4 1-.6 2-.6 3 0 1.8.6 3.4 1.6 4.7l-2 2c-1.6-1.2-3-2.7-4-4.4zM24 13.8c0-.8 0-1.7-.4-2.4l-10 10c.7.3 1.6.4 2.4.4 4.4 0 8-3.6 8-8z'/%3E%3Cpath fill='%23444' d='M26 9l-2.2 2.2c2 1.3 3.6 3 4.8 4.8-1.2 2-2.8 3.5-4.7 4.7-2.7 1.5-5.4 2.3-8 2.3-1.4 0-2.6 0-3.8-.4L10 25c2 .6 4 1 6 1 7 0 13-4 16-10-1.4-2.8-3.5-5.2-6-7z'/%3E%3C/svg%3E"); + } + + [type="password"] + a { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 8b10b2f91b..ffaa1a6a92 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -109,16 +109,48 @@ ul.color-picker li a { } /* pre-value editor */ - +/*.control-group.color-picker-preval:before { + content: ""; + display: inline-block; + vertical-align: middle; + height: 100%; +}*/ + +/*.control-group.color-picker-preval div.thumbnail { + display: inline-block; + vertical-align: middle; +}*/ +.control-group.color-picker-preval div.color-picker-prediv { + display: inline-block; + width: 60%; +} + .control-group.color-picker-preval pre { display: inline; margin-right: 20px; margin-left: 10px; + width: 50%; + white-space: nowrap; + overflow: hidden; + margin-bottom: 0; + vertical-align: middle; } +.control-group.color-picker-preval btn { + //vertical-align: middle; +} + +.control-group.color-picker-preval input[type="text"] { + min-width: 40%; + width: 40%; + display: inline-block; + margin-right: 20px; + margin-top: 1px; +} + .control-group.color-picker-preval label { - border:solid @white 1px; - padding:6px; + border: solid @white 1px; + padding: 6px; } @@ -126,21 +158,21 @@ ul.color-picker li a { // Media picker // -------------------------------------------------- .umb-mediapicker .add-link { - display: inline-block; - height: 120px; - width: 120px; - text-align: center; - color: @gray-8; - border: 2px @gray-8 dashed; - line-height: 120px; - text-decoration: none; + display: flex; + justify-content:center; + align-items:center; + width: 120px; + text-align: center; + color: @gray-8; + border: 2px @gray-8 dashed; + text-decoration: none; - transition: all 150ms ease-in-out; + transition: all 150ms ease-in-out; - &:hover { - color: @turquoise-d1; - border-color: @turquoise; - } + &:hover { + color: @turquoise-d1; + border-color: @turquoise; + } } .umb-mediapicker .picked-image { @@ -165,6 +197,10 @@ ul.color-picker li a { text-decoration: none; } +.umb-mediapicker .add-link-square { + height: 120px; +} + .umb-thumbnails{ @@ -207,11 +243,10 @@ ul.color-picker li a { .umb-mediapicker .umb-sortable-thumbnails li { flex-direction: column; - margin: 0; + margin: 0 5px 0 0; padding: 5px; } - .umb-sortable-thumbnails li:hover a { display: flex; justify-content: center; @@ -219,16 +254,20 @@ ul.color-picker li a { } .umb-sortable-thumbnails li img { - max-width:100%; - max-height:100%; - margin:auto; - display:block; - background-image: url(../img/checkered-background.png); + max-width:100%; + max-height:100%; + margin:auto; + display:block; + background-image: url(../img/checkered-background.png); } -.umb-sortable-thumbnails li img.noScale{ - max-width: none !important; - max-height: none !important; +.umb-sortable-thumbnails li img.trashed { + opacity:0.3; +} + +.umb-sortable-thumbnails li img.noScale { + max-width: none !important; + max-height: none !important; } .umb-sortable-thumbnails .umb-icon-holder { @@ -254,8 +293,8 @@ ul.color-picker li a { } .umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action { @@ -285,27 +324,27 @@ ul.color-picker li a { // ------------------------------------------------- .umb-cropper{ - position: relative; + position: relative; } .umb-cropper img, .umb-cropper-gravity img{ - position: relative; - max-width: 100%; - height: auto; - top: 0; - left: 0; + position: relative; + max-width: 100%; + height: auto; + top: 0; + left: 0; } .umb-cropper img { - max-width: none; + max-width: none; } .umb-cropper .overlay, .umb-cropper-gravity .overlay { - top: 0; - left: 0; - cursor: move; - z-index: @zindexCropperOverlay; - position: absolute; + top: 0; + left: 0; + cursor: move; + z-index: @zindexCropperOverlay; + position: absolute; } .umb-cropper .viewport{ @@ -317,43 +356,43 @@ ul.color-picker li a { } .umb-cropper-gravity .viewport{ - overflow: hidden; - position: relative; - width: 100%; - height: 100%; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; } .umb-cropper .viewport:after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: @zindexCropperOverlay - 1; - -moz-opacity: .75; - opacity: .75; - filter: alpha(opacity=7); - -webkit-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); - -moz-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); - box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: @zindexCropperOverlay - 1; + -moz-opacity: .75; + opacity: .75; + filter: alpha(opacity=7); + -webkit-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); } .umb-cropper-gravity .overlay{ - width: 14px; - height: 14px; - text-align: center; - border-radius: 20px; - background: @turquoise; - border: 3px solid @white; - opacity: 0.8; + width: 14px; + height: 14px; + text-align: center; + border-radius: 20px; + background: @turquoise; + border: 3px solid @white; + opacity: 0.8; } .umb-cropper-gravity .overlay i { - font-size: 26px; - line-height: 26px; - opacity: 0.8 !important; + font-size: 26px; + line-height: 26px; + opacity: 0.8 !important; } .umb-cropper .crop-container { @@ -361,16 +400,16 @@ ul.color-picker li a { } .umb-cropper .crop-slider { - padding: 10px; - border-top: 1px solid @gray-10; - margin-top: 10px; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - @media (min-width: 769px) { + padding: 10px; + border-top: 1px solid @gray-10; + margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + @media (min-width: 769px) { padding: 10px 50px 10px 50px; - } + } } .umb-cropper .crop-slider i { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 18158a5ff2..34dbf90de6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -16,7 +16,11 @@ maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), uploaded: false - } + } + $scope.togglePassword = function () { + var elem = $("form[name='loginForm'] input[name='password']"); + elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); + } function init() { // Check if it is a new user @@ -48,7 +52,7 @@ ]).then(function () { $scope.inviteStep = Number(inviteVal); - + }); } } @@ -82,7 +86,7 @@ var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); // set percentage property on file - $scope.avatarFile.uploadProgress = progressPercentage; + $scope.avatarFile.uploadProgress = progressPercentage; } }).success(function (data, status, headers, config) { @@ -149,11 +153,11 @@ //error formHelper.handleError(err); - + $scope.invitedUserPasswordModel.buttonState = "error"; }); - } + } }; var setFieldFocus = function (form, field) { @@ -180,7 +184,7 @@ } function resetInputValidation() { - $scope.confirmPassword = ""; + $scope.confirmPassword = ""; $scope.password = ""; $scope.login = ""; if ($scope.loginForm) { @@ -255,7 +259,7 @@ //TODO: Do validation properly like in the invite password update - //if the login and password are not empty we need to automatically + //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, // so if they're not empty, we'll just make sure to set them to valid. @@ -289,7 +293,7 @@ }); //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can + // while the form is invalid, then revalidate them so that the form can // be submitted again. $scope.loginForm.username.$viewChangeListeners.push(function () { if ($scope.loginForm.username.$invalid) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 61d04d30fb..6505af4de9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -24,8 +24,8 @@ Your new password cannot be blank! - Minimum {{invitedUserPasswordModel.passwordPolicies.minPasswordLength}} characters - + Minimum {{invitedUserPasswordModel.passwordPolicies.minPasswordLength}} characters +
@@ -49,15 +49,15 @@
- + - + - +
{{ avatarFile.serverErrorMessage }}
@@ -69,7 +69,7 @@ ngf-multiple="false" ngf-pattern="{{avatarFile.acceptedFileTypes}}" ngf-max-size="{{ avatarFile.maxFileSize }}"> - + - +
@@ -149,8 +149,9 @@
- -
+
+ Toggle +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index a6a2ddcbab..ccb033a57c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -56,6 +56,46 @@ angular.module("umbraco") $scope.target = dialogOptions.currentTarget; } + function onInit() { + if ($scope.startNodeId !== -1) { + entityResource.getById($scope.startNodeId, "media") + .then(function (ent) { + $scope.startNodeId = ent.id; + run(); + }); + } else { + run(); + } + } + + function run() { + //default root item + if (!$scope.target) { + if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { + entityResource.getById($scope.lastOpenedNode, "media") + .then(ensureWithinStartNode, gotoStartNode); + } else { + gotoStartNode(); + } + } else { + //if a target is specified, go look it up - generally this target will just contain ids not the actual full + //media object so we need to look it up + var id = $scope.target.udi ? $scope.target.udi : $scope.target.id + var altText = $scope.target.altText; + mediaResource.getById(id) + .then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectImage(node); + $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.altText = altText; + $scope.openDetailsDialog(); + } + }, + gotoStartNode); + } + } + $scope.upload = function(v) { angular.element(".umb-file-dropzone-directive .file-select").click(); }; @@ -107,7 +147,7 @@ angular.module("umbraco") if (folder.id > 0) { entityResource.getAncestors(folder.id, "media") - .then(function(anc) { + .then(function(anc) { $scope.path = _.filter(anc, function(f) { return f.path.indexOf($scope.startNodeId) !== -1; @@ -218,32 +258,6 @@ angular.module("umbraco") $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } - //default root item - if (!$scope.target) { - if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { - entityResource.getById($scope.lastOpenedNode, "media") - .then(ensureWithinStartNode, gotoStartNode); - } else { - gotoStartNode(); - } - } else { - //if a target is specified, go look it up - generally this target will just contain ids not the actual full - //media object so we need to look it up - var id = $scope.target.udi ? $scope.target.udi : $scope.target.id - var altText = $scope.target.altText; - mediaResource.getById(id) - .then(function(node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); - $scope.target.altText = altText; - $scope.openDetailsDialog(); - } - }, - gotoStartNode); - } - $scope.openDetailsDialog = function() { $scope.mediaPickerDetailsOverlay = {}; @@ -368,4 +382,7 @@ angular.module("umbraco") } } } + + onInit(); + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index 8c726f4a17..73b0915161 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -48,14 +48,14 @@ / -
  • +
  • 0; + + if ($scope.isConfigured) { + + for (var key in $scope.model.config.items) { + if (!$scope.model.config.items[key].hasOwnProperty("value")) + $scope.model.config.items[key] = { value: $scope.model.config.items[key], label: $scope.model.config.items[key] }; + } + + $scope.model.useLabel = isTrue($scope.model.config.useLabel); + initActiveColor(); + } + + $scope.toggleItem = function (color) { + + var currentColor = $scope.model.value.hasOwnProperty("value") + ? $scope.model.value.value + : $scope.model.value; + + var newColor; + if (currentColor === color.value) { + // deselect + $scope.model.value = $scope.model.useLabel ? { value: "", label: "" } : ""; + newColor = ""; + } + else { + // select + $scope.model.value = $scope.model.useLabel ? { value: color.value, label: color.label } : color.value; + newColor = color.value; + } + + // this is required to re-validate + $scope.propertyForm.modelValue.$setViewValue(newColor); + }; + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) - $scope.validateMandatory = function () { + $scope.validateMandatory = function () { + var isValid = !$scope.model.validation.mandatory || ( + $scope.model.value != null + && $scope.model.value != "" + && (!$scope.model.value.hasOwnProperty("value") || $scope.model.value.value !== "") + ); return { - isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value != ""), + isValid: isValid, errorMsg: "Value cannot be empty", errorKey: "required" }; } - $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; + $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; + + // A color is active if it matches the value and label of the model. + // If the model doesn't store the label, ignore the label during the comparison. + $scope.isActiveColor = function (color) { + + // no value + if (!$scope.model.value) + return false; + + // Complex color (value and label)? + if (!$scope.model.value.hasOwnProperty("value")) + return $scope.model.value === color.value; + + return $scope.model.value.value === color.value && $scope.model.value.label === color.label; + }; + + // Finds the color best matching the model's color, + // and sets the model color to that one. This is useful when + // either the value or label was changed on the data type. + function initActiveColor() { + + // no value + if (!$scope.model.value) + return; + + // Complex color (value and label)? + if (!$scope.model.value.hasOwnProperty("value")) + return; + + var modelColor = $scope.model.value.value; + var modelLabel = $scope.model.value.label; + + // Check for a full match or partial match. + var foundItem = null; + + // Look for a fully matching color. + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.value == modelColor && item.label == modelLabel) { + foundItem = item; + break; + } + } + + // Look for a color with a matching value. + if (!foundItem) { + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.value == modelColor) { + foundItem = item; + break; + } + } + } + + // Look for a color with a matching label. + if (!foundItem) { + for (var key in $scope.model.config.items) { + var item = $scope.model.config.items[key]; + if (item.label == modelLabel) { + foundItem = item; + break; + } + } + } + + // If a match was found, set it as the active color. + if (foundItem) { + $scope.model.value.value = foundItem.value; + $scope.model.value.label = foundItem.label; + } + } + + // figures out if a value is trueish enough + function isTrue(bool) { + return !!bool && bool !== "0" && angular.lowercase(bool) !== "false"; + } } angular.module("umbraco").controller("Umbraco.PropertyEditors.ColorPickerController", ColorPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index a493fffdd8..46b624adcc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -5,10 +5,10 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html index a772f830c5..2b917e69f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html @@ -1,12 +1,13 @@
    + + -
    -
    {{item.value}}
    +
    #{{item.value}} - {{item.label}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js index d31ac911a9..dd25741aeb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js @@ -2,15 +2,17 @@ function ($scope, $timeout, assetsService, angularHelper, $element) { //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. var defaultColor = "000000"; - + var defaultLabel = null; + $scope.newColor = defaultColor; + $scope.newLavel = defaultLabel; $scope.hasError = false; assetsService.load([ //"lib/spectrum/tinycolor.js", - "lib/spectrum/spectrum.js" + "lib/spectrum/spectrum.js" ], $scope).then(function () { - var elem = $element.find("input"); + var elem = $element.find("input[name='newColor']"); elem.spectrum({ color: null, showInitial: false, @@ -21,7 +23,7 @@ clickoutFiresChange: true, hide: function (color) { //show the add butotn - $element.find(".btn.add").show(); + $element.find(".btn.add").show(); }, change: function (color) { angularHelper.safeApply($scope, function () { @@ -39,21 +41,41 @@ //make an array from the dictionary var items = []; for (var i in $scope.model.value) { - items.push({ - value: $scope.model.value[i], - id: i - }); + var oldValue = $scope.model.value[i]; + if (oldValue.hasOwnProperty("value")) { + items.push({ + value: oldValue.value, + label: oldValue.label, + id: i + }); + } else { + items.push({ + value: oldValue, + label: oldValue, + id: i + }); + } } //now make the editor model the array $scope.model.value = items; } + // ensure labels + for (var i = 0; i < $scope.model.value.length; i++) { + var item = $scope.model.value[i]; + item.label = item.hasOwnProperty("label") ? item.label : item.value; + } + + function validLabel(label) { + return label !== null && typeof label !== "undefined" && label !== "" && label.length && label.length > 0; + } + $scope.remove = function (item, evt) { evt.preventDefault(); $scope.model.value = _.reject($scope.model.value, function (x) { - return x.value === item.value; + return x.value === item.value && x.label === item.label; }); }; @@ -63,15 +85,15 @@ evt.preventDefault(); if ($scope.newColor) { + var newLabel = validLabel($scope.newLabel) ? $scope.newLabel : $scope.newColor; var exists = _.find($scope.model.value, function(item) { - return item.value.toUpperCase() == $scope.newColor.toUpperCase(); + return item.value.toUpperCase() === $scope.newColor.toUpperCase() || item.label.toUpperCase() === newLabel.toUpperCase(); }); if (!exists) { - $scope.model.value.push({ value: $scope.newColor }); - //$scope.newColor = defaultColor; - // set colorpicker to default color - //var elem = $element.find("input"); - //elem.spectrum("set", $scope.newColor); + $scope.model.value.push({ + value: $scope.newColor, + label: newLabel + }); $scope.hasError = false; return; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 9b3316ec1a..229ec614de 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it -function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper) { +function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper, localizationService) { var unsubscribe; @@ -154,7 +154,6 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } } - if ($routeParams.section === "settings" && $routeParams.tree === "documentTypes") { //if the content-picker is being rendered inside the document-type editor, we don't need to process the startnode query dialogOptions.startNodeId = -1; @@ -287,8 +286,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper entityResource.getUrl(entity.id, entityType).then(function(data){ // update url angular.forEach($scope.renderModel, function(item){ - if(item.id === entity.id) { - item.url = data; + if (item.id === entity.id) { + if (entity.trashed) { + item.url = localizationService.dictionary.general_recycleBin; + } else { + item.url = data; + } } }); }); @@ -330,6 +333,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper "icon": item.icon, "path": item.path, "url": item.url, + "trashed": item.trashed, "published": (item.metaData && item.metaData.IsPublished === false && entityType === "Document") ? false : true // only content supports published/unpublished content so we set everything else to published so the UI looks correct }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 75cc74f02d..1755840cc8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -1,5 +1,8 @@
    +

    +

    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 8455571c42..315a7f15f3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -8,6 +8,6 @@ val-server="value" /> Required - Invalid email + Invalid email
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index 421aaf0d40..381db5a38c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -48,14 +48,18 @@ - {{currentCell.grid}} + {{currentCell.grid}}
    + + + + - + Delete diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index bb1da209bf..a282334cb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -81,6 +81,7 @@ angular.module("umbraco") var notIncludedRte = []; var cancelMove = false; + var startingArea; $scope.sortableOptionsCell = { distance: 10, @@ -112,9 +113,11 @@ angular.module("umbraco") }, over: function (event, ui) { - var allowedEditors = $(event.target).scope().area.allowed; + var area = $(event.target).scope().area; + var allowedEditors = area.allowed; - if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) { + if (($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) || + (startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1)) { $scope.$apply(function () { $(event.target).scope().area.dropNotAllowed = true; @@ -168,6 +171,10 @@ angular.module("umbraco") start: function (e, ui) { + //Get the starting area for reference + var area = $(e.target).scope().area; + startingArea = area; + // fade out control when sorting ui.item.context.style.display = "block"; ui.item.context.style.opacity = "0.5"; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 4ddca47489..c2513b9760 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -224,7 +224,7 @@ -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html index 06bdfe2e99..fa2da2f9e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html @@ -65,7 +65,9 @@ ng-repeat="area in layout.areas | filter:zeroWidthFilter" ng-style="{width: percentage(area.grid) + '%', 'max-width': '100%'}"> -
    +
    +

    {{area.maxItems}}

    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 8a7b20498d..91777bfc3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location) { + function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location, localizationService) { //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; @@ -26,17 +26,47 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // the mediaResource has server side auth configured for which the user must have // access to the media section, if they don't they'll get auth errors. The entityResource // acts differently in that it allows access if the user has access to any of the apps that - // might require it's use. Therefore we need to use the metatData property to get at the thumbnail + // might require it's use. Therefore we need to use the metaData property to get at the thumbnail // value. - entityResource.getByIds(ids, "Media").then(function (medias) { + entityResource.getByIds(ids, "Media").then(function(medias) { - _.each(medias, function (media, i) { + // The service only returns item results for ids that exist (deleted items are silently ignored). + // This results in the picked items value to be set to contain only ids of picked items that could actually be found. + // Since a referenced item could potentially be restored later on, instead of changing the selected values here based + // on whether the items exist during a save event - we should keep "placeholder" items for picked items that currently + // could not be fetched. This will preserve references and ensure that the state of an item does not differ depending + // on whether it is simply resaved or not. + // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders + // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. + + medias = _.map(ids, + function(id) { + var found = _.find(medias, + function(m) { + // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and + // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() + // compares and be completely sure it works. + return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); + }); + if (found) { + return found; + } else { + return { + name: localizationService.dictionary.mediaPicker_deletedItem, + id: $scope.model.config.idType !== "udi" ? id : null, + udi: $scope.model.config.idType === "udi" ? id : null, + icon: "icon-picture", + thumbnail: null, + trashed: true + }; + } + }); - //only show non-trashed items - if (media.parentId >= -1) { - - if (!media.thumbnail) { + _.each(medias, + function(media, i) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } @@ -44,12 +74,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if ($scope.model.config.idType === "udi") { $scope.ids.push(media.udi); - } - else { + } else { $scope.ids.push(media.id); } - } - }); + }); $scope.sync(); }); @@ -82,8 +110,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl submit: function(model) { _.each(model.selectedImages, function(media, i) { - - if (!media.thumbnail) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } @@ -101,10 +129,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; - } }; - }; $scope.sortableOptions = { @@ -142,5 +168,4 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl //update the display val again if it has changed from the server setupViewModel(); }; - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index f3aab992dd..3ef1430cd3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -1,47 +1,47 @@ - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 702d19a509..65a62f599c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -76,6 +76,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", icon.isCustom = false; break; case "styleselect": + case "fontsizeselect": icon.name = "icon-list"; icon.isCustom = true; break; diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index da10edda09..a915fc726c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1027,9 +1027,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7720 + 7740 / - http://localhost:7720 + http://localhost:7740 False False diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index 8b189ae1a0..1437d4f14b 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -60,21 +60,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = property.Value.ToString(); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + if (cssVals.Any()) + attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index e672aa2a11..7b4f602b26 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -60,21 +60,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = property.Value.ToString(); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + if (cssVals.Any()) + attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml index 0cac4eb1ff..4cf2e73658 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml @@ -4,9 +4,13 @@ @if (Model.editor.config.markup != null) { string markup = Model.editor.config.markup.ToString(); - - markup = markup.Replace("#value#", Model.value.ToString()); - markup = markup.Replace("#style#", Model.editor.config.style.ToString()); + var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); + markup = markup.Replace("#value#", umbracoHelper.ReplaceLineBreaksForHtml(HttpUtility.HtmlEncode(Model.value.ToString()))); + + if (Model.editor.config.style != null) + { + markup = markup.Replace("#style#", Model.editor.config.style.ToString()); + } @Html.Raw(markup) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 23895db95b..16ed8de775 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -935,6 +935,15 @@ Mange hilsner fra Umbraco robotten Du har ikke konfigureret nogen godkendte farver + + Du har valgt et dokument som er slettet eller lagt i papirkurven + Du har valgt dokumenter som er slettede eller lagt i papirkurven + + + Du har valgt et medie som er slettet eller lagt i papirkurven + Du har valgt medier som er slettede eller lagt i papirkurven + Slettet medie + indtast eksternt link vælg en intern side @@ -1251,6 +1260,9 @@ Mange hilsner fra Umbraco robotten Vælg standard er tilføjet + Maksimalt emner + Efterlad blank eller sat til 0 ubegrænset for + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 30ebc137b9..3cb67797de 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -87,14 +87,14 @@ Domain '%0%' has been updated Edit Current Domains - Inherit Culture - or inherit culture from parent nodes. Will also apply
    + or inherit culture from parent nodes. Will also apply
    to the current node, unless a domain below applies too.]]>
    Domains @@ -351,11 +351,11 @@ Number of columns Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only + Select a placeholder id from the list below. You can only choose Id's from the current template's master.]]> Click on the image to see full size @@ -397,15 +397,15 @@ - %0%' below
    You can add additional languages under the 'languages' in the menu on the left + %0%' below
    You can add additional languages under the 'languages' in the menu on the left ]]>
    Culture Name Edit the key of the dictionary item. - @@ -1081,6 +1081,15 @@ To manage your website, simply open the Umbraco back office and start adding con You have not configured any approved colours + + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Deleted item + enter external link choose internal page @@ -1401,6 +1410,8 @@ To manage your website, simply open the Umbraco back office and start adding con Allow all editors Allow all row configurations + Leave blank or set to 0 for unlimited + Maximum items Set as default Choose extra Choose default @@ -1869,4 +1880,4 @@ To manage your website, simply open the Umbraco back office and start adding con characters left - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index e69f2dcaf2..dce2ef165e 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1087,6 +1087,15 @@ To manage your website, simply open the Umbraco back office and start adding con You have not configured any approved colors + + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Deleted item + enter external link choose internal page @@ -1405,6 +1414,8 @@ To manage your website, simply open the Umbraco back office and start adding con Allow all editors Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited Set as default Choose extra Choose default diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index 2de944a7df..99283482aa 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -789,6 +789,9 @@ Permitir todos los controles de edición Permitir todas las configuraciones de fila + Artículos máximos + Laat dit leeg of is ingesteld op -1 voor onbeperkt + Dejar en blanco o se establece en 0 para ilimitada Campo opcional diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index c480373fd6..7b170cdc41 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -1077,7 +1077,9 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Alle editors toelaten Alle rijconfiguraties toelaten - Instellen als standaard + Maximale artikelen + Laat dit leeg of is ingesteld op -1 voor onbeperkt + Instellen als standaard Kies extra Kies standaard zijn toegevoegd diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs b/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs index 7e09d0b425..ababea628a 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs +++ b/src/Umbraco.Web.UI/umbraco/dialogs/Publish.aspx.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs } DocumentId = doc.Id; - PageName = doc.Name; + PageName = Server.HtmlEncode(doc.Name); DocumentPath = doc.Path; } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 511b33f812..d53ee84e7c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -643,9 +643,7 @@ namespace Umbraco.Web.Editors ShowMessageForPublishStatus(publishStatus.Result, display); break; } - - UpdatePreviewContext(contentItem.PersistedContent.Id); - + //If the item is new and the operation was cancelled, we need to return a different // status code so the UI can handle it since it won't be able to redirect since there // is no Id to redirect to! @@ -875,24 +873,6 @@ namespace Umbraco.Web.Editors } } - /// - /// Checks if the user is currently in preview mode and if so will update the preview content for this item - /// - /// - private void UpdatePreviewContext(int contentId) - { - var previewId = Request.GetPreviewCookieValue(); - if (previewId.IsNullOrWhiteSpace()) return; - Guid id; - if (Guid.TryParse(previewId, out id)) - { - var d = new Document(contentId); - var pc = new PreviewContent(UmbracoUser, id, false); - pc.PrepareDocument(UmbracoUser, d, true); - pc.SavePreviewSet(); - } - } - /// /// Maps the dto property values to the persisted model /// diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 0b1a697eae..1364a8e2af 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -599,12 +599,14 @@ namespace Umbraco.Web.Editors throw; } } - - [EnsureUserPermissionForMedia("folder.ParentId")] - public MediaItemDisplay PostAddFolder(EntityBasic folder) + + public MediaItemDisplay PostAddFolder(PostedFolder folder) { + var intParentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true); + var mediaService = ApplicationContext.Services.MediaService; - var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); + + var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder); mediaService.Save(f, Security.CurrentUser.Id); return Mapper.Map(f); @@ -636,66 +638,15 @@ namespace Umbraco.Web.Editors if (result.FileData.Count == 0) { return Request.CreateResponse(HttpStatusCode.NotFound); - } - + } + //get the string json from the request - int parentId; bool entityFound; GuidUdi parentUdi; - string currentFolderId = result.FormData["currentFolder"]; - // test for udi - if (GuidUdi.TryParse(currentFolderId, out parentUdi)) - { - currentFolderId = parentUdi.Guid.ToString(); - } - - if (int.TryParse(currentFolderId, out parentId) == false) - { - // if a guid then try to look up the entity - Guid idGuid; - if (Guid.TryParse(currentFolderId, out idGuid)) - { - var entity = Services.EntityService.GetByKey(idGuid); - if (entity != null) - { - entityFound = true; - parentId = entity.Id; - } - else - { - throw new EntityNotFoundException(currentFolderId, "The passed id doesn't exist"); - } - } - else - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); - } - - if (entityFound == false) - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); - } - } - - - //ensure the user has access to this folder by parent id! - if (CheckPermissions( - new Dictionary(), - Security.CurrentUser, - Services.MediaService, - Services.EntityService, - parentId) == false) - { - return Request.CreateResponse( - HttpStatusCode.Forbidden, - new SimpleNotificationModel(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), - SpeechBubbleIcon.Warning))); - } - + string currentFolderId = result.FormData["currentFolder"]; + int parentId = GetParentIdAsInt(currentFolderId, validatePermissions: true); + var tempFiles = new PostedFiles(); var mediaService = ApplicationContext.Services.MediaService; - - + //in case we pass a path with a folder in it, we will create it and upload media to it. if (result.FormData.ContainsKey("path")) { @@ -830,6 +781,69 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } + /// + /// Given a parent id which could be a GUID, UDI or an INT, this will resolve the INT + /// + /// + /// + /// If true, this will check if the current user has access to the resolved integer parent id + /// and if that check fails an unauthorized exception will occur + /// + /// + private int GetParentIdAsInt(string parentId, bool validatePermissions) + { + int intParentId; + GuidUdi parentUdi; + + // test for udi + if (GuidUdi.TryParse(parentId, out parentUdi)) + { + parentId = parentUdi.Guid.ToString(); + } + + //if it's not an INT then we'll check for GUID + if (int.TryParse(parentId, out intParentId) == false) + { + // if a guid then try to look up the entity + Guid idGuid; + if (Guid.TryParse(parentId, out idGuid)) + { + var entity = Services.EntityService.GetByKey(idGuid); + if (entity != null) + { + intParentId = entity.Id; + } + else + { + throw new EntityNotFoundException(parentId, "The passed id doesn't exist"); + } + } + else + { + throw new HttpResponseException( + Request.CreateValidationErrorResponse("The request was not formatted correctly, the parentId is not an integer, Guid or UDI")); + } + } + + //ensure the user has access to this folder by parent id! + if (validatePermissions && CheckPermissions( + new Dictionary(), + Security.CurrentUser, + Services.MediaService, + Services.EntityService, + intParentId) == false) + { + throw new HttpResponseException(Request.CreateResponse( + HttpStatusCode.Forbidden, + new SimpleNotificationModel(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), + SpeechBubbleIcon.Warning)))); + } + + return intParentId; + } + /// /// Ensures the item can be moved/copied to the new location /// diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index c4ae1c4eac..2caeb012bc 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -92,6 +92,8 @@ namespace Umbraco.Web using (var outputms = new MemoryStream()) { + bool lengthReached = false; + using (var outputtw = new StreamWriter(outputms)) { using (var ms = new MemoryStream()) @@ -106,7 +108,6 @@ namespace Umbraco.Web using (TextReader tr = new StreamReader(ms)) { bool isInsideElement = false, - lengthReached = false, insideTagSpaceEncountered = false, isTagClose = false; @@ -254,10 +255,15 @@ namespace Umbraco.Web //Check to see if there is an empty char between the hellip and the output string //if there is, remove it - if (string.IsNullOrWhiteSpace(firstTrim) == false) + if (addElipsis && lengthReached && string.IsNullOrWhiteSpace(firstTrim) == false) { result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) : firstTrim; } + else + { + result = firstTrim; + } + return new HtmlString(result); } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index d0194ac009..1e59aa1c93 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "template")] public string TemplateAlias { get; set; } - + [DataMember(Name = "allowedTemplates")] public IDictionary AllowedTemplates { get; set; } @@ -54,7 +54,7 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "allowedActions")] public IEnumerable AllowedActions { get; set; } - + [DataMember(Name = "isBlueprint")] public bool IsBlueprint { get; set; } } diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs index 9f6e5b28da..12afceea05 100644 --- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Models.Validation; diff --git a/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs b/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs new file mode 100644 index 0000000000..35cd908787 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Used to create a folder with the MediaController + /// + [DataContract] + public class PostedFolder + { + [DataMember(Name = "parentId")] + public string ParentId { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index 9741a2cff4..107ea590c2 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -43,7 +43,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Urls, expression => expression.MapFrom(content => UmbracoContext.Current == null - ? new[] {"Cannot generate urls without a current Umbraco Context"} + ? new[] { "Cannot generate urls without a current Umbraco Context" } : content.GetContentUrls(UmbracoContext.Current))) .ForMember(display => display.Properties, expression => expression.Ignore()) .ForMember(display => display.AllowPreview, expression => expression.Ignore()) @@ -85,8 +85,8 @@ namespace Umbraco.Web.Models.Mapping private static DateTime? GetPublishedDate(IContent content) { - var date = ((Content) content).PublishedDate; - return date == default (DateTime) ? (DateTime?) null : date; + var date = ((Content)content).PublishedDate; + return date == default(DateTime) ? (DateTime?)null : date; } /// @@ -110,16 +110,22 @@ namespace Umbraco.Web.Models.Mapping var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); display.TreeNodeUrl = url; - } - + } + + //set default template if template isn't set + if (string.IsNullOrEmpty(display.TemplateAlias)) + display.TemplateAlias = content.ContentType.DefaultTemplate == null + ? string.Empty + : content.ContentType.DefaultTemplate.Alias; + if (content.ContentType.IsContainer) { TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, localizedText); - } - + } + TabsAndPropertiesResolver.MapGenericProperties(content, display, localizedText); } - + /// /// Resolves a from the item and checks if the current user /// has access to see this data @@ -127,7 +133,7 @@ namespace Umbraco.Web.Models.Mapping private class ContentTypeBasicResolver : ValueResolver { protected override ContentTypeBasic ResolveCore(IContent source) - { + { //TODO: This would be much nicer with the IUmbracoContextAccessor so we don't use singletons //If this is a web request and there's a user signed in and the // user has access to the settings section, we will @@ -136,11 +142,11 @@ namespace Umbraco.Web.Models.Mapping { var contentTypeBasic = Mapper.Map(source.ContentType); return contentTypeBasic; - } + } //no access return null; } - } + } /// /// Creates the list of action buttons allowed for this user - Publish, Send to publish, save, unpublish returned as the button's 'letter' @@ -163,13 +169,13 @@ namespace Umbraco.Web.Models.Mapping } var svc = _userService.Value; - var permissions = svc.GetPermissions( - //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is - // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null - // refrence exception :( - UmbracoContext.Current.Security.CurrentUser, - // Here we need to do a special check since this could be new content, in which case we need to get the permissions - // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. + var permissions = svc.GetPermissions( + //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is + // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null + // refrence exception :( + UmbracoContext.Current.Security.CurrentUser, + // Here we need to do a special check since this could be new content, in which case we need to get the permissions + // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. source.HasIdentity ? source.Id : source.ParentId) .GetAllPermissions(); diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index 273dbd34f3..ec72c9839d 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -6,7 +6,6 @@ using Examine; using Examine.LuceneEngine.Providers; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; @@ -21,7 +20,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(x => x.Udi, expression => expression.MapFrom(x => Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(x.NodeObjectTypeId), x.Key))) .ForMember(basic => basic.Icon, expression => expression.MapFrom(entity => entity.ContentTypeIcon)) - .ForMember(dto => dto.Trashed, expression => expression.Ignore()) + .ForMember(dto => dto.Trashed, expression => expression.MapFrom(x => x.Trashed)) .ForMember(x => x.Alias, expression => expression.Ignore()) .AfterMap((entity, basic) => { @@ -98,8 +97,8 @@ namespace Umbraco.Web.Models.Mapping else if (entity.NodeObjectTypeId == Constants.ObjectTypes.TemplateTypeGuid) basic.Icon = "icon-newspaper-alt"; } - }); - + }); + config.CreateMap() //default to document icon .ForMember(x => x.Score, expression => expression.MapFrom(result => result.Score)) diff --git a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs index e7750c4c65..a504ed0431 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs @@ -1,9 +1,12 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.RegularExpressions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -11,7 +14,7 @@ namespace Umbraco.Web.PropertyEditors { internal class ColorListPreValueEditor : ValueListPreValueEditor { - + public ColorListPreValueEditor() { var field = Fields.First(); @@ -23,14 +26,98 @@ namespace Umbraco.Web.PropertyEditors //change the label field.Name = "Add color"; //need to have some custom validation happening here - field.Validators.Add(new ColorListValidator()); + field.Validators.Add(new ColorListValidator()); + + Fields.Insert(0, new PreValueField + { + Name = "Include labels?", + View = "boolean", + Key = "useLabel", + Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string." + }); } public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) { var dictionary = persistedPreVals.FormatAsDictionary(); - var arrayOfVals = dictionary.Select(item => item.Value).ToList(); - return new Dictionary { { "items", arrayOfVals.ToDictionary(x => x.Id, x => x.Value) } }; + var items = dictionary + .Where(x => x.Key != "useLabel") + .ToDictionary(x => x.Value.Id, x => x.Value.Value); + + var items2 = new Dictionary(); + foreach (var item in items) + { + if (item.Value.DetectIsJson() == false) + { + items2[item.Key] = item.Value; + continue; + } + + try + { + items2[item.Key] = JsonConvert.DeserializeObject(item.Value); + } + catch + { + // let's say parsing Json failed, so what we have is the string - build json + items2[item.Key] = new JObject { { "color", item.Value }, { "label", item.Value } }; + } + } + + var result = new Dictionary { { "items", items2 } }; + var useLabel = dictionary.ContainsKey("useLabel") && dictionary["useLabel"].Value == "1"; + if (useLabel) + result["useLabel"] = dictionary["useLabel"].Value; + + return result; + } + + public override IDictionary ConvertEditorToDb(IDictionary editorValue, PreValueCollection currentValue) + { + var val = editorValue["items"] as JArray; + var result = new Dictionary(); + if (val == null) return result; + + try + { + object useLabelObj; + var useLabel = false; + if (editorValue.TryGetValue("useLabel", out useLabelObj)) + { + useLabel = useLabelObj is string && (string) useLabelObj == "1"; + result["useLabel"] = new PreValue(useLabel ? "1" : "0"); + } + + // get all non-empty values + var index = 0; + foreach (var preValue in val.OfType() + .Where(x => x["value"] != null) + .Select(x => + { + var idString = x["id"] == null ? "0" : x["id"].ToString(); + int id; + if (int.TryParse(idString, out id) == false) id = 0; + + var color = x["value"].ToString(); + if (string.IsNullOrWhiteSpace(color)) return null; + + var label = x["label"].ToString(); + return new PreValue(id, useLabel + ? JsonConvert.SerializeObject(new { value = color, label = label }) + : color); + }) + .WhereNotNull()) + { + result.Add(index.ToInvariantString(), preValue); + index++; + } + } + catch (Exception ex) + { + LogHelper.Error("Could not deserialize the posted value: " + val, ex); + } + + return result; } internal class ColorListValidator : IPropertyValidator @@ -39,7 +126,7 @@ namespace Umbraco.Web.PropertyEditors { var json = value as JArray; if (json == null) yield break; - + //validate each item which is a json object for (var index = 0; index < json.Count; index++) { diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 763e28b608..f4beb9d5b8 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -20,9 +21,21 @@ namespace Umbraco.Web.Scheduling _appContext = appContext; } + private ILogger Logger { get { return _appContext.ProfilingLogger.Logger; } } + public override async Task PerformRunAsync(CancellationToken token) { if (_appContext == null) return true; // repeat... + + switch (_appContext.GetCurrentServerRole()) + { + case ServerRole.Slave: + Logger.Debug("Does not run on slave servers."); + return true; // DO repeat, server role can change + case ServerRole.Unknown: + Logger.Debug("Does not run on servers with unknown role."); + return true; // DO repeat, server role can change + } // ensure we do not run if not main domain, but do NOT lock it if (_appContext.MainDom.IsMainDom == false) @@ -31,7 +44,7 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) + using (_appContext.ProfilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) { string umbracoAppUrl = null; diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 46a756e0ad..919a531549 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Scheduling private ILogger Logger { get { return _appContext.ProfilingLogger.Logger; } } - public override async Task PerformRunAsync(CancellationToken token) + public override bool PerformRun() { if (_appContext == null) return true; // repeat... @@ -94,7 +94,7 @@ namespace Umbraco.Web.Scheduling public override bool IsAsync { - get { return true; } + get { return false; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UI/LegacyDialogHandler.cs b/src/Umbraco.Web/UI/LegacyDialogHandler.cs index c51a8bb08b..0c3d09f1a2 100644 --- a/src/Umbraco.Web/UI/LegacyDialogHandler.cs +++ b/src/Umbraco.Web/UI/LegacyDialogHandler.cs @@ -126,8 +126,10 @@ namespace Umbraco.Web.UI { var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Create, nodeType); if (task == null) - throw new InvalidOperationException( - string.Format("Could not task for operation {0} for node type {1}", Operation.Create, nodeType)); + { + //if no task was found it will use the default task and we cannot validate the application assigned so return true + return true; + } var dialogTask = task as LegacyDialogTask; if (dialogTask != null) @@ -154,8 +156,10 @@ namespace Umbraco.Web.UI { var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Delete, nodeType); if (task == null) - throw new InvalidOperationException( - string.Format("Could not task for operation {0} for node type {1}", Operation.Delete, nodeType)); + { + //if no task was found it will use the default task and we cannot validate the application assigned so return true + return true; + } var dialogTask = task as LegacyDialogTask; if (dialogTask != null) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0a40af560d..ec3dbcf043 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -393,6 +393,7 @@ + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 5773d88f73..342b8daa16 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -46,12 +46,11 @@ namespace Umbraco.Web /// protected virtual void ConfigureMiddleware(IAppBuilder app) { - //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN - // cookie configuration, this must be declared after it. + + // Configure OWIN for authentication. + ConfigureUmbracoAuthentication(app); + app - .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) - .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) - .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.Authorize) .UseSignalR() .FinalizeMiddlewareConfiguration(); } @@ -68,6 +67,20 @@ namespace Umbraco.Web Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); } + /// + /// Configure external/OAuth login providers + /// + /// + protected virtual void ConfigureUmbracoAuthentication(IAppBuilder app) + { + // Ensure owin is configured for Umbraco back office authentication. + // Front-end OWIN cookie configuration must be declared after this code. + app + .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.Authorize); + } + /// /// Raised when the middleware has been configured /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs index 1bd3918ec2..969550d9a9 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs @@ -162,6 +162,13 @@ namespace umbraco.presentation.developer.packages /*Data types */ cms.businesslogic.datatype.DataTypeDefinition[] umbDataType = cms.businesslogic.datatype.DataTypeDefinition.GetAll(); + + // sort array by name + Array.Sort(umbDataType, delegate(cms.businesslogic.datatype.DataTypeDefinition umbDataType1, cms.businesslogic.datatype.DataTypeDefinition umbDataType2) + { + return umbDataType1.Text.CompareTo(umbDataType2.Text); + }); + foreach (cms.businesslogic.datatype.DataTypeDefinition umbDtd in umbDataType) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/importDocumenttype.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/importDocumenttype.aspx.cs index 27c1724bff..147e7604c1 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/importDocumenttype.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/importDocumenttype.aspx.cs @@ -70,10 +70,11 @@ namespace umbraco.presentation.umbraco.dialogs private void import_Click(object sender, EventArgs e) { var xd = new XmlDocument(); + xd.XmlResolver = null; xd.Load(tempFile.Value); var userId = base.getUser().Id; - + var element = XElement.Parse(xd.InnerXml); var importContentTypes = ApplicationContext.Current.Services.PackagingService.ImportContentTypes(element, userId); var contentType = importContentTypes.FirstOrDefault(); @@ -104,7 +105,8 @@ namespace umbraco.presentation.umbraco.dialogs documentTypeFile.PostedFile.SaveAs(fileName); var xd = new XmlDocument(); - xd.Load(fileName); + xd.XmlResolver = null; + xd.Load(fileName); dtName.Text = xd.DocumentElement.SelectSingleNode("//DocumentType/Info/Name").FirstChild.Value; dtAlias.Text = xd.DocumentElement.SelectSingleNode("//DocumentType/Info/Alias").FirstChild.Value; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs index 70137da920..195ac15ec8 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/notifications.aspx.cs @@ -27,7 +27,7 @@ namespace umbraco.dialogs protected void Page_Load(object sender, EventArgs e) { Button1.Text = ui.Text("update"); - pane_form.Text = ui.Text("notifications", "editNotifications", node.Text, base.getUser()); + pane_form.Text = ui.Text("notifications", "editNotifications", Server.HtmlEncode(node.Text), base.getUser()); } #region Web Form Designer generated code diff --git a/src/umbraco.cms/businesslogic/member/Member.cs b/src/umbraco.cms/businesslogic/member/Member.cs index 71c8eda701..c46e4d914c 100644 --- a/src/umbraco.cms/businesslogic/member/Member.cs +++ b/src/umbraco.cms/businesslogic/member/Member.cs @@ -773,14 +773,20 @@ namespace umbraco.cms.businesslogic.member { var temp = new Hashtable(); + var groupIds = new List(); + using (var sqlHelper = Application.SqlHelper) using (var dr = sqlHelper.ExecuteReader( "select memberGroup from cmsMember2MemberGroup where member = @id", sqlHelper.CreateParameter("@id", Id))) { while (dr.Read()) - temp.Add(dr.GetInt("memberGroup"), - new MemberGroup(dr.GetInt("memberGroup"))); + groupIds.Add(dr.GetInt("memberGroup")); + } + + foreach (var groupId in groupIds) + { + temp.Add(groupId, new MemberGroup(groupId)); } _groups = temp; }