From d980ffe7837f3f9779d43fd46f290fa1c4cafd7a Mon Sep 17 00:00:00 2001 From: Lennard Fonteijn Date: Wed, 23 Aug 2017 23:14:49 +0200 Subject: [PATCH 01/47] Added possibilty to ignore collisions from an IContentFinder --- src/Umbraco.Web/Routing/PublishedContentRequest.cs | 5 ++++- src/Umbraco.Web/Routing/UrlProviderExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 9c21958603..4a4968e4e3 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -614,6 +614,9 @@ namespace Umbraco.Web.Routing set { _headers = value; } } - + /// + /// Gets of sets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + public bool IgnorePublishedContentCollisions { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index d464b8962e..accd7018fe 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -78,7 +78,7 @@ namespace Umbraco.Web.Routing { urls.Add(ui.Text("content", "routeError", "(error)", umbracoContext.Security.CurrentUser)); } - else if (pcr.PublishedContent.Id != content.Id) + else if (pcr.IgnorePublishedContentCollisions == false && pcr.PublishedContent.Id != content.Id) { var o = pcr.PublishedContent; string s; From bf1a90020c8c66d594da1b737713be671b834e66 Mon Sep 17 00:00:00 2001 From: John Churchley Date: Wed, 20 Dec 2017 16:35:24 +0000 Subject: [PATCH 02/47] Allow Members tree to work independently of Member Types tree by giving it permission to access the MemberType.GetAllTypes Method --- src/Umbraco.Web/Editors/MemberTypeController.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 1d8766f253..7e9475bbd7 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Editors /// An API controller used for dealing with content types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + [UmbracoTreeAuthorize(new string[] { Constants.Trees.MemberTypes, Constants.Trees.Members})] public class MemberTypeController : ContentTypeControllerBase { /// @@ -48,6 +48,7 @@ namespace Umbraco.Web.Editors private readonly MembershipProvider _provider; + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetById(int id) { var ct = Services.MemberTypeService.Get(id); @@ -67,6 +68,7 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public HttpResponseMessage DeleteById(int id) { var foundType = Services.MemberTypeService.Get(id); @@ -93,6 +95,8 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// + + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) @@ -106,6 +110,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(result); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); @@ -129,6 +134,7 @@ namespace Umbraco.Web.Editors return Enumerable.Empty(); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { var savedCt = PerformPostSave( From c040cfefcdf413a096d643171fba0b4c0be4cb88 Mon Sep 17 00:00:00 2001 From: Rebecca Date: Thu, 22 Feb 2018 13:02:34 +1000 Subject: [PATCH 03/47] Failed Logon Attempts resets to 0 after maximum number of attempts --- .../Security/BackOfficeUserManager.cs | 62 ++++++++++++++----- .../src/views/users/user.controller.js | 2 + .../src/views/users/views/user/details.html | 2 +- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 4969fa5f2f..38861f300c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -57,7 +57,7 @@ namespace Umbraco.Core.Security [Obsolete("Use the overload specifying all dependencies instead")] public static BackOfficeUserManager Create( IdentityFactoryOptions options, - IUserService userService, + IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase membershipProvider) { @@ -94,7 +94,7 @@ namespace Umbraco.Core.Security manager.InitUserManager(manager, membershipProvider, contentSectionConfig, options); return manager; } - + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use the overload specifying all dependencies instead")] public static BackOfficeUserManager Create( @@ -129,7 +129,7 @@ namespace Umbraco.Core.Security [Obsolete("Use the overload specifying all dependencies instead")] protected void InitUserManager( BackOfficeUserManager manager, - MembershipProviderBase membershipProvider, + MembershipProviderBase membershipProvider, IdentityFactoryOptions options) { InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options); @@ -216,8 +216,8 @@ namespace Umbraco.Core.Security /// /// protected void InitUserManager( - BackOfficeUserManager manager, - MembershipProviderBase membershipProvider, + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, IDataProtectionProvider dataProtectionProvider, IContentSection contentSectionConfig) { @@ -233,7 +233,7 @@ namespace Umbraco.Core.Security //use a custom hasher based on our membership provider manager.PasswordHasher = GetDefaultPasswordHasher(membershipProvider); - + if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); @@ -373,7 +373,7 @@ namespace Umbraco.Core.Security } #region Overrides for password logic - + /// /// Logic used to validate a username and password /// @@ -469,7 +469,7 @@ namespace Umbraco.Core.Security await UpdateSecurityStampInternal(user); return IdentityResult.Success; - + } /// @@ -507,15 +507,22 @@ namespace Umbraco.Core.Security #endregion - public override Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd) + public override async Task SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd) { - var result = base.SetLockoutEndDateAsync(userId, lockoutEnd); + var result = await base.SetLockoutEndDateAsync(userId, lockoutEnd); // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + { RaiseAccountLockedEvent(userId); + } else + { RaiseAccountUnlockedEvent(userId); + //Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(userId); + + } return result; } @@ -537,16 +544,39 @@ namespace Umbraco.Core.Security RaiseResetAccessFailedCountEvent(userId); return await UpdateAsync(user); } - - - public override Task AccessFailedAsync(int userId) + + /// + /// Overides the microsoft ASP.NET user managment method + /// + /// + /// + /// returns a Async Task + /// + /// + /// Doesnt set fail attempts back to 0 + /// + public override async Task AccessFailedAsync(int userId) { - var result = base.AccessFailedAsync(userId); + var lockoutStore = (IUserLockoutStore)Store; + var user = await FindByIdAsync(userId); + if (user == null) + throw new InvalidOperationException("No user found by user id " + userId); + + var count = await lockoutStore.IncrementAccessFailedCountAsync(user); + + if (count >= MaxFailedAccessAttemptsBeforeLockout) + { + await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(DefaultAccountLockoutTimeSpan)); + //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 + //here we are persisting the value for the back office + } + + var result = await UpdateAsync(user); //Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Result.Succeeded) + if (result.Succeeded) RaiseLoginFailedEvent(userId); return result; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 0f3519c7ba..3a59a50c2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -341,8 +341,10 @@ vm.unlockUserButtonState = "busy"; usersResource.unlockUsers([vm.user.id]).then(function (data) { vm.user.userState = 0; + vm.user.failedPasswordAttempts = 0; setUserDisplayState(); vm.unlockUserButtonState = "success"; + formHelper.showNotifications(data); }, function (error) { vm.unlockUserButtonState = "error"; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 67f204aa2f..83c28f9f99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -358,4 +358,4 @@ - \ No newline at end of file + From 863679b3864889ca31b2316b949e28eab0554b07 Mon Sep 17 00:00:00 2001 From: Samuel Sperling Date: Thu, 22 Feb 2018 14:06:26 +1100 Subject: [PATCH 04/47] U4-10849 fixed Umbraco user field "User Last updated" and "last locked date" not updating after password change / account locking.. --- .../Models/Identity/BackOfficeIdentityUser.cs | 15 +++++++++++++-- .../Models/Identity/IdentityModelMappings.cs | 3 ++- src/Umbraco.Core/Models/Identity/IdentityUser.cs | 8 +++++++- .../Security/BackOfficeUserManager.cs | 1 + src/Umbraco.Core/Security/BackOfficeUserStore.cs | 11 +++++++++-- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 1a8cdc446f..3151492106 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -120,13 +120,22 @@ namespace Umbraco.Core.Models.Identity set { _tracker.SetPropertyValueAndDetectChanges(value, ref _userName, Ps.Value.UserNameSelector); } } + /// + /// LastPasswordChangeDateUtc so we can track changes to it + /// + public override DateTime? LastPasswordChangeDateUtc + { + get { return _lastPasswordChangeDateUtc; } + set { _tracker.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, Ps.Value.LastPasswordChangeDateUtcSelector); } + } + /// /// Override LastLoginDateUtc so we can track changes to it /// public override DateTime? LastLoginDateUtc { get { return _lastLoginDateUtc; } - set { _tracker.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, Ps.Value.LastLoginDateUtcSelector); } + set { _tracker.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, Ps.Value.LastLoginDateUtcSelector); } } /// @@ -392,6 +401,7 @@ namespace Umbraco.Core.Models.Identity public readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email); public readonly PropertyInfo UserNameSelector = ExpressionHelper.GetPropertyInfo(x => x.UserName); public readonly PropertyInfo LastLoginDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.LastLoginDateUtc); + public readonly PropertyInfo LastPasswordChangeDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.LastPasswordChangeDateUtc); public readonly PropertyInfo EmailConfirmedSelector = ExpressionHelper.GetPropertyInfo(x => x.EmailConfirmed); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo AccessFailedCountSelector = ExpressionHelper.GetPropertyInfo(x => x.AccessFailedCount); @@ -419,6 +429,7 @@ namespace Umbraco.Core.Models.Identity private int _id; private bool _hasIdentity = false; private DateTime? _lastLoginDateUtc; + private DateTime? _lastPasswordChangeDateUtc; private bool _emailConfirmed; private string _name; private int _accessFailedCount; @@ -449,4 +460,4 @@ namespace Umbraco.Core.Models.Identity } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 954705d544..5ef270c789 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Models.Identity }) .ConstructUsing(user => new BackOfficeIdentityUser(user.Id, user.Groups)) .ForMember(user => user.LastLoginDateUtc, expression => expression.MapFrom(user => user.LastLoginDate.ToUniversalTime())) + .ForMember(user => user.LastPasswordChangeDateUtc, expression => expression.MapFrom(user => user.LastPasswordChangeDate.ToUniversalTime())) .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.EmailConfirmed, expression => expression.MapFrom(user => user.EmailConfirmedDate.HasValue)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) @@ -47,4 +48,4 @@ namespace Umbraco.Core.Models.Identity return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index c867dcf622..81c7c56d13 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -83,6 +83,12 @@ namespace Umbraco.Core.Models.Identity /// public virtual DateTime? LockoutEndDateUtc { get; set; } + /// + /// DateTime in UTC when the password was last changed. + /// + /// + public virtual DateTime? LastPasswordChangeDateUtc { get; set; } + /// /// Is lockout enabled for this user /// @@ -127,4 +133,4 @@ namespace Umbraco.Core.Models.Identity } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 4969fa5f2f..fff97f145c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -457,6 +457,7 @@ namespace Umbraco.Core.Security /// protected override async Task UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword) { + user.LastPasswordChangeDateUtc = DateTime.UtcNow; var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher; if (userAwarePasswordHasher == null) return await base.UpdatePassword(passwordStore, user, newPassword); diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 9db8347040..e0b91ce175 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -622,7 +622,7 @@ namespace Umbraco.Core.Security private bool UpdateMemberProperties(IUser user, BackOfficeIdentityUser identityUser) { var anythingChanged = false; - + //don't assign anything if nothing has changed as this will trigger the track changes of the model if (identityUser.IsPropertyDirty("LastLoginDateUtc") @@ -632,6 +632,13 @@ namespace Umbraco.Core.Security anythingChanged = true; user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); } + if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") + || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) + || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) + { + anythingChanged = true; + user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); + } if (identityUser.IsPropertyDirty("EmailConfirmed") || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) @@ -765,4 +772,4 @@ namespace Umbraco.Core.Security return Task.FromResult(false); } } -} \ No newline at end of file +} From b4926416333ab5c830836230b6059dcf745685ce Mon Sep 17 00:00:00 2001 From: mvanhelmont Date: Sun, 29 Apr 2018 08:48:49 +0200 Subject: [PATCH 05/47] U4-11275 This is a simple fix for U4-11275 so that packages also can set routePath to something like a overview page. Like the new user section when you don't want to display children. Is a copy of /:section/:tree/:method/:id --- src/Umbraco.Web.UI.Client/src/routes.js | 35 +++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 3fc4d3f78e..314b3b0c53 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -31,11 +31,11 @@ app.config(function ($routeProvider) { userService.getCurrentUser({ broadcastEvent: broadcast }).then(function (user) { //is auth, check if we allow or reject if (isRequired) { - + //This checks the current section and will force a redirect to 'content' as the default if ($route.current.params.section.toLowerCase() === "default" || $route.current.params.section.toLowerCase() === "umbraco" || $route.current.params.section === "") { $route.current.params.section = "content"; - } + } // U4-5430, Benjamin Howarth // We need to change the current route params if the user only has access to a single section @@ -141,13 +141,32 @@ app.config(function ($routeProvider) { resolve: canRoute(true) }) .when('/:section/:tree/:method', { - templateUrl: function (rp) { + //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. + template: "
", + //This controller will execute for this route, then we replace the template dynamnically based on the current tree. + controller: function ($scope, $route, $routeParams, treeService) { - //if there is no method registered for this then show the dashboard - if (!rp.method) - return "views/common/dashboard.html"; - - return ('views/' + rp.tree + '/' + rp.method + '.html'); + if (!$routeParams.tree || !$routeParams.method) { + $scope.templateUrl = "views/common/dashboard.html"; + } + + // Here we need to figure out if this route is for a package tree and if so then we need + // to change it's convention view path to: + // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html + + // otherwise if it is a core tree we use the core paths: + // views/{treetype}/{method}.html + + var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); + + if (packageTreeFolder) { + $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + + "/" + packageTreeFolder + + "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); + } + else { + $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); + } }, resolve: canRoute(true) }) From 8be303cb05d6100f5c47d5b128dfd7009132428d Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sun, 17 Jun 2018 14:33:32 +1000 Subject: [PATCH 06/47] Adds key for 'anchor' to en variants. Needs translations. --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 93d8e8b0f6..22596ff9dd 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -527,6 +527,7 @@ Add Alias All + Anchor Are you sure? Back Border 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 15256505a9..23d7f3698b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -526,6 +526,7 @@ Add Alias All + Anchor Are you sure? Back Border From f3375c01d8881181371978812792f4318fe0c16e Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sun, 17 Jun 2018 14:38:37 +1000 Subject: [PATCH 07/47] remove unused dialog service, add logic for managing anchor value in link object, add function for parsing an array of anchors from a string --- .../src/common/services/tinymce.service.js | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 86de5b586d..d862789539 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -6,7 +6,7 @@ * @description * A service containing all logic for all of the Umbraco TinyMCE plugins */ -function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { +function tinyMceService($log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { return { /** @@ -645,6 +645,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + data.anchor = anchorElm ? dom.getAttrib(anchorElm, 'anchor') : ''; if (selectedElm.nodeName === "IMG") { data.text = initialText = " "; @@ -677,8 +678,6 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro }; } - var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector(); - var dialogService = injector.get("dialogService"); var currentTarget = null; //if we already have a link selected, we want to pass that data over to the dialog @@ -690,10 +689,20 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro target: anchor.attr("target") }; + // split the URL to check for an anchor or querystring, then add that value to the currentTarget object + var urlParts = currentTarget.url.split(/(\?|#)/); + if (urlParts.length === 3) { + currentTarget.anchor = urlParts[2]; + } + //locallink detection, we do this here, to avoid poluting the dialogservice //so the dialog service can just expect to get a node-like structure - if (currentTarget.url.indexOf("localLink:") > 0) { - var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.length - 1); + if (currentTarget.url.indexOf("localLink:") > 0) { + // if the current link has an anchor, it needs to be considered when getting the udi/id + // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace + var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, + currentTarget.url.length - (currentTarget.anchor ? currentTarget.anchor.length + 2 : 1)); + //we need to check if this is an INT or a UDI var parsedIntId = parseInt(linkId, 10); if (isNaN(parsedIntId)) { @@ -741,6 +750,20 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro }); }, + + getAnchorNames: function(propertyString) { + var anchorPattern = //gi; + var matches = propertyString.match(anchorPattern); + var anchors = []; + + if (matches) { + anchors = matches.map(function(v) { + return v.substring(v.indexOf('"') + 1, v.lastIndexOf('\\')); + }); + } + + return anchors; + }, insertLinkInEditor: function(editor, target, anchorElm) { @@ -749,13 +772,18 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro var hasUdi = target.udi ? true : false; var id = hasUdi ? target.udi : (target.id ? target.id : null); + // if an anchor exists, check that it is appropriately prefixed + if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { + target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; + } + //Create a json obj used to create the attributes for the tag function createElemAttributes() { var a = { href: href, title: target.name, target: target.target ? target.target : null, - rel: target.rel ? target.rel : null + rel: target.rel ? target.rel : null }; if (hasUdi) { a["data-udi"] = target.udi; @@ -763,6 +791,12 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro else if (target.id) { a["data-id"] = target.id; } + + if (target.anchor) { + a["data-anchor"] = target.anchor; + a.href = a.href + target.anchor; + } + return a; } From 1cbb456dcd61485af8705b5cc78715ce6f2ad965 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sun, 17 Jun 2018 14:39:45 +1000 Subject: [PATCH 08/47] update rte controllers to add anchors array to overlay model - anchors are parsed from the current editor properties --- .../src/views/propertyeditors/grid/editors/rte.controller.js | 4 +++- .../src/views/propertyeditors/rte/rte.controller.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 99c3fd80ff..397438d5a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -1,7 +1,7 @@ (function() { "use strict"; - function GridRichTextEditorController($scope, tinyMceService, macroService) { + function GridRichTextEditorController($scope, tinyMceService, macroService, editorState) { var vm = this; @@ -11,9 +11,11 @@ vm.openEmbed = openEmbed; function openLinkPicker(editor, currentTarget, anchorElement) { + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, + anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 17f0f3b698..c5a739bec2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", - function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService) { + function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService, editorState) { $scope.isLoading = true; @@ -273,11 +273,12 @@ angular.module("umbraco") syncContent(editor); }); - + tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, + anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); From 3fecd55c49f5078212ccf3e18f0c5c3315c2e023 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sun, 17 Jun 2018 14:41:41 +1000 Subject: [PATCH 09/47] adds check for anchors when overlay opens, and fetches anchors when target updates. --- .../common/dialogs/linkpicker.controller.js | 13 +++++++---- .../linkpicker/linkpicker.controller.js | 23 +++++++++++-------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index e76db90f47..ba6835a6b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -39,6 +39,10 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", }); } } + + if (dialogOptions.anchors) { + $scope.anchorValues = dialogOptions.anchors; + } function nodeSelectHandler(ev, args) { args.event.preventDefault(); @@ -69,9 +73,10 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", $scope.target.url = "/"; } else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.target.url = url; - }); + contentResource.getById(args.node.id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; + }); } if (!angular.isUndefined($scope.target.isMedia)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 91c74311b3..5d6b0916f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -1,8 +1,10 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.model; + var anchorPattern = //gi; + var searchText = "Search..."; localizationService.localize("general_search").then(function (value) { searchText = value + "..."; @@ -22,11 +24,10 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", selectedSearchResults: [] }; - $scope.showTarget = $scope.model.hideTarget !== true; + $scope.showTarget = $scope.model.hideTarget !== true; if (dialogOptions.currentTarget) { $scope.model.target = dialogOptions.currentTarget; - //if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { @@ -47,9 +48,12 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", }); } } - - function nodeSelectHandler(ev, args) { - + + if (dialogOptions.anchors) { + $scope.anchorValues = dialogOptions.anchors; + } + + function nodeSelectHandler(ev, args) { if(args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); @@ -71,9 +75,10 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; } - else { - contentResource.getNiceUrl(args.node.id).then(function (url) { - $scope.model.target.url = url; + else { + contentResource.getById(args.node.id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; }); } From 9cfc997ba209780ac3e36f5b104cf33f8ff80ac2 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sun, 17 Jun 2018 14:42:16 +1000 Subject: [PATCH 10/47] update overlay/dialog to add input for anchor value - value can be a string or selection from the datalist. Is the dialog in use anymore? --- .../src/views/common/dialogs/linkpicker.html | 14 +++++++++++++- .../common/overlays/linkpicker/linkpicker.html | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.html index 442357edcc..0cc1c52595 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.html @@ -2,13 +2,25 @@