diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 2dcbe06458..becf68bd24 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { internal class ContentElement : UmbracoConfigurationElement, IContentSection { - private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; + private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"]; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 75ed8238d0..75e7b1dc3d 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -33,6 +33,8 @@ namespace Umbraco.Core.Models.Identity private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; + private DateTime? _lastPasswordChangeDateUtc; + /// /// Used to construct a new instance without an identity @@ -136,6 +138,15 @@ namespace Umbraco.Core.Models.Identity set => _beingDirty.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 /// @@ -419,6 +430,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); @@ -439,5 +451,6 @@ namespace Umbraco.Core.Models.Identity groups => groups.GetHashCode()); } + } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index 69b9e26385..b8ed355c55 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -75,6 +75,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 /// diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 8324ec11a0..c4406054e6 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -434,6 +434,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); @@ -484,15 +485,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; } @@ -517,13 +525,36 @@ namespace Umbraco.Core.Security - - 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.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 2f70f32d89..63970aca8f 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -637,6 +637,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)) diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index a61faa10c7..17115c50b1 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -358,5 +358,13 @@ namespace Umbraco.Core.Services /// The name of the snippet /// The content of the partial view. string GetPartialViewSnippetContent(string snippetName); + + /// + /// Gets the content of a view. + /// + /// The name of the view. + /// + string GetViewContent(string filename); + } } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index fe32a43cc9..0c52caf70f 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -144,7 +144,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void PreviewBadge() { - Assert.IsTrue(SettingsSection.Content.PreviewBadge == @"In Preview Mode - click to end"); + Assert.IsTrue(SettingsSection.Content.PreviewBadge == @"In Preview Mode - click to end"); } [Test] public void ResolveUrlsFromTextString() diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 5373afd1b5..08f1c94e21 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -75,7 +75,7 @@ true - In Preview Mode - click to end]]> + In Preview Mode - click to end]]> "; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', - { - 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId - }, - macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - - editor.selection.setNode(macroDiv); - - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - - //async load the macro content - this.loadMacroContent($macroDiv, macroObject, $scope); - - }, - - /** loads in the macro content async from the server */ - loadMacroContent: function($macroDiv, macroData, $scope) { - - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; - } - - var $ins = $macroDiv.find("ins"); - - //show the throbber - $macroDiv.addClass("loading"); - - var contentId = $routeParams.id; - - //need to wrap in safe apply since this might be occuring outside of angular - angularHelper.safeApply($scope, function() { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - $ins.html(htmlResult); - } - }); - }); - - }, - - createLinkPicker: function(editor, $scope, onClick) { - - function createLinkList(callback) { - return function() { - var linkList = editor.settings.link_list; - - if (typeof(linkList) === "string") { - tinymce.util.XHR.send({ - url: linkList, - success: function(text) { - callback(tinymce.util.JSON.parse(text)); - } - }); - } else { - callback(linkList); - } - }; - } - - function showDialog(linkList) { - var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText; - var win, linkListCtrl, relListCtrl, targetListCtrl; - - function linkListChangeHandler(e) { - var textCtrl = win.find('#text'); - - if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { - textCtrl.value(e.control.text()); - } - - win.find('#href').value(e.control.value()); - } - - function buildLinkList() { - var linkListItems = [{ - text: 'None', - value: '' +function tinyMceService($log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { + return { + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#configuration + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "rteApiBaseUrl", + "GetConfiguration"), { + cache: true + }), + 'Failed to retrieve tinymce configuration'); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#defaultPrevalues + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a default configration to fallback on in case none is provided + * + */ + defaultPrevalues: function () { + var cfg = {}; + cfg.toolbar = ["code", "bold", "italic", "styleselect", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"]; + cfg.stylesheets = []; + cfg.dimensions = { + height: 500 + }; + cfg.maxImageSize = 500; + return cfg; + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert embedded media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertEmbeddedMedia: function (editor, scope, callback) { + editor.addButton('umbembeddialog', { + icon: 'custom icon-tv', + tooltip: 'Embed', + onclick: function () { + if (callback) { + callback(); + } + } + }); + }, + + insertEmbeddedMediaInEditor: function (editor, preview) { + editor.insertContent(preview); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createMediaPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createMediaPicker: function (editor, scope, callback) { + editor.addButton('umbmediapicker', { + icon: 'custom icon-picture', + tooltip: 'Media Picker', + stateSelector: 'img', + onclick: function () { + + var selectedElm = editor.selection.getNode(), + currentTarget; + + + if (selectedElm.nodeName === 'IMG') { + var img = $(selectedElm); + + var hasUdi = img.attr("data-udi") ? true : false; + + currentTarget = { + altText: img.attr("alt"), + url: img.attr("src") + }; + + if (hasUdi) { + currentTarget["udi"] = img.attr("data-udi"); + } else { + currentTarget["id"] = img.attr("rel"); + } + } + + userService.getCurrentUser().then(function (userData) { + if (callback) { + callback(currentTarget, userData); + } + }); + + } + }); + }, + + insertMediaInEditor: function (editor, img) { + if (img) { + + var hasUdi = img.udi ? true : false; + + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + id: '__mcenew' + }; + + if (hasUdi) { + data["data-udi"] = img.udi; + } else { + //Considering these fixed because UDI will now be used and thus + // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 + data["rel"] = img.id; + data["data-id"] = img.id; + } + + editor.insertContent(editor.dom.createHTML('img', data)); + + $timeout(function () { + var imgElm = editor.dom.get('__mcenew'); + var size = editor.dom.getSize(imgElm); + + if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { + var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); + + var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; + editor.dom.setAttrib(imgElm, 'style', s); + editor.dom.setAttrib(imgElm, 'id', null); + + if (img.url) { + var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; + editor.dom.setAttrib(imgElm, 'data-mce-src', src); + } + } + }, 500); + } + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createUmbracoMacro + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the insert umbrco macro tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertMacro: function (editor, $scope, callback) { + + var createInsertMacroScope = this; + + /** Adds custom rules for the macro plugin and custom serialization */ + editor.on('preInit', function (args) { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out + editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { + nodes[i].parent.unwrap(); + } + } + }); + + }); + + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest(".umb-macro-holder"); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === "P") { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + + /** Adds the button instance */ + editor.addButton('umbmacro', { + icon: 'custom icon-settings-alt', + tooltip: 'Insert macro', + onPostRender: function () { + + var ctrl = this; + var isOnMacroElement = false; + + /** + if the selection comes from a different element that is not the macro's + we need to check if the selection includes part of the macro, if so we'll force the selection + to clear to the next element since if people can select part of the macro markup they can then modify it. + */ + function handleSelectionChange() { + + if (!editor.selection.isCollapsed()) { + var endSelection = tinymce.activeEditor.selection.getEnd(); + var startSelection = tinymce.activeEditor.selection.getStart(); + //don't proceed if it's an entire element selected + if (endSelection !== startSelection) { + + //if the end selection is a macro then move the cursor + //NOTE: we don't have to handle when the selection comes from a previous parent because + // that is automatically taken care of with the normal onNodeChanged logic since the + // evt.element will be the macro once it becomes part of the selection. + var $testForMacro = $(endSelection).closest(".umb-macro-holder"); + if ($testForMacro.length > 0) { + + //it came from before so move after, if there is no after then select ourselves + var next = $testForMacro.next(); + if (next.length > 0) { + editor.selection.setCursorLocation($testForMacro.next().get(0)); + } else { + selectMacroElement($testForMacro.get(0)); + } + + } + } + } + } + + /** helper method to select the macro element */ + function selectMacroElement(macroElement) { + + // move selection to top element to ensure we can't edit this + editor.selection.select(macroElement); + + // check if the current selection *is* the element (ie bug) + var currentSelection = editor.selection.getStart(); + if (tinymce.isIE) { + if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { + while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { + currentSelection = currentSelection.parentNode; + } + editor.selection.select(currentSelection); + } + } + } + + /** + * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. + * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves + * from the event listener before changing selection, however, it seems that putting a break point in this method + * will always cause an 'infinite' loop as the caret keeps changing. + */ + function onNodeChanged(evt) { + + //set our macro button active when on a node of class umb-macro-holder + var $macroElement = $(evt.element).closest(".umb-macro-holder"); + + handleSelectionChange(); + + //set the button active + ctrl.active($macroElement.length !== 0); + + if ($macroElement.length > 0) { + var macroElement = $macroElement.get(0); + + //remove the event listener before re-selecting + editor.off('NodeChange', onNodeChanged); + + selectMacroElement(macroElement); + + //set the flag + isOnMacroElement = true; + + //re-add the event listener + editor.on('NodeChange', onNodeChanged); + } else { + isOnMacroElement = false; + } + + } + + /** when the contents load we need to find any macros declared and load in their content */ + editor.on("LoadContent", function (o) { + + //get all macro divs and load their content + $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { + createInsertMacroScope.loadMacroContent($(this), null, $scope); + }); + + }); + + /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ + editor.on('BeforeExecCommand', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + /** This double checks and ensures you can't paste content into the rendered macro */ + editor.on("Paste", function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + + /** + * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so + * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request + * so the macro cannot be edited. + */ + editor.on('KeyDown', function (e) { + if (isOnMacroElement) { + var macroElement = editor.selection.getNode(); + + //get the 'real' element (either p or the real one) + macroElement = getRealMacroElem(macroElement); + + //prevent editing + e.preventDefault(); + e.stopPropagation(); + + var moveSibling = function (element, isNext) { + var $e = $(element); + var $sibling = isNext ? $e.next() : $e.prev(); + if ($sibling.length > 0) { + editor.selection.select($sibling.get(0)); + editor.selection.collapse(true); + } else { + //if we're moving previous and there is no sibling, then lets recurse and just select the next one + if (!isNext) { + moveSibling(element, true); + return; + } + + //if there is no sibling we'll generate a new p at the end and select it + editor.setContent(editor.getContent() + "

 

"); + editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); + editor.selection.collapse(true); + + } + }; + + //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) + //supported keys to remove the macro (8-backspace, 46-delete) + //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? + if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { + //move to next element + moveSibling(macroElement, true); + } else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { + //move to prev element + moveSibling(macroElement, false); + } else if ($.inArray(e.keyCode, [8, 46]) !== -1) { + //delete macro element + + //move first, then delete + moveSibling(macroElement, false); + editor.dom.remove(macroElement); + } + return; + } + }); + + }, + + /** The insert macro button click event handler */ + onclick: function () { + + var dialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true + }; + + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + var macroElement = editor.selection.getNode(); + macroElement = getRealMacroElem(macroElement); + if (macroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + var contents = $(macroElement).contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + dialogData = { + macroData: parsed + }; + } + + if (callback) { + callback(dialogData); + } + + } + }); + }, + + insertMacroInEditor: function (editor, macroObject, $scope) { + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ""; + //create an id class for this element so we can re-select it after inserting + var uniqueId = "umb-macro-" + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', { + 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId + }, + macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); + + editor.selection.setNode(macroDiv); + + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, $scope); + + }, + + /** loads in the macro content async from the server */ + loadMacroContent: function ($macroDiv, macroData, $scope) { + + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + + var $ins = $macroDiv.find("ins"); + + //show the throbber + $macroDiv.addClass("loading"); + + var contentId = $routeParams.id; + + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($scope, function () { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + $ins.html(htmlResult); + } + }); + }); + + }, + + createLinkPicker: function (editor, $scope, onClick) { + + function createLinkList(callback) { + return function () { + var linkList = editor.settings.link_list; + + if (typeof (linkList) === "string") { + tinymce.util.XHR.send({ + url: linkList, + success: function (text) { + callback(tinymce.util.JSON.parse(text)); + } + }); + } else { + callback(linkList); + } + }; + } + + function showDialog(linkList) { + var data = {}, + selection = editor.selection, + dom = editor.dom, + selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + + if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { + textCtrl.value(e.control.text()); + } + + win.find('#href').value(e.control.value()); + } + + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' }]; - tinymce.each(linkList, function(link) { - linkListItems.push({ - text: link.text || link.title, - value: link.value || link.url, - menu: link.menu - }); - }); + tinymce.each(linkList, function (link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); - return linkListItems; - } + return linkListItems; + } - function buildRelList(relValue) { - var relListItems = [{ - text: 'None', - value: '' + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' }]; - tinymce.each(editor.settings.rel_list, function(rel) { - relListItems.push({ - text: rel.text || rel.title, - value: rel.value, - selected: relValue === rel.value - }); - }); + tinymce.each(editor.settings.rel_list, function (rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); - return relListItems; - } + return relListItems; + } - function buildTargetList(targetValue) { - var targetListItems = [{ - text: 'None', - value: '' + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' }]; - if (!editor.settings.target_list) { - targetListItems.push({ - text: 'New window', - value: '_blank' - }); - } + if (!editor.settings.target_list) { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } - tinymce.each(editor.settings.target_list, function(target) { - targetListItems.push({ - text: target.text || target.title, - value: target.value, - selected: targetValue === target.value - }); - }); + tinymce.each(editor.settings.target_list, function (target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); - return targetListItems; - } + return targetListItems; + } - function buildAnchorListControl(url) { - var anchorList = []; + function buildAnchorListControl(url) { + var anchorList = []; - tinymce.each(editor.dom.select('a:not([href])'), function(anchor) { - var id = anchor.name || anchor.id; + tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { + var id = anchor.name || anchor.id; - if (id) { - anchorList.push({ - text: id, - value: '#' + id, - selected: url.indexOf('#' + id) !== -1 - }); - } - }); + if (id) { + anchorList.push({ + text: id, + value: '#' + id, + selected: url.indexOf('#' + id) !== -1 + }); + } + }); - if (anchorList.length) { - anchorList.unshift({ - text: 'None', - value: '' - }); + if (anchorList.length) { + anchorList.unshift({ + text: 'None', + value: '' + }); - return { - name: 'anchor', - type: 'listbox', - label: 'Anchors', - values: anchorList, - onselect: linkListChangeHandler - }; - } - } + return { + name: 'anchor', + type: 'listbox', + label: 'Anchors', + values: anchorList, + onselect: linkListChangeHandler + }; + } + } - function updateText() { - if (!initialText && data.text.length === 0) { - this.parent().parent().find('#text')[0].value(this.value()); - } - } + function updateText() { + if (!initialText && data.text.length === 0) { + this.parent().parent().find('#text')[0].value(this.value()); + } + } - selectedElm = selection.getNode(); - anchorElm = dom.getParent(selectedElm, 'a[href]'); + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); - data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'}); - data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; - data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; - data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ + format: 'text' + }); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; - if (selectedElm.nodeName === "IMG") { - data.text = initialText = " "; - } + if (selectedElm.nodeName === "IMG") { + data.text = initialText = " "; + } - if (linkList) { - linkListCtrl = { - type: 'listbox', - label: 'Link list', - values: buildLinkList(), - onselect: linkListChangeHandler - }; - } + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } - if (editor.settings.target_list !== false) { - targetListCtrl = { - name: 'target', - type: 'listbox', - label: 'Target', - values: buildTargetList(data.target) - }; - } + if (editor.settings.target_list !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } - if (editor.settings.rel_list) { - relListCtrl = { - name: 'rel', - type: 'listbox', - label: 'Rel', - values: buildRelList(data.rel) - }; - } + if (editor.settings.rel_list) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } - var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector(); - var dialogService = injector.get("dialogService"); - var currentTarget = null; + var currentTarget = null; - //if we already have a link selected, we want to pass that data over to the dialog - if(anchorElm){ - var anchor = $(anchorElm); - currentTarget = { - name: anchor.attr("title"), - url: anchor.attr("href"), - target: anchor.attr("target") - }; + //if we already have a link selected, we want to pass that data over to the dialog + if (anchorElm) { + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr("title"), + url: anchor.attr("href"), + target: anchor.attr("target") + }; - //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); - //we need to check if this is an INT or a UDI - var parsedIntId = parseInt(linkId, 10); - if (isNaN(parsedIntId)) { - //it's a UDI - currentTarget.udi = linkId; - } - else { - currentTarget.id = linkId; - } - } - } + // drop the lead char from the anchor text, if it has a value + var anchorVal = anchor[0].dataset.anchor; + if (anchorVal) { + currentTarget.anchor = anchorVal.substring(1); + } - if(onClick) { - onClick(currentTarget, anchorElm); - } + //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) { + // 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.lastIndexOf("}")); - } + //we need to check if this is an INT or a UDI + var parsedIntId = parseInt(linkId, 10); + if (isNaN(parsedIntId)) { + //it's a UDI + currentTarget.udi = linkId; + } else { + currentTarget.id = linkId; + } + } + } - editor.addButton('link', { - icon: 'link', - tooltip: 'Insert/edit link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]' - }); + if (onClick) { + onClick(currentTarget, anchorElm); + } - editor.addButton('unlink', { - icon: 'unlink', - tooltip: 'Remove link', - cmd: 'unlink', - stateSelector: 'a[href]' - }); + } - editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); - this.showDialog = showDialog; + editor.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]' + }); - editor.addMenuItem('link', { - icon: 'link', - text: 'Insert link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]', - context: 'insert', - prependToContext: true - }); + editor.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + cmd: 'unlink', + stateSelector: 'a[href]' + }); - }, + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; - insertLinkInEditor: function(editor, target, anchorElm) { + editor.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); - var href = target.url; - // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null - var hasUdi = target.udi ? true : false; - var id = hasUdi ? target.udi : (target.id ? target.id : null); + }, - //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 - }; - if (hasUdi) { - a["data-udi"] = target.udi; - } - else if (target.id) { - a["data-id"] = target.id; - } - return a; - } + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#getAnchorNames + * @methodOf umbraco.services.tinyMceService + * + * @description + * From the given string, generates a string array where each item is the id attribute value from a named anchor + * 'some string with a named anchor' returns ['anchor'] + * + * @param {string} input the string to parse + */ + getAnchorNames: function (input) { + var anchorPattern = //gi; + var matches = input.match(anchorPattern); + var anchors = []; - function insertLink() { - if (anchorElm) { - editor.dom.setAttribs(anchorElm, createElemAttributes()); + if (matches) { + anchors = matches.map(function (v) { + return v.substring(v.indexOf('"') + 1, v.lastIndexOf('\\')); + }); + } - editor.selection.select(anchorElm); - editor.execCommand('mceEndTyping'); - } - else { - editor.execCommand('mceInsertLink', false, createElemAttributes()); - } - } + return anchors; + }, - if (!href) { - editor.execCommand('unlink'); - return; - } + insertLinkInEditor: function (editor, target, anchorElm) { - //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set - if(id && (angular.isUndefined(target.isMedia) || !target.isMedia)){ - - href = "/{localLink:" + id + "}"; + var href = target.url; + // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null + var hasUdi = target.udi ? true : false; + var id = hasUdi ? target.udi : (target.id ? target.id : null); - insertLink(); - return; - } + // 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; + } + + // the href might be an external url, so check the value for an anchor/qs + // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (!target.anchor) { + var urlParts = href.split(/(#|\?)/); + if (urlParts.length === 3) { + href = urlParts[0]; + target.anchor = urlParts[1] + urlParts[2]; + } + } + + //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 + }; - // Is email and not //user@domain.com - if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { - href = 'mailto:' + href; - insertLink(); - return; - } + if (hasUdi) { + a["data-udi"] = target.udi; + } else if (target.id) { + a["data-id"] = target.id; + } - // Is www. prefixed - if (/^\s*www\./i.test(href)) { - href = 'http://' + href; - insertLink(); - return; - } + if (target.anchor) { + a["data-anchor"] = target.anchor; + a.href = a.href + target.anchor; + } else { + a["data-anchor"] = null; + } - insertLink(); + return a; + } - } + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, createElemAttributes()); - }; + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + editor.execCommand('mceInsertLink', false, createElemAttributes()); + } + } + + if (!href) { + editor.execCommand('unlink'); + return; + } + + //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set + if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) { + + href = "/{localLink:" + id + "}"; + + insertLink(); + return; + } + + // Is email and not //user@domain.com + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { + href = 'mailto:' + href; + insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + + insertLink(); + + } + + }; } angular.module('umbraco.services').factory('tinyMceService', tinyMceService); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index ffd363fe31..2eb2735279 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -531,6 +531,13 @@ input.umb-group-builder__group-title-input { overflow: hidden; } + .editor-validation-pattern{ + border: 1px solid @gray-7; + margin: 10px 0 0; + padding: 6px; + max-height: 32px; + } + .umb-dropdown { width: 100%; } 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 47fabb7573..67b684ab7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -3,10 +3,24 @@ // -------------------------------------------------- .umb-property-editor { min-width:66.6%; + + &-pull { + float:left; + width:66.6%; + } + + &-push { + float:right; + } } .umb-property-editor-tiny { width: 60px; + + &.umb-editor-push { + width:30%; + min-width:0; + } } .umb-property-editor-small { @@ -28,6 +42,27 @@ width: 99%; } +// displays property inline with preceeding +.umb-property { + &--pull { + float:left; + width:60%; + } + + &--push { + float:right; + width:35%; + } + + &--pull, &--push { + .umb-editor { + min-width:0; + width:100%; + } + } +} + + // // Content picker // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index d41427c203..8b5ca87470 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -147,13 +147,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.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'); + } }, reloadOnSearch: false, resolve: canRoute(true) 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 9bbf38568f..072f1b9b5e 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,58 +1,66 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { - var dialogOptions = $scope.model; + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { + var dialogOptions = $scope.model; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); + var anchorPattern = /
/gi; - if(!$scope.model.title) { - localizationService.localize("defaultdialogs_selectLink").then(function(value){ - $scope.model.title = value; - }); + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); } - $scope.dialogTreeApi = {}; - $scope.model.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; + $scope.dialogTreeApi = {}; + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; - $scope.showTarget = $scope.model.hideTarget !== true; + $scope.showTarget = $scope.model.hideTarget !== true; - if (dialogOptions.currentTarget) { - $scope.model.target = dialogOptions.currentTarget; + 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) { - //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) { + //will be either a udi or an int + var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; - //will be either a udi or an int - var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; + if (!$scope.model.target.path) { - if (!$scope.model.target.path) { - - entityResource.getPath(id, "Document").then(function (path) { - $scope.model.target.path = path; - //now sync the tree to this path - $scope.dialogTreeApi.syncTree({ path: $scope.model.target.path, tree: "content" }); - }); - } + entityResource.getPath(id, "Document").then(function (path) { + $scope.model.target.path = path; + //now sync the tree to this path + $scope.dialogTreeApi.syncTree({ path: $scope.model.target.path, tree: "content" }); + path: $scope.model.target.path, + tree: "content" + }); + }); + } - contentResource.getNiceUrl(id).then(function (url) { - $scope.model.target.url = url; - }); - } - } + // if a link exists, get the properties to build the anchor name list + contentResource.getById(id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; + }); + } else if ($scope.model.target.url.length) { + // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs + $scope.model.target.url = $scope.model.target.url.substring(0, $scope.model.target.url.search(/(#|\?)/)); + } + } else if (dialogOptions.anchors) { + $scope.anchorValues = dialogOptions.anchors; + } - function nodeSelectHandler(args) { - - if(args && args.event) { + function nodeSelectHandler(args) { + if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } @@ -72,76 +80,83 @@ 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]; }); } if (!angular.isUndefined($scope.model.target.isMedia)) { delete $scope.model.target.isMedia; } - } + } - function nodeExpandedHandler(args) { + function nodeExpandedHandler(args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } - } + } - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, - show: true, - submit: function(model) { - var media = model.selectedImages[0]; + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + startNodeIsVirtual: userData.startMediaIds.length !== 1, + show: true, + submit: function (model) { + var media = model.selectedImages[0]; - $scope.model.target.id = media.id; - $scope.model.target.udi = media.udi; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + $scope.model.target.id = media.id; + $scope.model.target.udi = media.udi; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); + + debugger; - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; - } - }; - }); - }; + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }); + }; - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, {event: evt, node: result}); - }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; - $scope.onTreeInit = function () { - $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); - $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); - } + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + } // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); + nodeSelectHandler({}, { + node: node + }); }; $scope.closeMiniListView = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html index 7772479149..e4ab635a44 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -1,26 +1,41 @@
- + + localize="placeholder" + placeholder="@general_url" + class="umb-property-editor umb-textstring" + ng-model="model.target.url" + ng-disabled="model.target.id" /> + + + + + + + + + localize="placeholder" + placeholder="@placeholders_entername" + class="umb-property-editor umb-textstring" + ng-model="model.target.name" /> @@ -30,43 +45,39 @@
- + -
+
- +
- +
- +
@@ -80,11 +91,10 @@
- + 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 c48099c5fe..ca65e3d107 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 @@ -20,9 +20,10 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", } $scope.treeModel = { hideHeader: false - } + } + $scope.toggle = toggleHandler; userService.getCurrentUser().then(function (userData) { - $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); var node = dialogOptions.currentNode; @@ -57,7 +58,27 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", if (args.node.metaData.isContainer) { openMiniListView(args.node); } - } + } + + function toggleHandler(type){ + // If the relateToOriginal toggle is clicked + if(type === "relate"){ + if($scope.relateToOriginal){ + $scope.relateToOriginal = false; + return; + } + $scope.relateToOriginal = true; + } + + // If the recurvise toggle is clicked + if(type === "recursive"){ + if($scope.recursive){ + $scope.recursive = false; + return; + } + $scope.recursive = true; + } + } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; 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 7501508a48..018c543d08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -69,13 +69,13 @@ - + - - + + 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 0543fc0c66..78ce1f29a5 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 @@ -88,7 +88,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper // sortable options $scope.sortableOptions = { + axis: "y", + containment: "parent", distance: 10, + opacity: 0.7, tolerance: "pointer", scroll: true, zIndex: 6000 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 54ec3031cb..a9d1cd1234 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); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index e92dd171c6..35b5bef306 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -5,15 +5,12 @@ angular.module("umbraco") var $typeahead; $scope.isLoading = true; - $scope.tagToAdd = ""; - - assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js", $scope).then(function () { - - $scope.isLoading = false; - - //load current value - - if ($scope.model.value) { + $scope.tagToAdd = ""; + + function setModelValue(val) { + + $scope.model.value = val || $scope.model.value; + if ($scope.model.value) { if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { //it is csv if (!$scope.model.value) { @@ -21,7 +18,14 @@ angular.module("umbraco") } else { if($scope.model.value.length > 0) { - $scope.model.value = $scope.model.value.split(","); + // split the csv string, and remove any duplicate values + var tempArray = $scope.model.value.split(',').map(function(v) { + return v.trim(); + }); + + $scope.model.value = tempArray.filter(function(v, i, self) { + return self.indexOf(v) === i; + }); } } } @@ -29,6 +33,14 @@ angular.module("umbraco") else { $scope.model.value = []; } + } + + assetsService.loadJs("lib/typeahead.js/typeahead.bundle.min.js", $scope).then(function () { + + $scope.isLoading = false; + + //load current value + setModelValue(); // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) $scope.validateMandatory = function () { @@ -85,17 +97,7 @@ angular.module("umbraco") //vice versa $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server - $scope.model.value = newVal; - - if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { - //it is csv - if (!$scope.model.value) { - $scope.model.value = []; - } - else { - $scope.model.value = $scope.model.value.split(","); - } - } + setModelValue(newVal); }; //configure the tags data source diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index 313cf99a49..de232c690c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 0f95f78adc..39ae1dd583 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -67,7 +67,7 @@ }); - vm.save = function () { + vm.save = function (suppressNotification) { vm.page.saveButtonState = "busy"; vm.template.content = vm.editor.getValue(); @@ -83,11 +83,13 @@ rebindCallback: function (orignal, saved) {} }).then(function (saved) { + if (!suppressNotification) { localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function(data){ var header = data[0]; var message = data[1]; notificationsService.success(header, message); }); + } vm.page.saveButtonState = "success"; @@ -173,6 +175,21 @@ vm.page.loading = false; vm.template = template; + // if this is a new template, bind to the blur event on the name + if ($routeParams.create) { + $timeout(function() { + var nameField = angular.element(document.querySelector('[data-element="editor-name-field"]')); + if (nameField) { + nameField.bind('blur', function(event) { + if (event.target.value) { + vm.save(true); + } + }); + } + }); + } + + //sync state editorState.set(vm.template); navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { 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 48ccef0e51..a3f87ffc93 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 @@ -329,8 +329,10 @@ vm.unlockUserButtonState = "busy"; usersResource.unlockUsers([vm.user.id]).then(function (data) { vm.user.userState = 0; + vm.user.failedPasswordAttempts = 0; setUserDisplayState(); vm.unlockUserButtonState = "success"; + }, function (error) { vm.unlockUserButtonState = "error"; }); diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index cf71dc281e..46a27a4eba 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -39,14 +39,14 @@

Easy start with Umbraco.tv

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- Umbraco.tv → + Umbraco.tv →

Be a part of the community

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

- our.Umbraco → + our.Umbraco →
diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index c092aaae41..b79a221fe0 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -40,7 +40,7 @@ - In Preview Mode - click to end]]> + In Preview Mode - click to end]]> - In Preview Mode - click to end + In Preview Mode - click to end ]]> diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index b8dcb9e8fa..38fbd0b865 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -61,7 +61,9 @@ namespace Umbraco.Web.Editors public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)))); + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); } } @@ -279,6 +281,43 @@ namespace Umbraco.Web.Editors { HandleContentNotFound(id); return null;//irrelevant since the above throws + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + } var content = MapToDisplay(foundContent, culture); diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index a756c527d3..77ff974aaa 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -21,11 +21,12 @@ namespace Umbraco.Web.Editors /// An API controller used for dealing with member types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + [UmbracoTreeAuthorize(new string[] { Constants.Trees.MemberTypes, Constants.Trees.Members})] public class MemberTypeController : ContentTypeControllerBase { private readonly MembershipProvider _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetById(int id) { var ct = Services.MemberTypeService.Get(id); @@ -45,6 +46,7 @@ namespace Umbraco.Web.Editors /// [HttpDelete] [HttpPost] + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public HttpResponseMessage DeleteById(int id) { var foundType = Services.MemberTypeService.Get(id); @@ -71,6 +73,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) @@ -84,6 +88,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(result); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetEmpty() { var ct = new MemberType(-1); @@ -107,6 +112,7 @@ namespace Umbraco.Web.Editors return Enumerable.Empty(); } + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { //get the persisted member type diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index c4e87d0648..0852317b31 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; +using AutoMapper; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; -using AutoMapper; using Umbraco.Core.IO; using Umbraco.Core.Models; -using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -178,6 +176,14 @@ namespace Umbraco.Web.Editors else { //create + + // file might already be on disk, if so grab the content to avoid overwriting + string content = Services.FileService.GetViewContent(display.Alias); + if (string.IsNullOrEmpty(content) == false) + { + display.Content = content; + } + ITemplate master = null; if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 5e1708f118..d3ff363156 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -385,7 +385,7 @@ namespace Umbraco.Web public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) { - return content.Ancestors().Any(x => x.Id == other.Id); + return other.Level < content.Level && content.Path.InvariantStartsWith(other.Path); } public static HtmlString IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -400,7 +400,7 @@ namespace Umbraco.Web public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) { - return content.AncestorsOrSelf().Any(x => x.Id == other.Id); + return content.Path.InvariantStartsWith(other.Path); } public static HtmlString IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -415,8 +415,8 @@ namespace Umbraco.Web public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) { - // avoid using Descendants(), that's expensive - return other.Ancestors().Any(x => x.Id == content.Id); + // avoid using Descendants(), or Ancestors(), they're expensive + return content.Level < other.Level && other.Path.InvariantStartsWith(content.Path); } public static HtmlString IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue) @@ -431,8 +431,8 @@ namespace Umbraco.Web public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) { - // avoid using DescendantsOrSelf(), that's expensive - return other.AncestorsOrSelf().Any(x => x.Id == content.Id); + // avoid using DescendantsOrSelf() or AncestorsOrSelf(), they're expensive + return other.Path.InvariantStartsWith(content.Path); } public static HtmlString IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index b2bf3f358e..bf51d8b6af 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -587,7 +587,6 @@ namespace Umbraco.Web return ContentQuery.ContentAtRoot(); } - /// Had to change to internal for testing. internal static bool ConvertIdObjectToInt(object id, out int intId) { switch (id) @@ -605,7 +604,6 @@ namespace Umbraco.Web } } - /// Had to change to internal for testing. internal static bool ConvertIdObjectToGuid(object id, out Guid guidId) { switch (id) diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index bf922da15b..544af44a60 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -68,7 +68,23 @@ namespace Umbraco.Web.WebApi.Filters if (parts.Length == 1) { - nodeId = (int)actionContext.ActionArguments[parts[0]]; + var argument = actionContext.ActionArguments[parts[0]].ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (Udi.TryParse(argument, true, out Udi udi)) + { + nodeId = ApplicationContext.Current.Services.EntityService.GetIdForUdi(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + nodeId = ApplicationContext.Current.Services.EntityService.GetIdForKey(key, UmbracoObjectTypes.Document).Result; + } } else {