diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 2c3855727b..525bff2999 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Configuration //make this volatile so that we can ensure thread safety with a double check lock private static volatile string _reservedUrlsCache; private static string _reservedPathsCache; - private static StartsWithContainer _reservedList = new StartsWithContainer(); + private static HashSet _reservedList = new HashSet(); private static string _reservedPaths; private static string _reservedUrls; //ensure the built on (non-changeable) reserved paths are there at all times @@ -767,38 +767,31 @@ namespace Umbraco.Core.Configuration // store references to strings to determine changes _reservedPathsCache = GlobalSettings.ReservedPaths; _reservedUrlsCache = GlobalSettings.ReservedUrls; - - string _root = SystemDirectories.Root.Trim().ToLower(); - + // add URLs and paths to a new list - StartsWithContainer _newReservedList = new StartsWithContainer(); - foreach (string reservedUrl in _reservedUrlsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)) + var newReservedList = new HashSet(); + foreach (var reservedUrlTrimmed in _reservedUrlsCache + .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/")) + .Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false)) { - if (string.IsNullOrWhiteSpace(reservedUrl)) - continue; - - - //resolves the url to support tilde chars - string reservedUrlTrimmed = IOHelper.ResolveUrl(reservedUrl.Trim()).Trim().ToLower(); - if (reservedUrlTrimmed.Length > 0) - _newReservedList.Add(reservedUrlTrimmed); + newReservedList.Add(reservedUrlTrimmed); } - foreach (string reservedPath in _reservedPathsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var reservedPathTrimmed in _reservedPathsCache + .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/")) + .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false)) { - bool trimEnd = !reservedPath.EndsWith("/"); - if (string.IsNullOrWhiteSpace(reservedPath)) - continue; - - //resolves the url to support tilde chars - string reservedPathTrimmed = IOHelper.ResolveUrl(reservedPath.Trim()).Trim().ToLower(); - - if (reservedPathTrimmed.Length > 0) - _newReservedList.Add(reservedPathTrimmed + (reservedPathTrimmed.EndsWith("/") ? "" : "/")); + newReservedList.Add(reservedPathTrimmed); } // use the new list from now on - _reservedList = _newReservedList; + _reservedList = newReservedList; } } } @@ -806,107 +799,17 @@ namespace Umbraco.Core.Configuration //The url should be cleaned up before checking: // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' // * We shouldn't be comparing the query at all - var pathPart = url.Split('?')[0]; - if (!pathPart.Contains(".") && !pathPart.EndsWith("/")) + var pathPart = url.Split(new[] {'?'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); + if (pathPart.Contains(".") == false) { - pathPart += "/"; + pathPart = pathPart.EnsureEndsWith('/'); } // return true if url starts with an element of the reserved list - return _reservedList.StartsWith(pathPart.ToLowerInvariant()); + return _reservedList.Any(x => pathPart.InvariantStartsWith(x)); } - /// - /// Structure that checks in logarithmic time - /// if a given string starts with one of the added keys. - /// - private class StartsWithContainer - { - /// Internal sorted list of keys. - public SortedList _list - = new SortedList(StartsWithComparator.Instance); - - /// - /// Adds the specified new key. - /// - /// The new key. - public void Add(string newKey) - { - // if the list already contains an element that begins with newKey, return - if (String.IsNullOrEmpty(newKey) || StartsWith(newKey)) - return; - - // create a new collection, so the old one can still be accessed - SortedList newList - = new SortedList(_list.Count + 1, StartsWithComparator.Instance); - - // add only keys that don't already start with newKey, others are unnecessary - foreach (string key in _list.Keys) - if (!key.StartsWith(newKey)) - newList.Add(key, null); - // add the new key - newList.Add(newKey, null); - - // update the list (thread safe, _list was never in incomplete state) - _list = newList; - } - - /// - /// Checks if the given string starts with any of the added keys. - /// - /// The target. - /// true if a key is found that matches the start of target - /// - /// Runs in O(s*log(n)), with n the number of keys and s the length of target. - /// - public bool StartsWith(string target) - { - return _list.ContainsKey(target); - } - - /// Comparator that tests if a string starts with another. - /// Not a real comparator, since it is not reflexive. (x==y does not imply y==x) - private sealed class StartsWithComparator : IComparer - { - /// Default string comparer. - private readonly static Comparer _stringComparer = Comparer.Default; - - /// Gets an instance of the StartsWithComparator. - public static readonly StartsWithComparator Instance = new StartsWithComparator(); - - /// - /// Tests if whole begins with all characters of part. - /// - /// The part. - /// The whole. - /// - /// Returns 0 if whole starts with part, otherwise performs standard string comparison. - /// - public int Compare(string part, string whole) - { - // let the default string comparer deal with null or when part is not smaller then whole - if (part == null || whole == null || part.Length >= whole.Length) - return _stringComparer.Compare(part, whole); - - ////ensure both have a / on the end - //part = part.EndsWith("/") ? part : part + "/"; - //whole = whole.EndsWith("/") ? whole : whole + "/"; - //if (part.Length >= whole.Length) - // return _stringComparer.Compare(part, whole); - - // loop through all characters that part and whole have in common - int pos = 0; - bool match; - do - { - match = (part[pos] == whole[pos]); - } while (match && ++pos < part.Length); - - // return result of last comparison - return match ? 0 : (part[pos] < whole[pos] ? -1 : 1); - } - } - } + } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs new file mode 100644 index 0000000000..50f78ca66d --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeOne +{ + /// + /// This fixes the storage of user languages from the old format like en_us to en-US + /// + [Migration("7.3.1", 0, GlobalSettings.UmbracoMigrationName)] + public class UpdateUserLanguagesToIsoCode : MigrationBase + { + public UpdateUserLanguagesToIsoCode(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var userData = Context.Database.Fetch(new Sql().Select("*").From(SqlSyntax)); + foreach (var user in userData.Where(x => x.UserLanguage.Contains("_"))) + { + var languageParts = user.UserLanguage.Split('_'); + if (languageParts.Length == 2) + { + Update.Table("umbracoUser") + .Set(new {userLanguage = languageParts[0] + "-" + languageParts[1].ToUpperInvariant()}) + .Where(new {id = user.Id}); + } + } + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs index 770ee65aed..c7cbc0f473 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs @@ -2,6 +2,7 @@ using System; namespace Umbraco.Core.PropertyEditors { + /// /// Maps a property source value to a data object. /// // todo: drop IPropertyEditorValueConverter support (when?). diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index bf94324388..9f5df7c886 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -187,7 +187,13 @@ namespace Umbraco.Core.Sync using (_profilingLogger.DebugDuration("Syncing from database...")) { ProcessDatabaseInstructions(); - PruneOldInstructions(); + switch (_appContext.GetCurrentServerRole()) + { + case ServerRole.Single: + case ServerRole.Master: + PruneOldInstructions(); + break; + } } } finally diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index fe37aa8fc7..cd10019f00 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -15,7 +15,12 @@ namespace Umbraco.Core.Sync // but at the moment it is exposed in CacheRefresher webservice // so for the time being we keep it as-is for backward compatibility reasons - // need the public one so it can be de-serialized + // need this public, parameter-less constructor so the web service messenger + // can de-serialize the instructions it receives + public RefreshInstruction() + { } + + // need this public one so it can be de-serialized - used by the Json thing // otherwise, should use GetInstructions(...) public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4552cac7f5..f9e3f3b38f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -435,6 +435,7 @@ + diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index 18ec84bf30..b82285862a 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -1163,6 +1163,36 @@ namespace Umbraco.Tests.Services Assert.That(propertyGroup.ParentId.HasValue, Is.False); } + [Test] + public void Can_Remove_PropertyGroup_Without_Removing_Property_Types() + { + var service = ServiceContext.ContentTypeService; + var basePage = (IContentType)MockedContentTypes.CreateBasicContentType(); + service.Save(basePage); + + var authorPropertyType = new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, "author") + { + Name = "Author", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeDefinitionId = -88 + }; + var authorAdded = basePage.AddPropertyType(authorPropertyType, "Content"); + service.Save(basePage); + + basePage = service.GetContentType(basePage.Id); + + var totalPt = basePage.PropertyTypes.Count(); + + basePage.RemovePropertyGroup("Content"); + service.Save(basePage); + + basePage = service.GetContentType(basePage.Id); + + Assert.AreEqual(totalPt, basePage.PropertyTypes.Count()); + } + [Test] public void Can_Add_PropertyGroup_With_Same_Name_On_Parent_and_Child() { diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs index e72290c163..8ccf158034 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs @@ -8,7 +8,7 @@ namespace Umbraco.Tests.TestHelpers.Entities { public static Content CreateSimpleContent(IContentType contentType) { - var content = new Content("Home", -1, contentType) { Language = "en-US", Level = 1, SortOrder = 1, CreatorId = 0, WriterId = 0 }; + var content = new Content("Home", -1, contentType) { Language = "en-US", Level = 1, SortOrder = 1, CreatorId = 0, WriterId = 0, Path = "-1" }; object obj = new { diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/logo.png b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo.png index 2b2ff80a1d..388354e8a5 100644 Binary files a/src/Umbraco.Web.UI.Client/src/assets/img/application/logo.png and b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo.png differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@2x.png b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@2x.png new file mode 100644 index 0000000000..3e7257a41a Binary files /dev/null and b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@2x.png differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@3x.png b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@3x.png new file mode 100644 index 0000000000..d4bc08f1bc Binary files /dev/null and b/src/Umbraco.Web.UI.Client/src/assets/img/application/logo@3x.png differ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js index d84bb8e24d..a94170d675 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js @@ -13,14 +13,16 @@ angular.module("umbraco.directives") restrict: 'E', replace: true, templateUrl: 'views/directives/umb-property.html', - + link: function(scope) { + scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; + }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope, $timeout) { var self = this; //set the API properties/methods - + self.property = $scope.property; self.setPropertyError = function(errorMsg) { $scope.property.propertyErrorMessage = errorMsg; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c136f88bc7..500f0fbad2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -139,6 +139,12 @@ function entityResource($q, $http, umbRequestHelper) { _.each(ids, function(item) { query += "ids=" + item + "&"; }); + + // if ids array is empty we need a empty variable in the querystring otherwise the service returns a error + if (ids.length === 0) { + query += "ids=&"; + } + query += "type=" + type; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index a60816c260..c953d96865 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -13,7 +13,11 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ //the null is important because we do an explicit bool check on this in the view //the avatar is by default the umbraco logo $scope.authenticated = null; - $scope.avatar = "assets/img/application/logo.png"; + $scope.avatar = [ + { value: "assets/img/application/logo.png" }, + { value: "assets/img/application/logo@2x.png" }, + { value: "assets/img/application/logo@3x.png" } + ]; $scope.touchDevice = appState.getGlobalState("touchDevice"); @@ -95,7 +99,14 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ $scope.$apply(function () { //this can be null if they time out if ($scope.user && $scope.user.emailHash) { - $scope.avatar = "https://www.gravatar.com/avatar/" + $scope.user.emailHash + ".jpg?s=64&d=mm"; + var avatarBaseUrl = "https://www.gravatar.com/avatar/", + hash = $scope.user.emailHash; + + $scope.avatar = [ + { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } + ]; } }); $("#avatar-img").fadeTo(1000, 1); diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 9ba3855b60..4e252d0a28 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -81,8 +81,8 @@ ul.sections li.avatar a { } ul.sections li.avatar a img { - border-radius: 16px; - width: 30px + border-radius: 50%; + width: 30px; } .faded ul.sections li { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index fc3015ed09..5ff55ba397 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -7,6 +7,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", searchText = value + "..."; }); + $scope.recursive = true; $scope.relateToOriginal = false; $scope.dialogTreeEventHandler = $({}); $scope.busy = false; @@ -95,7 +96,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.busy = true; $scope.error = false; - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal }) + contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index fddc07db22..f9a3c300d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -56,6 +56,12 @@ + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html index 16432bc1c4..32d55c3c90 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html @@ -5,7 +5,7 @@
-