diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index d1c2d23c4f..0a9d750964 100644 --- a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,8 +1,8 @@ namespace Umbraco.Core.PropertyEditors { - internal class DropDownFlexibleConfiguration : ValueListConfiguration + public class DropDownFlexibleConfiguration : ValueListConfiguration { [ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] public bool Multiple { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 784d04864e..48e577a8f0 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -315,8 +316,16 @@ namespace Umbraco.Core.Services /// /// Empties the recycle bin. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] OperationResult EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// /// Sorts documents. /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 78da440bc6..3fecb20035 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -162,8 +162,16 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] OperationResult EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a4def1d209..0d35915e72 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using Umbraco.Core.Events; @@ -1939,7 +1940,14 @@ namespace Umbraco.Core.Services.Implement /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public OperationResult EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public OperationResult EmptyRecycleBin() => EmptyRecycleBin(Constants.Security.SuperUserId); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId) { var nodeObjectType = Constants.ObjectTypes.Document; var deleted = new List(); @@ -1974,7 +1982,7 @@ namespace Umbraco.Core.Services.Implement recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, 0, Constants.System.RecycleBinContent, "Recycle bin emptied"); + Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied"); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index fa72896239..343fb7e2c6 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; @@ -1024,7 +1025,15 @@ namespace Umbraco.Core.Services.Implement /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public OperationResult EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public OperationResult EmptyRecycleBin() => EmptyRecycleBin(Constants.Security.SuperUserId); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId) { var nodeObjectType = Constants.ObjectTypes.Media; var deleted = new List(); @@ -1063,7 +1072,7 @@ namespace Umbraco.Core.Services.Implement args.CanCancel = false; scope.Events.Dispatch(EmptiedRecycleBin, this, args); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, 0, Constants.System.RecycleBinMedia, "Empty Media recycle bin"); + Audit(AuditType.Delete, userId, Constants.System.RecycleBinMedia, "Empty Media recycle bin"); scope.Complete(); } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index a4e08178ed..8b6ac372ec 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -379,6 +379,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture)); diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index 8cdab6b2ae..7ebb026a21 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -97,6 +97,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetUmbracoMedia(contentId) != null; diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 23aa93bb8d..fbb2a7549b 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -92,6 +92,9 @@ namespace Umbraco.Tests.PublishedContent throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return _content.ContainsKey(contentId); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtogglegroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtogglegroup.directive.js new file mode 100644 index 0000000000..709a3aaae4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtogglegroup.directive.js @@ -0,0 +1,105 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbToggleGroup +@restrict E +@scope + +@description +Use this directive to render a group of toggle buttons. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.toggle = toggle;
+
+            function toggle(item) {
+                if(item.checked) {
+                    // do something if item is checked
+                }
+                else {
+                    // do something else if item is unchecked
+                }
+            }
+
+            function init() {
+                vm.items = [{
+                    name: "Item 1",
+                    description: "Item 1 description",
+                    checked: false,
+                    disabled: false
+                }, {
+                    name: "Item 2",
+                    description: "Item 2 description",
+                    checked: true,
+                    disabled: true
+                }];
+            }
+
+            init();
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +@param {Array} items The items to list in the toggle group +@param {callback} onClick The function which should be called when the toggle is clicked for one of the items. + +**/ + +(function () { + 'use strict'; + + function ToggleGroupDirective() { + + function link(scope, el, attr, ctrl) { + + scope.change = function(item) { + if (item.disabled) { + return; + } + + item.checked = !item.checked; + if(scope.onClick) { + scope.onClick({'item': item}); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-toggle-group.html', + scope: { + items: "=", + onClick: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbToggleGroup', ToggleGroupDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 4ba4cf96bb..b81e62a66b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -12,6 +12,7 @@ function treeSearchBox(localizationService, searchService, $q) { searchFromName: "@", showSearch: "@", section: "@", + ignoreUserStartNodes: "@", hideSearchCallback: "=", searchCallback: "=" }, @@ -34,6 +35,7 @@ function treeSearchBox(localizationService, searchService, $q) { scope.showSearch = "false"; } + //used to cancel any request in progress if another one needs to take it's place var canceler = null; @@ -60,6 +62,11 @@ function treeSearchBox(localizationService, searchService, $q) { searchArgs["searchFrom"] = scope.searchFromId; } + //append ignoreUserStartNodes value if there is one + if (scope.ignoreUserStartNodes) { + searchArgs["ignoreUserStartNodes"] = scope.ignoreUserStartNodes; + } + searcher(searchArgs).then(function (data) { scope.searchCallback(data); //set back to null so it can be re-created diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index e7abc81841..4c1a8747d1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -91,6 +91,10 @@ Use this directive to generate a pagination. function link(scope, el, attr, ctrl) { function activate() { + // page number is sometimes a string - let's make sure it's an int before we do anything with it + if (scope.pageNumber) { + scope.pageNumber = parseInt(scope.pageNumber); + } scope.pagination = []; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbpermission.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbpermission.directive.js deleted file mode 100644 index bae87789ca..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbpermission.directive.js +++ /dev/null @@ -1,36 +0,0 @@ -(function () { - 'use strict'; - - function PermissionDirective() { - - function link(scope, el, attr, ctrl) { - - scope.change = function() { - scope.selected = !scope.selected; - if(scope.onChange) { - scope.onChange({'selected': scope.selected}); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/users/umb-permission.html', - scope: { - name: "=", - description: "=?", - selected: "=", - onChange: "&" - }, - link: link - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbPermission', PermissionDirective); - -})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 8b93666172..601ff2f671 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -215,14 +215,6 @@ angular.module('umbraco.mocks'). "placeholders_nameentity": "Name the %0%...", "placeholders_search": "Type to search...", "placeholders_filter": "Type to filter...", - "editcontenttype_allowedchildnodetypes": "Allowed child nodetypes", - "editcontenttype_create": "Create", - "editcontenttype_deletetab": "Delete tab", - "editcontenttype_description": "Description", - "editcontenttype_newtab": "New tab", - "editcontenttype_tab": "Tab", - "editcontenttype_thumbnail": "Thumbnail", - "editcontenttype_iscontainercontenttype": "Use as container content type", "editdatatype_addPrevalue": "Add prevalue", "editdatatype_dataBaseDatatype": "Database datatype", "editdatatype_guid": "Property editor GUID", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b807a4dc31..d571de0e2d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -365,17 +365,28 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} id id of content item to return - * @param {Int} culture optional culture to retrieve the item in + * @param {Bool} options.ignoreUserStartNodes set to true to allow a user to choose nodes that they normally don't have access to * @returns {Promise} resourcePromise object containing the content item. * */ - getById: function (id) { + getById: function (id, options) { + var defaults = { + ignoreUserStartNodes: false + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetById", - { id: id })), + [{ id: id }, { ignoreUserStartNodes: options.ignoreUserStartNodes }])), 'Failed to retrieve data for content id ' + id) .then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 753d180880..d5145727ac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -284,15 +284,31 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getAncestors: function (id, type, culture) { + getAncestors: function (id, type, culture, options) { + var defaults = { + ignoreUserStartNodes: false + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; if (culture === undefined) culture = ""; return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetAncestors", - [{ id: id }, { type: type }, { culture: culture }])), - 'Failed to retrieve ancestor data for id ' + id); + [ + { id: id }, + { type: type }, + { culture: culture }, + { ignoreUserStartNodes: options.ignoreUserStartNodes } + ])), + + 'Failed to retrieve ancestor data for id ' + id); }, /** @@ -424,7 +440,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + ignoreUserStartNodes: false }; if (options === undefined) { options = {}; @@ -453,7 +470,8 @@ function entityResource($q, $http, umbRequestHelper) { pageSize: options.pageSize, orderBy: options.orderBy, orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) + filter: encodeURIComponent(options.filter), + ignoreUserStartNodes: options.ignoreUserStartNodes } )), 'Failed to retrieve child data for id ' + parentId); @@ -481,12 +499,19 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity array. * */ - search: function (query, type, searchFrom, canceler) { + search: function (query, type, options, canceler) { var args = [{ query: query }, { type: type }]; - if (searchFrom) { - args.push({ searchFrom: searchFrom }); + + if(options !== undefined) { + if (options.searchFrom) { + args.push({ searchFrom: options.searchFrom }); + } + if (options.ignoreUserStartNodes) { + args.push({ ignoreUserStartNodes: options.ignoreUserStartNodes }); + } } + var httpConfig = {}; if (canceler) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index dad5345e6c..bcdaddd22f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -53,7 +53,7 @@ function logResource($q, $http, umbRequestHelper) { * * @param {Object} options options object * @param {Int} options.id the id of the entity - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10 * @param {Int} options.pageNumber if paging data, current page index, default = 1 * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` * @param {Date} options.sinceDate if provided this will only get log entries going back to this date @@ -122,7 +122,7 @@ function logResource($q, $http, umbRequestHelper) { * * * @param {Object} options options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10 * @param {Int} options.pageNumber if paging data, current page index, default = 1 * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` * @param {Date} options.sinceDate if provided this will only get log entries going back to this date diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 1d6d5171a1..462184c9f2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -329,7 +329,8 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { filter: '', orderDirection: "Ascending", orderBy: "SortOrder", - orderBySystemField: true + orderBySystemField: true, + ignoreUserStartNodes: false }; if (options === undefined) { options = {}; @@ -367,6 +368,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "GetChildren", [ { id: parentId }, + { ignoreUserStartNodes: options.ignoreUserStartNodes }, { pageNumber: options.pageNumber }, { pageSize: options.pageSize }, { orderBy: options.orderBy }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 8aaddbf98f..20d014ab0f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -45,7 +45,7 @@ (function () { 'use strict'; - function listViewHelper(localStorageService) { + function listViewHelper($location, localStorageService, urlHelper) { var firstSelectedIndex = 0; var localStorageKey = "umblistViewLayout"; @@ -559,6 +559,32 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#editItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for opening an item in a list view for editing. + * + * @param {Object} item The item to edit + */ + function editItem(item) { + if (!item.editPath) { + return; + } + var parts = item.editPath.split("?"); + var path = parts[0]; + var params = parts[1] + ? urlHelper.getQueryStringParams("?" + parts[1]) + : {}; + + $location.path(path); + for (var p in params) { + $location.search(p, params[p]); + } + } + function isMatchingLayout(id, layout) { // legacy format uses "nodeId", be sure to look for both return layout.id === id || layout.nodeId === id; @@ -579,7 +605,8 @@ isSelectedAll: isSelectedAll, setSortingDirection: setSortingDirection, setSorting: setSorting, - getButtonPermissions: getButtonPermissions + getButtonPermissions: getButtonPermissions, + editItem: editItem }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index 04c431767c..a2010d20f2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -42,7 +42,11 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { + var options = { + searchFrom: args.searchFrom + } + + return entityResource.search(args.term, "Member", options).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureMemberResult(item); }); @@ -67,7 +71,12 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { + var options = { + searchFrom: args.searchFrom, + ignoreUserStartNodes: args.ignoreUserStartNodes + } + + return entityResource.search(args.term, "Document", options, args.canceler).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureContentResult(item); }); @@ -92,7 +101,12 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { + var options = { + searchFrom: args.searchFrom, + ignoreUserStartNodes: args.ignoreUserStartNodes + } + + return entityResource.search(args.term, "Media", options).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureMediaResult(item); }); 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 ce4bf6077c..2fd75184ca 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 @@ -48,7 +48,14 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s if (configuredStylesheets) { angular.forEach(configuredStylesheets, function (val, key) { - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); + if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) { + // current format (full path to stylesheet) + stylesheets.push(val); + } + else { + // legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css" + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); + } promises.push(stylesheetResource.getRulesByName(val).then(function (rules) { angular.forEach(rules, function (rule) { @@ -1143,11 +1150,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s let self = this; + function getIgnoreUserStartNodes(args) { + var ignoreUserStartNodes = false; + // Most property editors have a "config" property with ignoreUserStartNodes on then + if (args.model.config) { + ignoreUserStartNodes = Object.toBoolean(args.model.config.ignoreUserStartNodes); + } + // EXCEPT for the grid's TinyMCE editor, that one wants to be special and the config is called "configuration" instead + else if (args.model.configuration) { + ignoreUserStartNodes = Object.toBoolean(args.model.configuration.ignoreUserStartNodes); + } + return ignoreUserStartNodes; + } + //create link picker self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { var linkPicker = { currentTarget: currentTarget, anchors: editorState.current ? self.getAnchorNames(JSON.stringify(editorState.current.properties)) : [], + ignoreUserStartNodes: getIgnoreUserStartNodes(args), submit: function (model) { self.insertLinkInEditor(args.editor, model.target, anchorElement); editorService.close(); @@ -1161,13 +1182,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //Create the insert media plugin self.createMediaPicker(args.editor, function (currentTarget, userData) { + + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + + var ignoreUserStartNodes = getIgnoreUserStartNodes(args); + if (ignoreUserStartNodes) { + ignoreUserStartNodes = true; + startNodeId = -1; + startNodeIsVirtual = true; + } + var mediaPicker = { currentTarget: currentTarget, onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, submit: function (model) { self.insertMediaInEditor(args.editor, model.selection[0]); editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 6a15c0f553..d61d1c3ba1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -29,6 +29,15 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return cacheKey; } + // Adapted from: https://stackoverflow.com/a/2140723 + // Please note, we can NOT test this functionality correctly in Phantom because it implements + // the localeCompare method incorrectly: https://github.com/ariya/phantomjs/issues/11063 + function invariantEquals(a, b) { + return typeof a === "string" && typeof b === "string" + ? a.localeCompare(b, undefined, { sensitivity: "base" }) === 0 + : a === b; + } + return { /** Internal method to return the tree cache */ @@ -165,8 +174,8 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS Umbraco.Sys.ServerVariables.umbracoPlugins.trees && angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { - var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function(item) { - return item.alias === treeAlias; + var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { + return invariantEquals(item.alias, treeAlias); }); return found ? found.packageFolder : undefined; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 2a53c25f68..f02babda30 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -164,6 +164,7 @@ @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; @import "components/buttons/umb-toggle.less"; +@import "components/buttons/umb-toggle-group.less"; @import "components/notifications/umb-notifications.less"; @import "components/umb-file-dropzone.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle-group.less new file mode 100644 index 0000000000..df60c8aed1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle-group.less @@ -0,0 +1,35 @@ +.umb-toggle-group { + .umb-toggle-group-item { + display: flex; + border-bottom: 1px solid @gray-9; + padding: 7px 0; + } + + .umb-toggle-group-item:last-of-type { + border-bottom: none; + } + + .umb-toggle-group-item__toggle { + padding-right: 20px; + cursor: pointer; + } + + .umb-toggle-group-item__content { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 1 auto; + cursor: pointer; + } + + .umb-toggle-group-item--disabled .umb-toggle-group-item__toggle, + .umb-toggle-group-item--disabled .umb-toggle-group-item__content { + cursor: not-allowed; + opacity: 0.8; + } + + .umb-toggle-group-item__description { + font-size: 13px; + color: @gray-4; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less index 2d1cbd10ff..2a8137e5f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less @@ -1,7 +1,7 @@ .umb-expansion-panel { background: @white; border-radius: 3px; - margin-bottom: 16px; + margin-bottom: 20px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16); } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index b66ab40335..8358dc4b67 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -228,40 +228,64 @@ body.touch .umb-tree { } } -.umb-tree-item__annotation { - &::before { - font-family: 'icomoon'; - position: absolute; - bottom: 0; - } -} +.has-unpublished-version, .is-container, .protected { + > .umb-tree-item__inner { + > .umb-tree-item__annotation { + background-color: @white; + border-radius: 50%; + width: 12px; + height: 12px; + position: absolute; + margin-left: 12px; + top: 17px; -.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { - content: "\e25a"; - color: @green; - font-size: 20px; - margin-left: -25px; + &::before { + font-family: 'icomoon'; + position: absolute; + top: -4px; + } + } + + &:hover > .umb-tree-item__annotation { + background-color: @ui-option-hover; + } + } + + &.current > .umb-tree-item__inner > .umb-tree-item__annotation { + background-color: @pinkLight; + } } .is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; +} + +.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { + content: "\e25a"; + color: @green; + font-size: 23px; + margin-left: 16px; + left: -21px; } .protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e256"; color: @red; - font-size: 20px; - margin-left: -25px; + font-size: 23px; + margin-left: -3px; + left: -2px; } .locked > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e0a7"; color: @red; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; } .no-access { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index fb83504a1f..c0e91e28c2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -1,7 +1,7 @@ .umb-box { background: @white; border-radius: 3px; - margin-bottom: 8px; + margin-bottom: 20px; box-shadow: 0 1px 1px 0 rgba(0,0,0,.16); } @@ -28,4 +28,4 @@ .umb-box-content { padding: 20px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index f4725fa82d..1e3cf54450 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -28,9 +28,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", searchFromName: null, showSearch: false, results: [], - selectedSearchResults: [] + selectedSearchResults: [], + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes }; + $scope.customTreeParams = dialogOptions.ignoreUserStartNodes ? "ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes : ""; $scope.showTarget = $scope.model.hideTarget !== true; // this ensures that we only sync the tree once and only when it's ready @@ -73,7 +75,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); // get the content properties to build the anchor name list - contentResource.getById(id).then(function (resp) { + + var options = {}; + options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; + + contentResource.getById(id, options).then(function (resp) { $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); $scope.model.target.url = resp.urls[0].text; }); @@ -119,7 +125,10 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; } else { - contentResource.getById(args.node.id).then(function (resp) { + var options = {}; + options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; + + contentResource.getById(args.node.id, options).then(function (resp) { $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); $scope.model.target.url = resp.urls[0].text; }); @@ -139,9 +148,17 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + if (dialogOptions.ignoreUserStartNodes) { + startNodeId = -1; + startNodeIsVirtual = true; + } + var mediaPicker = { - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, submit: function (model) { var media = model.selection[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 71fcf2f493..414dbecfc2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -68,6 +68,7 @@ search-from-id="{{searchInfo.searchFromId}}" search-from-name="{{searchInfo.searchFromName}}" show-search="{{searchInfo.showSearch}}" + ignore-user-start-nodes="{{searchInfo.ignoreUserStartNodes}}" section="{{section}}"> @@ -84,6 +85,7 @@ section="content" hideheader="true" hideoptions="true" + customtreeparams="{{customTreeParams}}" api="dialogTreeApi" on-init="onTreeInit()" enablelistviewexpand="true" diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 2d6a2be471..2ba2300730 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -20,11 +20,13 @@ angular.module("umbraco") $scope.showDetails = dialogOptions.showDetails; $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; + $scope.ignoreUserStartNodes = Object.toBoolean(dialogOptions.ignoreUserStartNodes); $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; + var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { @@ -54,7 +56,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: "", + ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }; //preload selected item @@ -66,7 +69,7 @@ angular.module("umbraco") function onInit() { if ($scope.startNodeId !== -1) { entityResource.getById($scope.startNodeId, "media") - .then(function (ent) { + .then(function(ent) { $scope.startNodeId = ent.id; run(); }); @@ -143,7 +146,7 @@ angular.module("umbraco") } }; - $scope.gotoFolder = function(folder) { + $scope.gotoFolder = function (folder) { if (!$scope.multiPicker) { deselectAllImages($scope.model.selection); } @@ -152,8 +155,10 @@ angular.module("umbraco") folder = { id: -1, name: "Media", icon: "icon-folder" }; } + var options = {}; if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media") + options.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; + entityResource.getAncestors(folder.id, "media", options) .then(function(anc) { $scope.path = _.filter(anc, function(f) { @@ -169,13 +174,26 @@ angular.module("umbraco") $scope.path = []; } - $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual; + $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; + $scope.currentFolder = folder; localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - return getChildren(folder.id); + options.ignoreUserStartNodes = $scope.ignoreUserStartNodes; + return getChildren(folder.id, options); }; + function hasFolderAccess(node) { + var nodePath = node.path ? node.path.split(',') : [node.id]; + + for (var i = 0; i < nodePath.length; i++) { + if (userStartNodes.indexOf(parseInt(nodePath[i])) !== -1) + return true; + } + + return false; + } + $scope.clickHandler = function(image, event, index) { if (image.isFolder) { if ($scope.disableFolderSelect) { @@ -299,7 +317,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: "", + ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }; getChildren($scope.currentFolder.id); } @@ -367,9 +386,9 @@ angular.module("umbraco") } } - function getChildren(id) { + function getChildren(id, options) { $scope.loading = true; - return mediaResource.getChildren(id) + return mediaResource.getChildren(id, options) .then(function(data) { $scope.searchOptions.filter = ""; $scope.images = data.items ? data.items : []; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/nodepermissions/nodepermissions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/nodepermissions/nodepermissions.html index 1846e3caa2..58490fa224 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/nodepermissions/nodepermissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/nodepermissions/nodepermissions.html @@ -17,12 +17,7 @@ - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 5883313753..a6e2838b56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -36,6 +36,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", selectedSearchResults: [] } vm.startNodeId = $scope.model.startNodeId; + vm.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; //Used for toggling an empty-state message //Some trees can have no items (dictionary & forms email templates) vm.hasItems = true; @@ -171,6 +172,9 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (vm.startNodeId) { queryParams["startNodeId"] = $scope.model.startNodeId; } + if (vm.ignoreUserStartNodes) { + queryParams["ignoreUserStartNodes"] = $scope.model.ignoreUserStartNodes; + } if (vm.selectedLanguage && vm.selectedLanguage.id) { queryParams["culture"] = vm.selectedLanguage.culture; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index c592b4ec3b..acd838f7bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -27,7 +27,7 @@ {{language.name}} - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js index 9e758a5b21..74a677b7c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js @@ -8,6 +8,10 @@ function ItemPickerOverlay($scope, localizationService) { $scope.model.title = value; }); } + + if (!$scope.model.orderBy) { + $scope.model.orderBy = "name"; + } } $scope.selectItem = function(item) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index aac4830d52..328344ea84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -12,7 +12,7 @@
    -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html new file mode 100644 index 0000000000..1a9026c7ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html @@ -0,0 +1,13 @@ +
    +
    + + +
    +
    {{ item.name }}
    +
    {{ item.description }}
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 1f5235146d..a91bc8c876 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -4,7 +4,7 @@
    -
    +
    {{item.name}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-permission.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-permission.html deleted file mode 100644 index 3385b8e073..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-permission.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - - -
    -
    {{ name }}
    -
    {{ description }}
    -
    -
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/notify.html b/src/Umbraco.Web.UI.Client/src/views/content/notify.html index 43f14d519f..e6226ffb5e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/notify.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/notify.html @@ -21,11 +21,7 @@
    - - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 77584dac12..f7f1e36fcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -127,7 +127,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index 31c7c628c0..292db6f105 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -116,12 +116,7 @@ - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js index c250138adb..c8a08d57ca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js @@ -15,9 +15,15 @@ function MacrosSettingsController($scope, editorService, localizationService) { $scope.model.openViewPicker = openViewPicker; $scope.model.removeMacroView = removeMacroView; + var labels = {}; + + localizationService.localizeMany(["macro_selectViewFile"]).then(function(data) { + labels.selectViewFile = data[0]; + }); + function openViewPicker() { const controlPicker = { - title: "Select view", + title: labels.selectViewFile, section: "settings", treeAlias: "partialViewMacros", entityType: "partialView", diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index 987cd91ffc..cb620675a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -14,7 +14,9 @@ hide-description="true" hide-alias="true" navigation="content.apps" - on-select-navigation-item="appChanged(item)"> + on-select-navigation-item="appChanged(item)" + show-back-button="showBack()" + on-back="onBack()"> diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 8dda2d6669..d4d538b82c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -9,7 +9,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, - editorState, umbRequestHelper, $http, eventsService) { + editorState, umbRequestHelper, $http, eventsService, $location) { var evts = []; var nodeId = null; @@ -279,6 +279,17 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, } } + $scope.showBack = function () { + return !infiniteMode && !!$scope.page.listViewPath; + } + + /** Callback for when user clicks the back-icon */ + $scope.onBack = function() { + if ($scope.page.listViewPath) { + $location.path($scope.page.listViewPath); + } + }; + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index ee6e9c625c..d4078e56c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -13,8 +13,10 @@ menu="page.menu" hide-icon="true" hide-description="true" - hide-alias="true"> - + hide-alias="true" + show-back-button="showBack()" + on-back="onBack()"> + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index bf32f83c4e..6d5bc9036c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -15,14 +15,9 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR $scope.page.menu.currentSection = appState.getSectionState("currentSection"); $scope.page.menu.currentNode = null; //the editors affiliated node $scope.page.nameLocked = false; - $scope.page.listViewPath = null; $scope.page.saveButtonState = "init"; $scope.page.exportButton = "init"; - $scope.page.listViewPath = ($routeParams.page && $routeParams.listName) - ? "/member/member/list/" + $routeParams.listName + "?page=" + $routeParams.page - : null; - //build a path to sync the tree with function buildTreePath(data) { return $routeParams.listName ? "-1," + $routeParams.listName : "-1"; @@ -192,6 +187,19 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR }; + $scope.showBack = function () { + return !!$routeParams.listName; + } + + /** Callback for when user clicks the back-icon */ + $scope.onBack = function () { + $location.path("/member/member/list/" + $routeParams.listName); + $location.search("listName", null); + if ($routeParams.page) { + $location.search("page", $routeParams.page); + } + }; + $scope.export = function() { var memberKey = $scope.content.key; memberResource.exportMemberData(memberKey); 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 866cfb54ab..894ad2eedf 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 @@ -81,6 +81,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper showOpenButton: false, showEditButton: false, showPathOnHover: false, + ignoreUserStartNodes: false, maxNumber: 1, minNumber: 0, startNode: { @@ -118,7 +119,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.model.config.showOpenButton = Object.toBoolean($scope.model.config.showOpenButton); $scope.model.config.showEditButton = Object.toBoolean($scope.model.config.showEditButton); $scope.model.config.showPathOnHover = Object.toBoolean($scope.model.config.showPathOnHover); - + $scope.model.config.ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); + var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" @@ -134,6 +136,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper entityType: entityType, filterCssClass: "not-allowed not-published", startNodeId: null, + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, currentNode: editorState ? editorState.current : null, callback: function (data) { if (angular.isArray(data)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index a589cf8947..be4dbb9b12 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -14,6 +14,7 @@ sortable="!sortableOptions.disabled" allow-remove="allowRemoveButton" allow-open="model.config.showOpenButton && allowOpenButton && !dialogEditor" + ignore-user-startnodes="model.config.ignoreUserStartNodes" on-remove="remove($index)" on-open="openContentEditor(node)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index eb1032a9c7..71bb51f686 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,25 +1,32 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", function ($scope, $timeout, userService, editorService) { + var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); $scope.thumbnailUrl = getThumbnailUrl(); if (!$scope.model.config.startNodeId) { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; - $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - }); + if (ignoreUserStartNodes === true) { + $scope.model.config.startNodeId = -1; + $scope.model.config.startNodeIsVirtual = true; + + } else { + userService.getCurrentUser().then(function (userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + }); + } } $scope.setImage = function(){ var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - var mediaPicker = { startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, showDetails: true, disableFolderSelect: true, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js index 292ca3f975..9da70e38dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js @@ -117,7 +117,7 @@ } function goToItem(item, $event, $index) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + listViewHelper.editItem(item); } activate(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 294dd50147..4230633e96 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { + function ListViewListLayoutController($scope, listViewHelper, mediaHelper, mediaTypeHelper, urlHelper) { var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; @@ -53,8 +53,7 @@ } function clickItem(item) { - // if item.id is 2147483647 (int.MaxValue) use item.key - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); + listViewHelper.editItem(item); } function isSortDirection(col, direction) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index bac8eb903a..c937360693 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -7,9 +7,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; + var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); $scope.allowEditMedia = false; $scope.allowAddMedia = false; + + function setupViewModel() { $scope.mediaItems = []; $scope.ids = []; @@ -90,26 +93,31 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // reload. We only reload the images that is already picked but has been updated. // We have to get the entities from the server because the media // can be edited without being selected - _.each($scope.images, function (image, i) { - if (updatedMediaNodes.indexOf(image.udi) !== -1) { - image.loading = true; - entityResource.getById(image.udi, "media") - .then(function (mediaEntity) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); - image.loading = false; - }); - } - }) + _.each($scope.images, + function (image, i) { + if (updatedMediaNodes.indexOf(image.udi) !== -1) { + image.loading = true; + entityResource.getById(image.udi, "media") + .then(function (mediaEntity) { + angular.extend(image, mediaEntity); + image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); + image.loading = false; + }); + } + }); } function init() { - userService.getCurrentUser().then(function (userData) { if (!$scope.model.config.startNodeId) { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; } + if (ignoreUserStartNodes === true) { + $scope.model.config.startNodeId = -1; + $scope.model.config.startNodeIsVirtual = true; + } + // only allow users to add and edit media if they have access to the media section var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1; $scope.allowEditMedia = hasAccessToMedia; @@ -167,12 +175,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl var mediaPicker = { startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, allowMediaEdit: true, - submit: function(model) { + submit: function (model) { editorService.close(); @@ -182,7 +191,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - $scope.mediaItems.push(media); + $scope.mediaItems.push(media); if ($scope.model.config.idType === "udi") { $scope.ids.push(media.udi); @@ -205,7 +214,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; - + $scope.sortableOptions = { disabled: !$scope.isMultiPicker, @@ -217,7 +226,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. $timeout(function () { - angular.forEach($scope.mediaItems, function(value, key) { + angular.forEach($scope.mediaItems, function (value, key) { r.push($scope.model.config.idType === "udi" ? value.udi : value.id); }); $scope.ids = r; @@ -236,5 +245,5 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; init(); - + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 063a726f44..5b02479813 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -69,9 +69,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en url: link.url, target: link.target } : null; - + var linkPicker = { currentTarget: target, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), submit: function (model) { if (model.target.url || model.target.anchor) { // if an anchor exists, check that it is appropriately prefixed diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 0d24f9d1cc..0b62d877eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -127,6 +127,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop show: false, style: {}, filter: $scope.scaffolds.length > 15 ? true : false, + orderBy: "$index", view: "itempicker", event: $event, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index e0a16d8687..a4cf675c8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -8,7 +8,7 @@ Document Type - Tab + Group Template @@ -51,8 +51,8 @@

    - Tab:
    - Select the tab who's properties should be displayed. If left blank, the first tab on the doc type will be used. + Group:
    + Select the group whose properties should be displayed. If left blank, the first group on the document type will be used.

    Template:
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index d54a17e15a..979baef0f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -25,6 +25,7 @@ section: "content", treeAlias: "content", multiPicker: false, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), idType: $scope.model.config.idType ? $scope.model.config.idType : "int", submit: function (model) { select(model.selection[0]); @@ -47,6 +48,7 @@ section: "content", treeAlias: "content", multiPicker: false, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), idType: $scope.model.config.idType ? $scope.model.config.idType : "udi", submit: function (model) { select(model.selection[0]); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 59e0429678..1b489d6283 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -40,14 +40,17 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.stylesheets = stylesheets; }); - $scope.selected = function(cmd, alias, lookup){ - if (lookup && angular.isArray(lookup)) { - cmd.selected = lookup.indexOf(alias) >= 0; - return cmd.selected; - } - return false; + $scope.commandSelected = function(cmd) { + cmd.selected = $scope.model.value.toolbar.indexOf(cmd.alias) >= 0; + return cmd.selected; }; + $scope.cssSelected = function (css) { + // support both current format (full stylesheet path) and legacy format (stylesheet name only) + css.selected = $scope.model.value.stylesheets.indexOf(css.path) >= 0 ||$scope.model.value.stylesheets.indexOf(css.name) >= 0; + return css.selected; + } + $scope.selectCommand = function(command){ var index = $scope.model.value.toolbar.indexOf(command.alias); @@ -60,11 +63,16 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.selectStylesheet = function (css) { - var index = $scope.model.value.stylesheets.indexOf(css.name); + // find out if the stylesheet is already selected; first look for the full stylesheet path (current format) + var index = $scope.model.value.stylesheets.indexOf(css.path); + if (index === -1) { + // ... then look for the stylesheet name (legacy format) + index = $scope.model.value.stylesheets.indexOf(css.name); + } - if(css.selected && index === -1){ - $scope.model.value.stylesheets.push(css.name); - }else if(index >= 0){ + if(index === -1){ + $scope.model.value.stylesheets.push(css.path); + }else{ $scope.model.value.stylesheets.splice(index, 1); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 6314e0b31e..463b1a03e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -4,7 +4,7 @@

    diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index e32297341d..965880d94f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -128,15 +128,10 @@ -
    - - -
    + +
    @@ -150,15 +145,10 @@ -
    - - -
    + +
    diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js index c2ad072078..4d19cf557a 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js @@ -303,7 +303,7 @@ describe('tree service tests', function () { }); it('returns undefined for a not found tree', function () { - //we know this exists in the mock umbraco server vars + //we know this does not exist in the mock umbraco server vars var found = treeService.getTreePackageFolder("asdfasdf"); expect(found).not.toBeDefined(); }); @@ -315,8 +315,8 @@ describe('tree service tests', function () { it('hasChildren has to be updated on parent', function () { var tree = getContentTree(); - while (tree.children.length > 0){ - treeService.removeNode(tree.children[0]) + while (tree.children.length > 0) { + treeService.removeNode(tree.children[0]); } expect(tree.hasChildren).toBe(false); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml index 42fe9f920b..a229898673 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml @@ -259,13 +259,6 @@ Pište pro filtrování... - Povolené podřízené typy uzlů - Vytvořit - Odstranit záložku - Popis - Nová záložka - Záložka - Miniatura Přidat předlohu diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index e75ff6d83d..7cc7c7823a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -503,13 +503,6 @@ Genererer alias... - Tilladte typer - Opret - Slet fane - Beskrivelse - Ny fane - Fane - Thumbnail Opret brugerdefineret listevisning Fjern brugerdefineret listevisning @@ -609,6 +602,7 @@ Første Generelt Grupper + Gruppe Højde Hjælp Skjul @@ -1338,6 +1332,7 @@ Mange hilsner fra Umbraco robotten Indtast makronavn Parametre Definer de parametre, der skal være tilgængelige, når du bruger denne makro. + Vælg partial view makrofil Alternativt felt diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index 7e1cb96228..3c8d24ec9c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -273,13 +273,6 @@ Der Benutzername ist normalerweise Ihre E-Mail-Adresse - Dokumenttypen, die unterhalb dieses Typs erlaubt sind - Erstellen - Registerkarte löschen - Beschreibung - Neue Registerkarte - Registerkarte - Illustration Angepasste Listenansicht erstellen Angepasste Listenansicht entfernen diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 372bc3158d..b13f55bf7a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -521,13 +521,6 @@ Generating alias... - Allowed child node types - Create - Delete tab - Description - New tab - Tab - Thumbnail Create custom list view Remove custom list view A content type, media type or member type with this alias already exists @@ -632,6 +625,7 @@ First General Groups + Group Height Help Hide @@ -1615,6 +1609,7 @@ To manage your website, simply open the Umbraco back office and start adding con Enter macro name Parameters Define the parameters that should be available when using this macro. + Select partial view macro file Building models 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 fb5a6bd359..4e13407a4c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -523,13 +523,6 @@ Generating alias... - Allowed child node types - Create - Delete tab - Description - New tab - Tab - Thumbnail Create custom list view Remove custom list view A content type, media type or member type with this alias already exists @@ -635,6 +628,7 @@ First General Groups + Group Height Help Hide @@ -1625,6 +1619,7 @@ To manage your website, simply open the Umbraco back office and start adding con Enter macro name Parameters Define the parameters that should be available when using this macro. + Select partial view macro file Building models diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml index aef92af6e1..66cad5fd26 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml @@ -391,13 +391,6 @@ Tu nombre de usuario normalmente es tu e-mail - Tipos de nodos hijos permitidos - Crear - Borrar pestaña - Descripción - Nueva pestaña - Pestaña - Miniatura Crear un tipo de listado personalizado Quitar el tipo de listado personalizado diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index e992767f4f..8e378f0bc9 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -406,13 +406,6 @@ Votre nom d'utilisateur est généralement votre adresse email - Types de noeuds enfants autorisés - Créer - Supprimer l'onglet - Description - Nouvel onglet - Onglet - Miniature Créer une liste personnalisée Supprimer la liste personnalisée diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml index a0a6c2cefc..ece1810ef5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml @@ -202,13 +202,6 @@ שם התצוגה לשפה - תת פריטי תוכן מאושרים: - צור - מחק לשונית - תיאור - לשונית חדשה - לשונית - תמונה ממוזערת הוסף ערך מקדים diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 8d8105c4d1..3ae078252a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -205,13 +205,6 @@ - Possibili nodetypes figli - Crea - Cancella tab - Descrizione - Nuova tab - Tab - Miniatura diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml index f6d957743e..303d0f0427 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml @@ -300,13 +300,6 @@ 入力してタグを追加 (各タグの後に Enter を押してください)... - 子ノードとして許可するタイプ - 新規作成 - タブの削除 - 説明 - 新しいタブ - タブ - サムネイル カスタムリストビューを作成する カスタムリストビューを削除する diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml index 2336dccb16..512d192237 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml @@ -201,13 +201,6 @@ 국가명 - 자식노드 타입 허용 - 생성 - 색인 삭제 - 설명 - 새 색인 - 색인 - 썸네일 이전값 더하기 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml index b38c11a6e0..cd6878a196 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml @@ -268,13 +268,6 @@ Skriv inn nøkkelord (trykk på Enter etter hvert nøkkelord)... - Tillatte underordnede noder - Opprett - Slett arkfane - Beskrivelse - Ny arkfane - Arkfane - Miniatyrbilde Opprett brukerdefinert listevisning Fjern brukerdefinert listevisning diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index 28d71ed003..556a8c4d48 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -330,13 +330,6 @@ Jouw gebruikersnaam is meestal jouw email - Toegelaten subnodetypes - Nieuw - Tab verwijderen - Omschrijving - Nieuwe tab - Tab - Miniatuur Maak een aangepaste lijstweergave Verwijder aangepaste lijstweergave diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml index 96e8e980a6..055d9eddcf 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml @@ -393,13 +393,6 @@ Twoja nazwa użytkownika to przeważnie Twój adres e-mail - Dozwolone węzły pochodne - Stwórz - Usuń zakładkę - Opis - Nowa zakładka - Zakładka - Miniatura Stwórz niestandardowy widok listy Usuń niestandardowy widok listy diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml index a275fc8cd9..767617a2d5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml @@ -199,13 +199,6 @@ Nome da Cultura - Tipos de nós filhos permitidos - Criar - Remover guia - Descrição - Nova guia - Guia - Miniatura Adicionar valor prévio diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index a0fc2c3ffc..7698453b4a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -427,15 +427,8 @@ Обзор словаря - Допустимые типы дочерних узлов - Создать Создать пользовательский список - Удалить вкладку - Описание - Новая вкладка Удалить пользовательский список - Вкладка - Миниатюра Добавить предустановленное значение diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index 9e2e312c38..bd847a605a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -257,15 +257,8 @@ Språknamn - Tillåtna typer för underliggande noder - Skapa Skapa en anpassad listvy - Ta bort flik - Beskrivning - Ny flik Radera anpassad listvy - Flik - Miniatyrbild Lägg till värde diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml index 1a298c0a35..347f205a1f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml @@ -239,13 +239,6 @@ (Basın, her etiketinden sonra girin) etiket eklemek için yazın ... - İzin alt düğüm çeşitleri - Oluştur - Sekmesini sil - Tanım - Yeni sekme - Sekme - Küçük resim Özel liste görünüm oluşturun Özel liste görünümü kaldır diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml index c8c3f75e17..40a81413e5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml @@ -318,13 +318,6 @@ 输入您的电子邮件 - 允许子项节点类型 - 创建 - 删除选项卡 - 描述 - 新建选项卡 - 选项卡 - 缩略图 创建自定义列表视图 删除自定义列表视图 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml index 56639aa308..a212826bbc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml @@ -311,13 +311,6 @@ 輸入您的電子郵件 - 允許子項節點類型 - 創建 - 刪除選項卡 - 描述 - 新建選項卡 - 選項卡 - 縮略圖 新增自訂清單檢視 移除自訂清單檢視 diff --git a/src/Umbraco.Web.UI/favicon.ico b/src/Umbraco.Web.UI/favicon.ico new file mode 100644 index 0000000000..c0749ddf7f Binary files /dev/null and b/src/Umbraco.Web.UI/favicon.ico differ diff --git a/src/Umbraco.Web/Compose/NotificationsComponent.cs b/src/Umbraco.Web/Compose/NotificationsComponent.cs index 08bcdf345c..adc0008f48 100644 --- a/src/Umbraco.Web/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Web/Compose/NotificationsComponent.cs @@ -46,6 +46,9 @@ namespace Umbraco.Web.Compose //Send notifications for the unpublish action ContentService.Unpublished += (sender, args) => _notifier.Notify(_actions.GetAction(), args.PublishedEntities.ToArray()); + + //Send notifications for the rollback action + ContentService.RolledBack += (sender, args) => _notifier.Notify(_actions.GetAction(), args.Entity); } public void Terminate() diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index a8824d416f..8ec05d1f01 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1483,7 +1483,7 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)] public HttpResponseMessage EmptyRecycleBin() { - Services.ContentService.EmptyRecycleBin(); + Services.ContentService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index e648a4d140..05f7b6525c 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -99,8 +99,25 @@ namespace Umbraco.Web.Editors /// A starting point for the search, generally a node id, but for members this is a member type alias /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [HttpGet] public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) + { + return Search(query, type, ignoreUserStartNodes: false, searchFrom); + } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// If set to true, user and group start node permissions will be ignored. + /// + [HttpGet] + public IEnumerable Search(string query, UmbracoEntityTypes type, bool? ignoreUserStartNodes, string searchFrom = null) { // TODO: Should we restrict search results based on what app the user has access to? // - Theoretically you shouldn't be able to see member data if you don't have access to members right? @@ -110,7 +127,7 @@ namespace Umbraco.Web.Editors //TODO: This uses the internal UmbracoTreeSearcher, this instead should delgate to the ISearchableTree implementation for the type - return ExamineSearch(query, type, searchFrom); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes != null && ignoreUserStartNodes.Value); } /// @@ -534,6 +551,7 @@ namespace Umbraco.Web.Editors } } + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] public PagedResult GetPagedDescendants( int id, UmbracoEntityTypes type, @@ -542,6 +560,20 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, type, pageNumber, pageSize, + ignoreUserStartNodes: false, orderBy, orderDirection, filter); + } + + public PagedResult GetPagedDescendants( + int id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + bool ignoreUserStartNodes, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -569,7 +601,7 @@ namespace Umbraco.Web.Editors break; } - entities = aids == null || aids.Contains(Constants.System.Root) + entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? Services.EntityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection), includeTrashed: false) @@ -611,9 +643,15 @@ namespace Umbraco.Web.Editors } } - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings) + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormDataCollection queryStrings) { - return GetResultForAncestors(id, type, queryStrings); + return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes: false); + } + + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings, bool ignoreUserStartNodes) + { + return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes); } /// @@ -622,10 +660,11 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { - return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, searchFrom); + return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, ignoreUserStartNodes, searchFrom); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) @@ -651,7 +690,7 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null) + private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null, bool ignoreUserStartNodes = false) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) @@ -660,35 +699,38 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); - int[] aids = null; - switch (entityType) + if (ignoreUserStartNodes == false) { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } - - if (aids != null) - { - var lids = new List(); - var ok = false; - foreach (var i in ids) + int[] aids = null; + switch (entityType) { - if (ok) - { - lids.Add(i); - continue; - } - if (aids.Contains(i)) - { - lids.Add(i); - ok = true; - } + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + break; + } + + if (aids != null) + { + var lids = new List(); + var ok = false; + foreach (var i in ids) + { + if (ok) + { + lids.Add(i); + continue; + } + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } + } + ids = lids.ToArray(); } - ids = lids.ToArray(); } var culture = queryStrings?.GetValue("culture"); diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index 4f750920d3..c3e3790378 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -18,10 +18,15 @@ namespace Umbraco.Web.Editors [UmbracoApplicationAuthorize(Core.Constants.Applications.Content, Core.Constants.Applications.Media)] public PagedResult GetPagedEntityLog(int id, int pageNumber = 1, - int pageSize = 0, + int pageSize = 10, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null) { + if (pageSize <= 0 || pageNumber <= 0) + { + return new PagedResult(0, pageNumber, pageSize); + } + long totalRecords; var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); @@ -37,10 +42,15 @@ namespace Umbraco.Web.Editors public PagedResult GetPagedCurrentUserLog( int pageNumber = 1, - int pageSize = 0, + int pageSize = 10, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null) { + if (pageSize <= 0 || pageNumber <= 0) + { + return new PagedResult(0, pageNumber, pageSize); + } + long totalRecords; var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; var userId = Security.GetUserId().ResultOr(0); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 3b7142397b..1097646830 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -82,7 +82,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); + var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); var mapped = Mapper.Map(emptyContent); //remove the listview app if it exists @@ -246,6 +246,7 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(int id, + bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -255,7 +256,7 @@ namespace Umbraco.Web.Editors { //if a request is made for the root node data but the user's start node is not the default, then // we need to return their start nodes - if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + if (id == Constants.System.Root && UserStartNodes.Length > 0 && (UserStartNodes.Contains(Constants.System.Root) == false && ignoreUserStartNodes == false)) { if (pageNumber > 0) return new PagedResult>(0, 0, 0); @@ -311,6 +312,7 @@ namespace Umbraco.Web.Editors } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity GUID id /// /// @@ -321,8 +323,34 @@ namespace Umbraco.Web.Editors /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Guid id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Returns the child media objects - using the entity GUID id + /// + /// + /// /// If set to true, user and group start node permissions will be ignored. + /// + /// + /// + /// + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(Guid id, + bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -333,12 +361,13 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(id); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } throw new HttpResponseException(HttpStatusCode.NotFound); } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity UDI id /// /// @@ -349,6 +378,7 @@ namespace Umbraco.Web.Editors /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Udi id, int pageNumber = 0, @@ -357,6 +387,31 @@ namespace Umbraco.Web.Editors Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Returns the child media objects - using the entity UDI id + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + /// + /// + /// + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(Udi id, + bool ignoreUserStartNodes, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -364,7 +419,7 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(guidUdi.Guid); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } } @@ -392,7 +447,7 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundMedia.Trashed == false) { - var moveResult = Services.MediaService.MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + var moveResult = Services.MediaService.MoveToRecycleBin(foundMedia, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -402,7 +457,7 @@ namespace Umbraco.Web.Editors } else { - var deleteResult = Services.MediaService.Delete(foundMedia, (int)Security.CurrentUser.Id); + var deleteResult = Services.MediaService.Delete(foundMedia, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -426,7 +481,7 @@ namespace Umbraco.Web.Editors var destinationParentID = move.ParentId; var sourceParentID = toMove.ParentId; - var moveResult = Services.MediaService.Move(toMove, move.ParentId); + var moveResult = Services.MediaService.Move(toMove, move.ParentId, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (sourceParentID == destinationParentID) { @@ -502,7 +557,7 @@ namespace Umbraco.Web.Editors } //save the item - var saveStatus = Services.MediaService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + var saveStatus = Services.MediaService.Save(contentItem.PersistedContent, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); //return the updated model var display = Mapper.Map(contentItem.PersistedContent); @@ -548,7 +603,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage EmptyRecycleBin() { - Services.MediaService.EmptyRecycleBin(); + Services.MediaService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } diff --git a/src/Umbraco.Web/Media/ImageHelper.cs b/src/Umbraco.Web/Media/ImageHelper.cs index 5a5724dc7d..899f817d5a 100644 --- a/src/Umbraco.Web/Media/ImageHelper.cs +++ b/src/Umbraco.Web/Media/ImageHelper.cs @@ -1,17 +1,7 @@ using System; -using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Media; -using Umbraco.Core.Models; -using Umbraco.Web.Composing; using Umbraco.Web.Media.Exif; namespace Umbraco.Web.Media @@ -28,9 +18,9 @@ namespace Umbraco.Web.Media /// use potentially large amounts of memory. public static Size GetDimensions(Stream stream) { + //Try to load with exif try { - //Try to load with exif var jpgInfo = ImageFile.FromStream(stream); if (jpgInfo != null @@ -45,11 +35,17 @@ namespace Umbraco.Web.Media return new Size(width, height); } } + } + catch + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } - //we have no choice but to try to read in via GDI + //we have no choice but to try to read in via GDI + try + { using (var image = Image.FromStream(stream)) { - var fileWidth = image.Width; var fileHeight = image.Height; return new Size(fileWidth, fileHeight); @@ -57,13 +53,10 @@ namespace Umbraco.Web.Media } catch (Exception) { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); + //We will just swallow, just means we can't read via GDI, we don't want to log an error either } - + + return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); } - - - } } diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 78c25ced33..94698f84bd 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Models.Mapping mapper.Define((source, context) => new EntityBasic(), Map); mapper.Define((source, context) => new ContentTypeSort(), Map); mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); mapper.Define((source, context) => new SearchResultEntity(), Map); mapper.Define>((source, context) => context.MapEnumerable(source)); mapper.Define, IEnumerable>((source, context) => context.MapEnumerable(source)); diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs index 7879e2b42b..5653e3fe03 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs @@ -10,5 +10,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor public Udi StartNodeId { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs index e2b46b360d..136ab88204 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs @@ -15,5 +15,8 @@ namespace Umbraco.Web.PropertyEditors // TODO: Make these strongly typed, for now this works though [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public JObject Rte { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.
    Note: this applies to all editors in this grid editor except for the rich text editor, which has it's own option for that.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index 4844e2f822..fa430e103b 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -19,5 +19,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "mediapicker")] public Udi StartNodeId { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index b6333c3140..a0a2467b1c 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -22,5 +22,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpen { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs index 515512eff8..ec9439ceea 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -9,5 +9,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("maxNumber", "Maximum number of items", "number")] public int MaxNumber { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs index 13bf269bcd..d99c2b17e0 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs @@ -14,5 +14,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("hideLabel", "Hide Label", "boolean")] public bool HideLabel { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 0a597a7138..33094221f7 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -29,6 +30,15 @@ namespace Umbraco.Web.PublishedCache /// The value of overrides defaults. IPublishedContent GetById(bool preview, Guid contentId); + /// + /// Gets a content identified by its Udi identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content Udi identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent GetById(bool preview, Udi contentId); + /// /// Gets a content identified by its unique identifier. /// @@ -45,6 +55,14 @@ namespace Umbraco.Web.PublishedCache /// Considers published or unpublished content depending on defaults. IPublishedContent GetById(Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent GetById(Udi contentId); + /// /// Gets a value indicating whether the cache contains a specified content. /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 89de144403..c4277d6a79 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -236,6 +236,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return GetNodePublishedContent(node, preview); } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Document) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); + + return GetById(preview, guidUdi.Guid); + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index d63035b219..fc04d9d1b3 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -44,6 +45,20 @@ namespace Umbraco.Web.PublishedCache.NuCache return n?.PublishedModel; } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Media) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Media}\".", nameof(contentId)); + + // ignore preview, there's only draft for media + var n = _snapshot.Get(guidUdi.Guid); + return n?.PublishedModel; + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index 44f0499a63..d726664db0 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -19,23 +20,22 @@ namespace Umbraco.Web.PublishedCache public abstract IPublishedContent GetById(bool preview, int contentId); public IPublishedContent GetById(int contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); public abstract IPublishedContent GetById(bool preview, Guid contentId); public IPublishedContent GetById(Guid contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); + + public abstract IPublishedContent GetById(bool preview, Udi contentId); + + public IPublishedContent GetById(Udi contentId) + => GetById(PreviewDefault, contentId); public abstract bool HasById(bool preview, int contentId); public bool HasById(int contentId) - { - return HasById(PreviewDefault, contentId); - } + => HasById(PreviewDefault, contentId); public abstract IEnumerable GetAtRoot(bool preview); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 43db9ff0ba..7fb51a61eb 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Search } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Searches for results based on the entity type /// /// @@ -50,11 +51,39 @@ namespace Umbraco.Web.Search /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] public IEnumerable ExamineSearch( string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, out long totalFound, string searchFrom = null) + long pageIndex, + out long totalFound, + string searchFrom = null) + { + return ExamineSearch(query, entityType, pageSize, pageIndex, out totalFound, ignoreUserStartNodes: false, searchFrom); + } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + public IEnumerable ExamineSearch( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + bool ignoreUserStartNodes, + string searchFrom = null) { var sb = new StringBuilder(); @@ -85,12 +114,12 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -288,7 +317,7 @@ namespace Umbraco.Web.Search } } - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService) + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, bool ignoreUserStartNodes, IEntityService entityService) { if (sb == null) throw new ArgumentNullException(nameof(sb)); if (entityService == null) throw new ArgumentNullException(nameof(entityService)); @@ -311,7 +340,7 @@ namespace Umbraco.Web.Search // make sure we don't find anything sb.Append("+__Path:none "); } - else if (startNodeIds.Contains(-1) == false) // -1 = no restriction + else if (startNodeIds.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction { var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 0c02dd6e46..d01e9fffb4 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); - if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false && IgnoreUserStartNodes(queryStrings) == false) { node.AdditionalData["noAccess"] = true; } @@ -90,11 +90,11 @@ namespace Umbraco.Web.Trees internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings) { var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out var hasPathAccess); - if (entityIsAncestorOfStartNodes == false) + if (IgnoreUserStartNodes(queryStrings) == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); - if (hasPathAccess == false) + if (IgnoreUserStartNodes(queryStrings) == false && hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; } @@ -134,7 +134,7 @@ namespace Umbraco.Web.Trees // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access - if (HasPathAccess(id, queryStrings) == false) + if (IgnoreUserStartNodes(queryStrings) == false && HasPathAccess(id, queryStrings) == false) { Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); return nodes; @@ -255,8 +255,9 @@ namespace Umbraco.Web.Trees /// protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { + var ignoreUserStartNodes = queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); //check if we're rendering the root - if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root)) + if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root) || ignoreUserStartNodes) { var altStartId = string.Empty; diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 4acf807b77..2e409c2820 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -369,6 +369,16 @@ namespace Umbraco.Web.Trees return queryStrings.GetValue(TreeQueryStringParameters.Use) == "dialog"; } + /// + /// If the request should allows a user to choose nodes that they normally don't have access to + /// + /// + /// + protected bool IgnoreUserStartNodes(FormDataCollection queryStrings) + { + return queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); + } + /// /// An event that allows developers to modify the tree node collection that is being rendered /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 466aff5a1f..0fcf5321e4 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,6 +8,7 @@ public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; //public const string OnNodeClick = "OnNodeClick"; //public const string RenderParent = "RenderParent"; } diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index efee045890..0c53838592 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Web.Actions; using Umbraco.Core.Security; using System.Net; +using System.Web; namespace Umbraco.Web.WebApi.Filters { @@ -66,7 +67,7 @@ namespace Umbraco.Web.WebApi.Filters //not logged in throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); } - + int nodeId; if (_nodeId.HasValue == false) { @@ -116,24 +117,29 @@ namespace Umbraco.Web.WebApi.Filters nodeId = _nodeId.Value; } - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - Current.UmbracoContext.Security.CurrentUser, - Current.Services.UserService, - Current.Services.ContentService, - Current.Services.EntityService, - out var contentItem, - _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) - throw new HttpResponseException(HttpStatusCode.NotFound); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); - - if (contentItem != null) + var queryStringCollection = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); + bool.TryParse(queryStringCollection["ignoreUserStartNodes"], out var ignoreUserStartNodes); + if (ignoreUserStartNodes == false) { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; + var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + Current.UmbracoContext.Security.CurrentUser, + Current.Services.UserService, + Current.Services.ContentService, + Current.Services.EntityService, + out var contentItem, + _permissionToCheck.HasValue ? new[] {_permissionToCheck.Value} : null); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + throw new HttpResponseException(HttpStatusCode.NotFound); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); + + if (contentItem != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; + } } base.OnActionExecuting(actionContext); diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index 21dc60e6cc..f8b02f08ca 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -3,12 +3,14 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Web; using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Composing; using Umbraco.Core.Security; +using Umbraco.Web.Trees; namespace Umbraco.Web.WebApi.Filters { @@ -72,7 +74,12 @@ namespace Umbraco.Web.WebApi.Filters protected virtual void FilterItems(IUser user, IList items) { - FilterBasedOnStartNode(items, user); + bool.TryParse(HttpContext.Current.Request.QueryString.Get(TreeQueryStringParameters.IgnoreUserStartNodes), out var ignoreUserStartNodes); + + if (ignoreUserStartNodes == false) + { + FilterBasedOnStartNode(items, user); + } } internal void FilterBasedOnStartNode(IList items, IUser user)