From cc9a7ff942d1f90c7dfd21904d47598a64f35069 Mon Sep 17 00:00:00 2001 From: Dan White Date: Sat, 23 Mar 2019 04:51:36 -0700 Subject: [PATCH 01/47] U4 10147 - Bypass User Security option in pickers (#2441) --- .../tree/umbtreesearchbox.directive.js | 7 + .../src/common/resources/content.resource.js | 1577 +++++------ .../src/common/resources/entity.resource.js | 49 +- .../src/common/resources/media.resource.js | 6 +- .../src/common/services/search.service.js | 22 +- .../overlays/contentpicker/contentpicker.html | 3 +- .../linkpicker/linkpicker.controller.js | 301 +- .../overlays/linkpicker/linkpicker.html | 2 + .../mediaPicker/mediapicker.controller.js | 55 +- .../treepicker/treepicker.controller.js | 1048 +++---- .../overlays/treepicker/treepicker.html | 1 + .../contentpicker/contentpicker.controller.js | 4 +- .../grid/editors/media.controller.js | 17 +- .../grid/editors/rte.controller.js | 17 +- .../mediapicker/mediapicker.controller.js | 16 +- .../multiurlpicker.controller.js | 3 +- .../relatedlinks/relatedlinks.controller.js | 2 + .../propertyeditors/rte/rte.controller.js | 15 +- .../propertyeditors/rte/rte.prevalues.html | 1 + src/Umbraco.Web/Editors/ContentController.cs | 2446 +++++++++-------- src/Umbraco.Web/Editors/EntityController.cs | 76 +- src/Umbraco.Web/Editors/MediaController.cs | 20 +- .../ContentPicker2PropertyEditor.cs | 14 +- .../PropertyEditors/GridPropertyEditor.cs | 3 + .../MediaPicker2PropertyEditor.cs | 9 +- .../MultiNodeTreePicker2PropertyEditor.cs | 10 +- .../MultiUrlPickerPropertyEditor.cs | 7 + .../RelatedLinks2PropertyEditor.cs | 6 +- .../PropertyEditors/RichTextPreValueEditor.cs | 8 + src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 11 +- .../Trees/ContentTreeControllerBase.cs | 12 +- src/Umbraco.Web/Trees/TreeControllerBase.cs | 10 + .../Trees/TreeQueryStringParameters.cs | 3 +- ...EnsureUserPermissionForContentAttribute.cs | 15 +- .../FilterAllowedOutgoingMediaAttribute.cs | 9 +- 35 files changed, 3025 insertions(+), 2780 deletions(-) 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/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b695809eaa..77e587bcc6 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 @@ -1,782 +1,795 @@ -/** - * @ngdoc service - * @name umbraco.resources.contentResource - * @description Handles all transactions of content data - * from the angular application to the Umbraco database, using the Content WebApi controller - * - * all methods returns a resource promise async, so all operations won't complete untill .then() is completed. - * - * @requires $q - * @requires $http - * @requires umbDataFormatter - * @requires umbRequestHelper - * - * ##usage - * To use, simply inject the contentResource into any controller or service that needs it, and make - * sure the umbraco.resources module is accesible - which it should be by default. - * - *
-  *    contentResource.getById(1234)
-  *          .then(function(data) {
-  *              $scope.content = data;
-  *          });    
-  * 
- **/ - -function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files, restApiUrl) { - return umbRequestHelper.postSaveContent({ - restApiUrl: restApiUrl, - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } - - return { - - - savePermissions: function (saveModel) { - if (!saveModel) { - throw "saveModel cannot be null"; - } - if (!saveModel.contentId) { - throw "saveModel.contentId cannot be null"; - } - if (!saveModel.permissions) { - throw "saveModel.permissions cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSaveUserGroupPermissions"), - saveModel), - 'Failed to save permissions'); - }, - - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for content recycle bin'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-          * var ids = [123,34533,2334,23434];
-          * contentResource.sort({ parentId: 1244, sortedIds: ids })
-          *    .then(function() {
-          *        $scope.complete = true;
-          *    });
-          * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-          * contentResource.move({ parentId: 1244, id: 123 })
-          *    .then(function() {
-          *        alert("node was moved");
-          *    }, function(err){
-          *      alert("node didnt move:" + err.data.Message); 
-          *    });
-          * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-          * contentResource.copy({ parentId: 1244, id: 123 })
-          *    .then(function() {
-          *        alert("node was copied");
-          *    }, function(err){
-          *      alert("node wasnt copy:" + err.data.Message); 
-          *    });
-          * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
-          * contentResource.unPublish(1234)
-          *    .then(function() {
-          *        alert("node was unpulished");
-          *    }, function(err){
-          *      alert("node wasnt unpublished:" + err.data.Message); 
-          *    });
-          * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-          * contentResource.emptyRecycleBin()
-          *    .then(function() {
-          *        alert('its empty!');
-          *    });
-          * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-          * contentResource.deleteById(1234)
-          *    .then(function() {
-          *        alert('its gone!');
-          *    });
-          * 
- * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, - - deleteBlueprint: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteBlueprint", - [{ id: id }])), - 'Failed to delete blueprint ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *        var myDoc = content; 
-          *        alert('its here!');
-          *    });
-          * 
- * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - getBlueprintById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetBlueprintById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - getNotifySettingsById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNotificationOptions", - [{ contentId: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - setNotifySettingsById: function (id, options) { - if (!id) { - throw "contentId cannot be null"; - } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostNotificationOptions", - { contentId: id, notifyOptions: options })), - 'Failed to set notify settings for content id ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-          * contentResource.getByIds( [1234,2526,28262])
-          *    .then(function(contentArray) {
-          *        var myDoc = contentArray; 
-          *        alert('they are here!');
-          *    });
-          * 
- * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); - }, - - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
-          * contentResource.getScaffold(1234, 'homepage')
-          *    .then(function(scaffold) {
-          *        var myDoc = scaffold;
-          *        myDoc.name = "My new document"; 
-          *
-          *        contentResource.publish(myDoc, true)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and published again");
-          *            });
-          *    });
-          * 
- * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); - }, - - getBlueprintScaffold: function (parentId, blueprintId) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ blueprintId: blueprintId }, { parentId: parentId}])), - 'Failed to retrieve blueprint for id ' + blueprintId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-          * contentResource.getNiceUrl(id)
-          *    .then(function(url) {
-          *        alert('its here!');
-          *    });
-          * 
- * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl", [{ id: id }])), - 'Failed to retrieve url for id:' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-          * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-          *    .then(function(contentArray) {
-          *        var children = contentArray; 
-          *        alert('they are here!');
-          *    });
-          * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { - - var defaults = { - includeProperties: [], - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - 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; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - //converts the value to a js bool - function toBool(v) { - if (angular.isNumber(v)) { - return v > 0; - } - if (angular.isString(v)) { - return v === "true"; - } - if (typeof v === "boolean") { - return v; - } - return false; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - { - id: parentId, - includeProperties: _.pluck(options.includeProperties, 'alias').join(","), - pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection, - orderBySystemField: toBool(options.orderBySystemField), - filter: options.filter - })), - 'Failed to retrieve children for content item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
-          * contentResource.hasPermission('p',1234)
-          *    .then(function() {
-          *        alert('You are allowed to publish this item');
-          *    });
-          * 
- * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function (permission, id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission }, { nodeId: id }])), - 'Failed to check permission for item ' + id); - }, - - getDetailedPermissions: function (contentId) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetDetailedPermissions", { contentId: contentId })), - 'Failed to retrieve permissions for content item ' + contentId); - }, - - getPermissions: function (nodeIds) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetPermissions"), - nodeIds), - 'Failed to get permissions'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name!";
-          *          contentResource.save(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and saved again");
-          *            });
-          *    });
-          * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - save: function (content, isNew, files) { - var endpoint = umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"); - return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); - }, - - saveBlueprint: function (content, isNew, files) { - var endpoint = umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSaveBlueprint"); - return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name, and be published!";
-          *          contentResource.publish(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and published again");
-          *            });
-          *    });
-          * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - publish: function (content, isNew, files) { - var endpoint = umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"); - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files, endpoint); - }, - - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-          * contentResource.getById(1234)
-          *    .then(function(content) {
-          *          content.name = "I want a new name, and be published!";
-          *          contentResource.sendToPublish(content, false)
-          *            .then(function(content){
-          *                alert("Retrieved, updated and notication send off");
-          *            });
-          *    });
-          * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - sendToPublish: function (content, isNew, files) { - var endpoint = umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"); - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files, endpoint); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-          * contentResource.publishById(1234)
-          *    .then(function(content) {
-          *        alert("published");
-          *    });
-          * 
- * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function (id) { - - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - }, - - createBlueprintFromContent: function (contentId, name) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl("contentApiBaseUrl", "CreateBlueprintFromContent", { - contentId: contentId, name: name - }) - ), - "Failed to create blueprint from content with id " + contentId - ); - } - - - }; -} - -angular.module('umbraco.resources').factory('contentResource', contentResource); +/** + * @ngdoc service + * @name umbraco.resources.contentResource + * @description Handles all transactions of content data + * from the angular application to the Umbraco database, using the Content WebApi controller + * + * all methods returns a resource promise async, so all operations won't complete untill .then() is completed. + * + * @requires $q + * @requires $http + * @requires umbDataFormatter + * @requires umbRequestHelper + * + * ##usage + * To use, simply inject the contentResource into any controller or service that needs it, and make + * sure the umbraco.resources module is accesible - which it should be by default. + * + *
+  *    contentResource.getById(1234)
+  *          .then(function(data) {
+  *              $scope.content = data;
+  *          });    
+  * 
+ **/ + +function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { + + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files, restApiUrl) { + return umbRequestHelper.postSaveContent({ + restApiUrl: restApiUrl, + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); + } + + return { + + + savePermissions: function (saveModel) { + if (!saveModel) { + throw "saveModel cannot be null"; + } + if (!saveModel.contentId) { + throw "saveModel.contentId cannot be null"; + } + if (!saveModel.permissions) { + throw "saveModel.permissions cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSaveUserGroupPermissions"), + saveModel), + 'Failed to save permissions'); + }, + + + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+          * var ids = [123,34533,2334,23434];
+          * contentResource.sort({ parentId: 1244, sortedIds: ids })
+          *    .then(function() {
+          *        $scope.complete = true;
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort content'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+          * contentResource.move({ parentId: 1244, id: 123 })
+          *    .then(function() {
+          *        alert("node was moved");
+          *    }, function(err){
+          *      alert("node didnt move:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
+          * contentResource.copy({ parentId: 1244, id: 123 })
+          *    .then(function() {
+          *        alert("node was copied");
+          *    }, function(err){
+          *      alert("node wasnt copy:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), + args), + 'Failed to copy content'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
+          * contentResource.unPublish(1234)
+          *    .then(function() {
+          *        alert("node was unpulished");
+          *    }, function(err){
+          *      alert("node wasnt unpublished:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ + unPublish: function (id) { + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
+          * contentResource.emptyRecycleBin()
+          *    .then(function() {
+          *        alert('its empty!');
+          *    });
+          * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
+          * contentResource.deleteById(1234)
+          *    .then(function() {
+          *        alert('its gone!');
+          *    });
+          * 
+ * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, + + deleteBlueprint: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteBlueprint", + [{ id: id }])), + 'Failed to delete blueprint ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *        var myDoc = content; 
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Int} id id of content item to return + * @param {Object} options optional options object + * @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, 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 }, { ignoreUserStartNodes: options.ignoreUserStartNodes }])), + 'Failed to retrieve data for content id ' + id); + }, + + getBlueprintById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetBlueprintById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); + }, + + getNotifySettingsById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNotificationOptions", + [{ contentId: id }])), + 'Failed to retrieve data for content id ' + id); + }, + + setNotifySettingsById: function (id, options) { + if (!id) { + throw "contentId cannot be null"; + } + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostNotificationOptions", + { contentId: id, notifyOptions: options })), + 'Failed to set notify settings for content id ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
+          * contentResource.getByIds( [1234,2526,28262])
+          *    .then(function(contentArray) {
+          *        var myDoc = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ + getByIds: function (ids) { + + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for content with multiple ids'); + }, + + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
+          * contentResource.getScaffold(1234, 'homepage')
+          *    .then(function(scaffold) {
+          *        var myDoc = scaffold;
+          *        myDoc.name = "My new document"; 
+          *
+          *        contentResource.publish(myDoc, true)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and published again");
+          *            });
+          *    });
+          * 
+ * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty content item type ' + alias); + }, + + getBlueprintScaffold: function (parentId, blueprintId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ blueprintId: blueprintId }, { parentId: parentId}])), + 'Failed to retrieve blueprint for id ' + blueprintId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
+          * contentResource.getNiceUrl(id)
+          *    .then(function(url) {
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), + 'Failed to retrieve url for id:' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
+          * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+          *    .then(function(contentArray) {
+          *        var children = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { + + var defaults = { + includeProperties: [], + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + 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; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + { + id: parentId, + includeProperties: _.pluck(options.includeProperties, 'alias').join(","), + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + orderBySystemField: toBool(options.orderBySystemField), + filter: options.filter + })), + 'Failed to retrieve children for content item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
+          * contentResource.hasPermission('p',1234)
+          *    .then(function() {
+          *        alert('You are allowed to publish this item');
+          *    });
+          * 
+ * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), + 'Failed to check permission for item ' + id); + }, + + getDetailedPermissions: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetDetailedPermissions", { contentId: contentId })), + 'Failed to retrieve permissions for content item ' + contentId); + }, + + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name!";
+          *          contentResource.save(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + save: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); + }, + + saveBlueprint: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSaveBlueprint"); + return saveContentItem(content, "save" + (isNew ? "New" : ""), files, endpoint); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name, and be published!";
+          *          contentResource.publish(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and published again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + publish: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files, endpoint); + }, + + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name, and be published!";
+          *          contentResource.sendToPublish(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and notication send off");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + sendToPublish: function (content, isNew, files) { + var endpoint = umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"); + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files, endpoint); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
+          * contentResource.publishById(1234)
+          *    .then(function(content) {
+          *        alert("published");
+          *    });
+          * 
+ * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { + + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + + }, + + createBlueprintFromContent: function (contentId, name) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "CreateBlueprintFromContent", { + contentId: contentId, name: name + }) + ), + "Failed to create blueprint from content with id " + contentId + ); + } + + + }; +} + +angular.module('umbraco.resources').factory('contentResource', contentResource); 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 72f8ad5539..4875491dc6 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 @@ -292,14 +292,29 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getAncestors: function (id, type) { + getAncestors: function (id, type, 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( "entityApiBaseUrl", "GetAncestors", - [{id: id}, {type: type}])), - 'Failed to retrieve ancestor data for id ' + id); + [ + { id: id }, + { type: type }, + { ignoreUserStartNodes: options.ignoreUserStartNodes } + ])), + 'Failed to retrieve ancestor data for id ' + id); }, /** @@ -431,7 +446,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + ignoreUserStartNodes: false }; if (options === undefined) { options = {}; @@ -460,7 +476,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); @@ -488,12 +505,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 }); + var defaults = { + searchFrom: null, + 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; var httpConfig = {}; if (canceler) { @@ -505,7 +529,12 @@ function entityResource($q, $http, umbRequestHelper) { umbRequestHelper.getApiUrl( "entityApiBaseUrl", "Search", - args), + { + query: query, + type: type, + searchFrom: options.searchFrom, + ignoreUserStartNodes: options.ignoreUserStartNodes + }), httpConfig), 'Failed to retrieve entity data for query ' + query); }, 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 8c27f20aea..e968913047 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 = {}; @@ -372,7 +373,8 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { { orderBy: options.orderBy }, { orderDirection: options.orderDirection }, { orderBySystemField: toBool(options.orderBySystemField) }, - { filter: options.filter } + { filter: options.filter }, + { ignoreUserStartNodes: options.ignoreUserStartNodes } ])), 'Failed to retrieve children for media item ' + parentId); }, 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 8738c1011e..0d00678282 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); }); @@ -157,4 +171,4 @@ angular.module('umbraco.services') var currentSection = sectionAlias; } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html index 43eab532d4..4391e50c28 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html @@ -9,6 +9,7 @@ search-from-id="{{searchInfo.searchFromId}}" search-from-name="{{searchInfo.searchFromName}}" show-search="{{searchInfo.showSearch}}" + ignore-user-startnodes="{{searchInfo.ignoreUserStartNodes}}" section="content"> @@ -45,4 +46,4 @@ on-close="closeMiniListView()"> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index fcce34621b..79b9362d3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -1,57 +1,58 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { - var dialogOptions = $scope.model; + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { + var dialogOptions = $scope.model; - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); - } + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); + } - $scope.dialogTreeEventHandler = $({}); - $scope.model.target = {}; - $scope.searchInfo = { - searchFromId: null, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - }; + $scope.dialogTreeEventHandler = $({}); + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, + results: [], + selectedSearchResults: [] + }; + $scope.customTreeParams = dialogOptions.ignoreUserStartNodes ? "ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes : ""; + $scope.showTarget = $scope.model.hideTarget !== true; - $scope.showTarget = $scope.model.hideTarget !== true; + if (dialogOptions.currentTarget) { + // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target + $scope.model.target = angular.copy(dialogOptions.currentTarget); + //if we have a node ID, we fetch the current node to build the form data + if ($scope.model.target.id || $scope.model.target.udi) { - if (dialogOptions.currentTarget) { - // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target - $scope.model.target = angular.copy(dialogOptions.currentTarget); - //if we have a node ID, we fetch the current node to build the form data - if ($scope.model.target.id || $scope.model.target.udi) { + //will be either a udi or an int + var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; - //will be either a udi or an int - var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; + // is it a content link? + if (!$scope.model.target.isMedia) { + // get the content path + entityResource.getPath(id, "Document").then(function (path) { + //now sync the tree to this path + $scope.dialogTreeEventHandler.syncTree({ + path: path, + tree: "content" + }); + }); - // is it a content link? - if (!$scope.model.target.isMedia) { - // get the content path - entityResource.getPath(id, "Document").then(function(path) { - //now sync the tree to this path - $scope.dialogTreeEventHandler.syncTree({ - path: path, - tree: "content" - }); - }); - - // get the content properties to build the anchor name list - contentResource.getById(id).then(function (resp) { - $scope.model.target.url = resp.urls[0]; - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - }); - } - } else if ($scope.model.target.url.length) { - // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs + // if a link exists, get the properties to build the anchor name list + contentResource.getById(id, { ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes }).then(function (resp) { + $scope.model.target.url = resp.urls[0]; + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + }); + } + } else if ($scope.model.target.url.length) { + // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); if (indexOfAnchor > -1) { @@ -60,124 +61,132 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", // then rewrite the model and populate the link $scope.model.target.url = $scope.model.target.url.substring(0, indexOfAnchor); } - } - } else if (dialogOptions.anchors) { - $scope.anchorValues = dialogOptions.anchors; - } + } + } else if (dialogOptions.anchors) { + $scope.anchorValues = dialogOptions.anchors; + } - function nodeSelectHandler(ev, args) { - if (args && args.event) { - args.event.preventDefault(); - args.event.stopPropagation(); - } + function nodeSelectHandler(ev, args) { + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } - eventsService.emit("dialogs.linkPicker.select", args); + eventsService.emit("dialogs.linkPicker.select", args); - if ($scope.currentNode) { - //un-select if there's a current one selected - $scope.currentNode.selected = false; - } + if ($scope.currentNode) { + //un-select if there's a current one selected + $scope.currentNode.selected = false; + } - $scope.currentNode = args.node; - $scope.currentNode.selected = true; - $scope.model.target.id = args.node.id; - $scope.model.target.udi = args.node.udi; - $scope.model.target.name = args.node.name; + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.model.target.id = args.node.id; + $scope.model.target.udi = args.node.udi; + $scope.model.target.name = args.node.name; - if (args.node.id < 0) { - $scope.model.target.url = "/"; - } else { - contentResource.getById(args.node.id).then(function (resp) { - $scope.model.target.url = resp.urls[0]; - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - }); - } + if (args.node.id < 0) { + $scope.model.target.url = "/"; + } else { + contentResource.getById(args.node.id, { ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes }).then(function (resp) { + $scope.model.target.url = resp.urls[0]; + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + }); + } - if (!angular.isUndefined($scope.model.target.isMedia)) { - delete $scope.model.target.isMedia; - } - } + if (!angular.isUndefined($scope.model.target.isMedia)) { + delete $scope.model.target.isMedia; + } + } - function nodeExpandedHandler(ev, args) { - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - } + function nodeExpandedHandler(ev, args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } - $scope.switchToMediaPicker = function () { - userService.getCurrentUser().then(function (userData) { - $scope.mediaPickerOverlay = { - view: "mediapicker", - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, - show: true, - submit: function (model) { - var media = model.selectedImages[0]; + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; - $scope.model.target.id = media.id; - $scope.model.target.udi = media.udi; - $scope.model.target.isMedia = true; - $scope.model.target.name = media.name; - $scope.model.target.url = mediaHelper.resolveFile(media); + if (dialogOptions.ignoreUserStartNodes) { + startNodeId = -1; + startNodeIsVirtual = true; + } + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + show: true, + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, + submit: function (model) { + var media = model.selectedImages[0]; - $scope.mediaPickerOverlay.show = false; - $scope.mediaPickerOverlay = null; + $scope.model.target.id = media.id; + $scope.model.target.udi = media.udi; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); - // make sure the content tree has nothing highlighted - $scope.dialogTreeEventHandler.syncTree({ - path: "-1", - tree: "content" - }); - } - }; - }); - }; + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; - $scope.hideSearch = function () { - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = null; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } + // make sure the content tree has nothing highlighted + $scope.dialogTreeEventHandler.syncTree({ + path: "-1", + tree: "content" + }); + } + }; + }); + }; - // method to select a search result - $scope.selectResult = function (evt, result) { - result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { - event: evt, - node: result - }); - }; + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } - //callback when there are search results - $scope.onSearchResults = function (results) { - $scope.searchInfo.results = results; - $scope.searchInfo.showSearch = true; - }; + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { + event: evt, + node: result + }); + }; - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - }); + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - // Mini list view - $scope.selectListViewNode = function (node) { - node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { - node: node - }); - }; + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { + node: node + }); + }; - function openMiniListView(node) { - $scope.miniListView = node; - } + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; - }); + function openMiniListView(node) { + $scope.miniListView = node; + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html index deba19aa11..c9f3ad54a7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -50,6 +50,7 @@ search-from-id="{{searchInfo.searchFromId}}" search-from-name="{{searchInfo.searchFromName}}" show-search="{{searchInfo.showSearch}}" + ignore-user-startnodes="{{searchInfo.ignoreUserStartNodes}}" section="{{section}}"> @@ -64,6 +65,7 @@ 0) { - entityResource.getAncestors(folder.id, "media") - .then(function(anc) { + entityResource.getAncestors(folder.id, "media", { ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }) + .then(function(anc) { $scope.path = _.filter(anc, function(f) { return f.path.indexOf($scope.startNodeId) !== -1; @@ -161,13 +168,13 @@ angular.module("umbraco") } else { $scope.path = []; } - + mediaTypeHelper.getAllowedImagetypes(folder.id) .then(function (types) { $scope.acceptedMediatypes = types; }); - - $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); @@ -263,6 +270,17 @@ angular.module("umbraco") } } + 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; + } + function gotoStartNode(err) { $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } @@ -297,7 +315,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: '', + ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }; getChildren($scope.currentFolder.id); } @@ -369,7 +388,7 @@ angular.module("umbraco") function getChildren(id) { $scope.loading = true; - return mediaResource.getChildren(id) + return mediaResource.getChildren(id, { ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }) .then(function(data) { $scope.searchOptions.filter = ""; $scope.images = data.items ? data.items : []; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index fa7a797125..1ff2228d62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -1,518 +1,530 @@ -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { - - var tree = null; - var dialogOptions = $scope.model; - $scope.treeReady = false; - $scope.dialogTreeEventHandler = $({}); - $scope.section = dialogOptions.section; - $scope.treeAlias = dialogOptions.treeAlias; - $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; - // if you need to load a not initialized tree set this value to false - default is true - $scope.onlyInitialized = dialogOptions.onlyInitialized; - $scope.searchInfo = { - searchFromId: dialogOptions.startNodeId, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - $scope.model.selection = []; - - //Used for toggling an empty-state message - //Some trees can have no items (dictionary & forms email templates) - $scope.hasItems = true; - $scope.emptyStateMessage = dialogOptions.emptyStateMessage; - var node = dialogOptions.currentNode; - - //This is called from ng-init - //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. - //this is probably an anti pattern IMO and shouldn't be used - $scope.init = function (contentType) { - - if (contentType === "content") { - $scope.entityType = "Document"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); - } - } else if (contentType === "member") { - $scope.entityType = "Member"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); - } - } else if (contentType === "media") { - $scope.entityType = "Media"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); - } - } - } - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; - - - //min / max values - if (dialogOptions.minNumber) { - dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); - } - if (dialogOptions.maxNumber) { - dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); - } - - if (dialogOptions.section === "member") { - $scope.entityType = "Member"; - } - else if (dialogOptions.section === "media") { - $scope.entityType = "Media"; - } - - // Search and listviews is only working for content, media and member section - var searchableSections = ["content", "media", "member"]; - - $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; - - //if a alternative startnode is used, we need to check if it is a container - if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { - entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { - if (node.metaData.IsContainer) { - openMiniListView(node); - } - initTree(); - }); - } - else { - initTree(); - } - - //Configures filtering - if (dialogOptions.filter) { - - dialogOptions.filterExclude = false; - dialogOptions.filterAdvanced = false; - - //used advanced filtering - if (angular.isFunction(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else if (angular.isObject(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else { - if (dialogOptions.filter.startsWith("!")) { - dialogOptions.filterExclude = true; - dialogOptions.filterTypes = dialogOptions.filter.substring(1); - } else { - dialogOptions.filterExclude = false; - dialogOptions.filterTypes = dialogOptions.filter; - } - - //used advanced filtering - if (dialogOptions.filter.startsWith("{")) { - dialogOptions.filterAdvanced = true; - //convert to object - dialogOptions.filter = angular.fromJson(dialogOptions.filter); - } - } - - $scope.filter = { - filterAdvanced: dialogOptions.filterAdvanced, - filterExclude: dialogOptions.filterExclude, - filter: dialogOptions.filterTypes - }; - } - - function initTree() { - //create the custom query string param for this tree - $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; - $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; - $scope.treeReady = true; - } - - function nodeExpandedHandler(ev, args) { - - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - - //now we need to look in the already selected search results and - // toggle the check boxes for those ones that are listed - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - if (exists) { - child.selected = true; - } - }); - - //check filter - performFiltering(args.children); - } - } - - //gets the tree object when it loads - function treeLoadedHandler(ev, args) { - //args.tree contains children (args.tree.root.children) - $scope.hasItems = args.tree.root.children.length > 0; - - tree = args.tree; - - var nodeHasPath = typeof node !== "undefined" && typeof node.path !== "undefined"; - var startNodeNotDefined = typeof dialogOptions.startNodeId === "undefined" || dialogOptions.startNodeId === "" || dialogOptions.startNodeId === "-1"; - if (startNodeNotDefined && nodeHasPath) { - $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); - } - - } - - //wires up selection - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.isSearchResult) { - //check if the item selected was a search result from a list view - - //unselect - select(args.node.name, args.node.id); - - //remove it from the list view children - var listView = args.node.parent(); - listView.children = _.reject(listView.children, function (child) { - return child.id == args.node.id; - }); - - //remove it from the custom tracked search result list - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == args.node.id; - }); - } - else { - eventsService.emit("dialogs.treePickerController.select", args); - - if (args.node.filtered) { - return; - } - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - if ($scope.model.select) { - $scope.model.select(args.node) - } else { - select(args.node.name, args.node.id); - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - } - } - - /** Method used for selecting a node */ - function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - - var rootNode = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - - if ($scope.multiPicker) { - if (entity) { - multiSelectItem(entity); - } else { - multiSelectItem(rootNode); - } - } - else { - $scope.model.selection.push(rootNode); - $scope.model.submit($scope.model); - } - } - else { - - if ($scope.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - - else { - - $scope.hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.model.selection.push(entity); - $scope.model.submit($scope.model); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - $scope.model.selection.push(ent); - $scope.model.submit($scope.model); - }); - } - } - } - } - - function multiSelectItem(item) { - - var found = false; - var foundIndex = 0; - - if ($scope.model.selection.length > 0) { - for (i = 0; $scope.model.selection.length > i; i++) { - var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { - found = true; - foundIndex = i; - } - } - } - - if (found) { - $scope.model.selection.splice(foundIndex, 1); - } else { - $scope.model.selection.push(item); - } - - } - - function performFiltering(nodes) { - - if (!dialogOptions.filter) { - return; - } - - //remove any list view search nodes from being filtered since these are special nodes that always must - // be allowed to be clicked on - nodes = _.filter(nodes, function (n) { - return !angular.isObject(n.metaData.listViewNode); - }); - - if (dialogOptions.filterAdvanced) { - - //filter either based on a method or an object - var filtered = angular.isFunction(dialogOptions.filter) - ? _.filter(nodes, dialogOptions.filter) - : _.where(nodes, dialogOptions.filter); - - angular.forEach(filtered, function (value, key) { - value.filtered = true; - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - }); - } else { - var a = dialogOptions.filterTypes.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { - - var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; - - if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { - value.filtered = true; - - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - } - }); - } - } - - $scope.multiSubmit = function (result) { - entityResource.getByIds(result, $scope.entityType).then(function (ents) { - $scope.submit(ents); - }); - }; - - /** method to select a search result */ - $scope.selectResult = function (evt, result) { - - if (result.filtered) { - return; - } - - result.selected = result.selected === true ? false : true; - - //since result = an entity, we'll pass it in so we don't have to go back to the server - select(result.name, result.id, result); - - //add/remove to our custom tracked list of selected search results - if (result.selected) { - $scope.searchInfo.selectedSearchResults.push(result); - } - else { - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == result.id; - }); - } - - //ensure the tree node in the tree is checked/unchecked if it already exists there - if (tree) { - var found = treeService.getDescendantNode(tree.root, result.id); - if (found) { - found.selected = result.selected; - } - } - - }; - - $scope.hideSearch = function () { - - //Traverse the entire displayed tree and update each node to sync with the selected search results - if (tree) { - - //we need to ensure that any currently displayed nodes that get selected - // from the search get updated to have a check box! - function checkChildren(children) { - _.each(children, function (child) { - //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - //if the curr node exists in selected search results, ensure it's checked - if (exists) { - child.selected = true; - } - //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result - else if (child.metaData.isSearchResult) { - //if this tree node is under a list view it means that the node was added - // to the tree dynamically under the list view that was searched, so we actually want to remove - // it all together from the tree - var listView = child.parent(); - listView.children = _.reject(listView.children, function (c) { - return c.id == child.id; - }); - } - - //check if the current node is a list view and if so, check if there's any new results - // that need to be added as child nodes to it based on search results selected - if (child.metaData.isContainer) { - - child.cssClasses = _.reject(child.cssClasses, function (c) { - return c === 'tree-node-slide-up-hide-active'; - }); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function (c) { - return c.id == item.id; - }); - if (!childExists) { - var parent = child; - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return parent; - } - }); - } - }); - } - - //recurse - if (child.children && child.children.length > 0) { - checkChildren(child.children); - } - }); - } - checkChildren(tree.root.children); - } - - - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = dialogOptions.startNodeId; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - $scope.onSearchResults = function (results) { - - //filter all items - this will mark an item as filtered - performFiltering(results); - - //now actually remove all filtered items so they are not even displayed - results = _.filter(results, function (item) { - return !item.filtered; - }); - - $scope.searchInfo.results = results; - - //sync with the curr selected results - _.each($scope.searchInfo.results, function (result) { - var exists = _.find($scope.model.selection, function (selectedId) { - return result.id == selectedId; - }); - if (exists) { - result.selected = true; - } - }); - - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - - $scope.selectListViewNode = function (node) { - select(node.name, node.id); - //toggle checked state - node.selected = node.selected === true ? false : true; - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", + function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { + + var tree = null; + var dialogOptions = $scope.model; + $scope.treeReady = false; + $scope.dialogTreeEventHandler = $({}); + $scope.section = dialogOptions.section; + $scope.treeAlias = dialogOptions.treeAlias; + $scope.multiPicker = dialogOptions.multiPicker; + $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; + // if you need to load a not initialized tree set this value to false - default is true + $scope.onlyInitialized = dialogOptions.onlyInitialized; + $scope.searchInfo = { + searchFromId: dialogOptions.startNodeId, + searchFromName: null, + showSearch: false, + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, + results: [], + selectedSearchResults: [] + } + + $scope.model.selection = []; + + //Used for toggling an empty-state message + //Some trees can have no items (dictionary & forms email templates) + $scope.hasItems = true; + $scope.emptyStateMessage = dialogOptions.emptyStateMessage; + var node = dialogOptions.currentNode; + + //This is called from ng-init + //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. + //this is probably an anti pattern IMO and shouldn't be used + $scope.init = function (contentType) { + + if (contentType === "content") { + $scope.entityType = "Document"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); + } + } else if (contentType === "member") { + $scope.entityType = "Member"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); + } + } else if (contentType === "media") { + $scope.entityType = "Media"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); + } + } + } + + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + // Allow the entity type to be passed in but defaults to Document for backwards compatibility. + $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; + + + //min / max values + if (dialogOptions.minNumber) { + dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); + } + if (dialogOptions.maxNumber) { + dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); + } + + if (dialogOptions.section === "member") { + $scope.entityType = "Member"; + } + else if (dialogOptions.section === "media") { + $scope.entityType = "Media"; + } + + // Search and listviews is only working for content, media and member section + var searchableSections = ["content", "media", "member"]; + + $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; + + //if a alternative startnode is used, we need to check if it is a container + if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { + entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { + if (node.metaData.IsContainer) { + openMiniListView(node); + } + initTree(); + }); + } + else { + initTree(); + } + + //Configures filtering + if (dialogOptions.filter) { + + dialogOptions.filterExclude = false; + dialogOptions.filterAdvanced = false; + + //used advanced filtering + if (angular.isFunction(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else if (angular.isObject(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else { + if (dialogOptions.filter.startsWith("!")) { + dialogOptions.filterExclude = true; + dialogOptions.filterTypes = dialogOptions.filter.substring(1); + } else { + dialogOptions.filterExclude = false; + dialogOptions.filterTypes = dialogOptions.filter; + } + + //used advanced filtering + if (dialogOptions.filter.startsWith("{")) { + dialogOptions.filterAdvanced = true; + //convert to object + dialogOptions.filter = angular.fromJson(dialogOptions.filter); + } + } + + $scope.filter = { + filterAdvanced: dialogOptions.filterAdvanced, + filterExclude: dialogOptions.filterExclude, + filter: dialogOptions.filterTypes + }; + } + + function initTree() { + //create the custom query string param for this tree + var params = []; + + if (dialogOptions.startNodeId) + params.push("startNodeId=" + dialogOptions.startNodeId); + + if (dialogOptions.ignoreUserStartNodes) + params.push("ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes); + + if (dialogOptions.customTreeParams) + params.push(dialogOptions.customTreeParams); + + $scope.customTreeParams = params.join('&'); + + $scope.treeReady = true; + } + + function nodeExpandedHandler(ev, args) { + + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + + //now we need to look in the already selected search results and + // toggle the check boxes for those ones that are listed + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + if (exists) { + child.selected = true; + } + }); + + //check filter + performFiltering(args.children); + } + } + + //gets the tree object when it loads + function treeLoadedHandler(ev, args) { + //args.tree contains children (args.tree.root.children) + $scope.hasItems = args.tree.root.children.length > 0; + + tree = args.tree; + + var nodeHasPath = typeof node !== "undefined" && typeof node.path !== "undefined"; + var startNodeNotDefined = typeof dialogOptions.startNodeId === "undefined" || dialogOptions.startNodeId === "" || dialogOptions.startNodeId === "-1"; + if (startNodeNotDefined && nodeHasPath) { + $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); + } + + } + + //wires up selection + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.isSearchResult) { + //check if the item selected was a search result from a list view + + //unselect + select(args.node.name, args.node.id); + + //remove it from the list view children + var listView = args.node.parent(); + listView.children = _.reject(listView.children, function (child) { + return child.id == args.node.id; + }); + + //remove it from the custom tracked search result list + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == args.node.id; + }); + } + else { + eventsService.emit("dialogs.treePickerController.select", args); + + if (args.node.filtered) { + return; + } + + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + if ($scope.model.select) { + $scope.model.select(args.node) + } else { + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + + } + } + + /** Method used for selecting a node */ + function select(text, id, entity) { + //if we get the root, we just return a constructed entity, no need for server data + if (id < 0) { + + var rootNode = { + alias: null, + icon: "icon-folder", + id: id, + name: text + }; + + if ($scope.multiPicker) { + if (entity) { + multiSelectItem(entity); + } else { + multiSelectItem(rootNode); + } + } + else { + $scope.model.selection.push(rootNode); + $scope.model.submit($scope.model); + } + } + else { + + if ($scope.multiPicker) { + + if (entity) { + multiSelectItem(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + multiSelectItem(ent); + }); + } + + } + + else { + + $scope.hideSearch(); + + //if an entity has been passed in, use it + if (entity) { + $scope.model.selection.push(entity); + $scope.model.submit($scope.model); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + $scope.model.selection.push(ent); + $scope.model.submit($scope.model); + }); + } + } + } + } + + function multiSelectItem(item) { + + var found = false; + var foundIndex = 0; + + if ($scope.model.selection.length > 0) { + for (i = 0; $scope.model.selection.length > i; i++) { + var selectedItem = $scope.model.selection[i]; + if (selectedItem.id === item.id) { + found = true; + foundIndex = i; + } + } + } + + if (found) { + $scope.model.selection.splice(foundIndex, 1); + } else { + $scope.model.selection.push(item); + } + + } + + function performFiltering(nodes) { + + if (!dialogOptions.filter) { + return; + } + + //remove any list view search nodes from being filtered since these are special nodes that always must + // be allowed to be clicked on + nodes = _.filter(nodes, function (n) { + return !angular.isObject(n.metaData.listViewNode); + }); + + if (dialogOptions.filterAdvanced) { + + //filter either based on a method or an object + var filtered = angular.isFunction(dialogOptions.filter) + ? _.filter(nodes, dialogOptions.filter) + : _.where(nodes, dialogOptions.filter); + + angular.forEach(filtered, function (value, key) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + }); + } else { + var a = dialogOptions.filterTypes.toLowerCase().replace(/\s/g, '').split(','); + angular.forEach(nodes, function (value, key) { + + var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; + + if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { + value.filtered = true; + + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + } + }); + } + } + + $scope.multiSubmit = function (result) { + entityResource.getByIds(result, $scope.entityType).then(function (ents) { + $scope.submit(ents); + }); + }; + + /** method to select a search result */ + $scope.selectResult = function (evt, result) { + + if (result.filtered) { + return; + } + + result.selected = result.selected === true ? false : true; + + //since result = an entity, we'll pass it in so we don't have to go back to the server + select(result.name, result.id, result); + + //add/remove to our custom tracked list of selected search results + if (result.selected) { + $scope.searchInfo.selectedSearchResults.push(result); + } + else { + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == result.id; + }); + } + + //ensure the tree node in the tree is checked/unchecked if it already exists there + if (tree) { + var found = treeService.getDescendantNode(tree.root, result.id); + if (found) { + found.selected = result.selected; + } + } + + }; + + $scope.hideSearch = function () { + + //Traverse the entire displayed tree and update each node to sync with the selected search results + if (tree) { + + //we need to ensure that any currently displayed nodes that get selected + // from the search get updated to have a check box! + function checkChildren(children) { + _.each(children, function (child) { + //check if the id is in the selection, if so ensure it's flagged as selected + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + //if the curr node exists in selected search results, ensure it's checked + if (exists) { + child.selected = true; + } + //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result + else if (child.metaData.isSearchResult) { + //if this tree node is under a list view it means that the node was added + // to the tree dynamically under the list view that was searched, so we actually want to remove + // it all together from the tree + var listView = child.parent(); + listView.children = _.reject(listView.children, function (c) { + return c.id == child.id; + }); + } + + //check if the current node is a list view and if so, check if there's any new results + // that need to be added as child nodes to it based on search results selected + if (child.metaData.isContainer) { + + child.cssClasses = _.reject(child.cssClasses, function (c) { + return c === 'tree-node-slide-up-hide-active'; + }); + + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + var childExists = _.find(child.children, function (c) { + return c.id == item.id; + }); + if (!childExists) { + var parent = child; + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: "icon umb-tree-icon sprTree " + item.icon, + level: child.level + 1, + metaData: { + isSearchResult: true + }, + hasChildren: false, + parent: function () { + return parent; + } + }); + } + }); + } + + //recurse + if (child.children && child.children.length > 0) { + checkChildren(child.children); + } + }); + } + checkChildren(tree.root.children); + } + + + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = dialogOptions.startNodeId; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + $scope.onSearchResults = function (results) { + + //filter all items - this will mark an item as filtered + performFiltering(results); + + //now actually remove all filtered items so they are not even displayed + results = _.filter(results, function (item) { + return !item.filtered; + }); + + $scope.searchInfo.results = results; + + //sync with the curr selected results + _.each($scope.searchInfo.results, function (result) { + var exists = _.find($scope.model.selection, function (selectedId) { + return result.id == selectedId; + }); + if (exists) { + result.selected = true; + } + }); + + $scope.searchInfo.showSearch = true; + }; + + $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + + $scope.selectListViewNode = function (node) { + select(node.name, node.id); + //toggle checked state + node.selected = node.selected === true ? false : true; + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html index a3a5fd6107..c338e3402c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html @@ -10,6 +10,7 @@ search-from-id="{{searchInfo.searchFromId}}" search-from-name="{{searchInfo.searchFromName}}" show-search="{{searchInfo.showSearch}}" + ignore-user-startnodes="{{searchInfo.ignoreUserStartNodes}}" section="{{section}}"> 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 ee8c193b4b..ce836a8d68 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 @@ -66,6 +66,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper showOpenButton: false, showEditButton: false, showPathOnHover: false, + ignoreUserStartNodes: false, maxNumber: 1, minNumber : 0, startNode: { @@ -99,7 +100,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.model.config.showOpenButton = ($scope.model.config.showOpenButton === "1" ? true : false); $scope.model.config.showEditButton = ($scope.model.config.showEditButton === "1" ? true : false); $scope.model.config.showPathOnHover = ($scope.model.config.showPathOnHover === "1" ? true : false); - + $scope.model.config.ignoreUserStartNodes = ($scope.model.config.ignoreUserStartNodes === "1" ? true : false); + var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" 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 21f9534848..a093ccb034 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,12 +1,20 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", function ($scope, $rootScope, $timeout, userService) { + + var ignoreUserStartNodes = $scope.model.config.ignoreUserStartNodes === "1" ? true : false; 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(){ @@ -14,6 +22,7 @@ angular.module("umbraco") $scope.mediaPickerOverlay.view = "mediapicker"; $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; + $scope.mediaPickerOverlay.ignoreUserStartNodes = ignoreUserStartNodes; $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 397438d5a0..cf81300b32 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -16,6 +16,7 @@ view: "linkpicker", currentTarget: currentTarget, anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes === "1", show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); @@ -26,11 +27,23 @@ } function openMediaPicker(editor, currentTarget, userData) { + var ignoreUserStartNodes = false; + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + + if ($scope.model.config.ignoreUserStartNodes === "1") { + ignoreUserStartNodes = true; + startNodeId = -1; + startNodeIsVirtual = true; + } + vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, - showDetails: true, - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], + showDetails: true, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, view: "mediapicker", show: true, submit: function(model) { 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 7b8445a4dc..1f1305b0f0 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,12 +7,19 @@ 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 = $scope.model.config.ignoreUserStartNodes === "1" ? true : false; 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; + }); + } } function setupViewModel() { @@ -105,6 +112,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl title: "Select media", startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, 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 e4f2ae303f..c7e67a0a42 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 @@ -67,10 +67,11 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en url: link.url, target: link.target } : null; - + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: target, + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes === "1", show: true, submit: function (model) { if (model.target.url || model.target.anchor) { 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 142298e0d8..6047169c54 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 @@ $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.ignoreUserStartNodes = $scope.model.config.ignoreUserStartNodes === "1" ? true: false; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { @@ -50,6 +51,7 @@ $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.ignoreUserStartNodes = $scope.model.config.ignoreUserStartNodes === "1" ? true : false; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 1fc648083e..f8dea2ee8b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -274,6 +274,7 @@ angular.module("umbraco") view: "linkpicker", currentTarget: currentTarget, anchors: editorState.current ? tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)) : [], + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes === "1", show: true, submit: function(model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); @@ -285,14 +286,24 @@ angular.module("umbraco") //Create the insert media plugin tinyMceService.createMediaPicker(editor, $scope, function(currentTarget, userData){ + var ignoreUserStartNodes = false; + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + + if ($scope.model.config.ignoreUserStartNodes === "1") { + ignoreUserStartNodes = true; + startNodeId = -1; + startNodeIsVirtual = true; + } $scope.mediaPickerOverlay = { 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, view: "mediapicker", show: true, submit: function(model) { 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 b9d63df9f3..9d7ff0cf40 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 @@ -40,4 +40,5 @@ Pixels + \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 5f3022b2a9..3d1e9d321c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1219 +1,1227 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Publishing; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Events; -using Constants = Umbraco.Core.Constants; -using umbraco.cms.businesslogic; -using System.Collections; -using umbraco; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Configures this controller with a custom action selector - /// - private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); - } - - /// - /// Updates the permissions for a content item for a particular user group - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to update - /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be alowed to set granular permissions? - - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) - .ToDictionary(x => x.UserGroupId, x => x); - - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - - //loop through each user group - foreach (var userGroup in allUserGroups) - { - //check if there's a permission set posted up for this user group - IEnumerable groupPermissions; - if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) - { - //create a string collection of the assigned letters - var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions - //for this group/node which will go back to the defaults - if (groupPermissionCodes.Length == 0) - { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored - else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) - { - //only remove them if they are actually currently assigned - if (contentPermissions.ContainsKey(userGroup.Id)) - { - //remove these permissions from this node for this group since the ones being assigned are the same as the defaults - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - } - //if they are different we need to update, otherwise there's nothing to update - else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) - { - - Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); - } - } - } - - return GetDetailedPermissions(content, allUserGroups); - } - - /// - /// Returns the user group permissions for user groups assigned to this node - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to view - /// - [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be able to see detailed permissions? - - var allUserGroups = Services.UserService.GetAllUserGroups(); - - return GetDetailedPermissions(content, allUserGroups); - } - - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) - { - //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. - - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - - var defaultPermissionsAsDictionary = defaultPermissionsByGroup - .ToDictionary(x => Convert.ToInt32(x.Id), x => x); - - //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); - - //iterate over assigned and update the defaults with the real values - foreach (var assignedGroupPermission in assignedPermissionsByGroup) - { - var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - - //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - } - - return defaultPermissionsByGroup; - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - SetupBlueprint(content, foundContent); - - return content; - } - - private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) - { - content.AllowPreview = false; - - //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - - content.AllowedActions = new[] { "A" }; - content.IsBlueprint = true; - - var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; - var propsTab = content.Tabs.Last(); - propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); - // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); - // if your user type doesn't have access to the Settings section it would not get this property mapped - if(mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(int blueprintId, int parentId) - { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); - if (blueprint == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - blueprint.Id = 0; - blueprint.Name = string.Empty; - blueprint.ParentId = parentId; - - var mapped = Mapper.Map(blueprint); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Guid id) - { - var url = Umbraco.UrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetNiceUrl(guidUdi.Guid); - } - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - string includeProperties, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children.Select(content => - Mapper.Map>(content, - opts => - { - // if there's a list of property aliases to map - we will make sure to store this in the mapping context. - if (String.IsNullOrWhiteSpace(includeProperties) == false) - { - opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); - } - })); - - return pagedResult; - } - - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - var permissions = Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds); - - var permissionsDictionary = new Dictionary(); - foreach (var nodeId in nodeIds) - { - var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); - permissionsDictionary.Add(nodeId, aggregatePerms); - } - - return permissionsDictionary; - } - - /// - /// Checks a nodes permission for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - /// - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); - if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Creates a blueprint from a content item - /// - /// The content id to copy - /// The name of the blueprint - /// - [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - - var content = Services.ContentService.GetById(contentId); - if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - EnsureUniqueName(name, content, "name"); - - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); - - Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); - - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) - ); - - return notificationModel; - } - - private void EnsureUniqueName(string name, IContent content, string modelName) - { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); - if (existing.Any(x => x.Name == name && x.Id != content.Id)) - { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); - } - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSaveBlueprint( - [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); - - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - return PostSaveInternal(contentItem, - content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) - { - //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). - if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) - { - foreach (var file in contentItem.UploadedFiles) - { - file.FileName = Path.GetFileName(file.FileName); - } - } - - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = saveMethod(contentItem.PersistedContent); - - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } - - //return the updated model - var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - contentItem.ReleaseDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentSavedText") - ); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); - break; - } - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - display.PersistedContent = contentItem.PersistedContent; - - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) - { - var found = Services.ContentService.GetBlueprintById(id); - - if (found == null) - { - return HandleContentNotFound(id, false); - } - - Services.ContentService.DeleteBlueprint(found); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); - } - - /// - /// Change the sort order for content - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - try - { - var contentService = Services.ContentService; - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - base.MapPropertyValues(contentItem); - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - expireDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText") - ); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } - - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasContentRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinContent) - ? user.HasContentBinAccess(entityService) - : user.HasPathAccess(contentItem, entityService); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Length == 0) - { - return true; - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var path = contentItem != null ? contentItem.Path : nodeId.ToString(); - var permission = userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - [EnsureUserPermissionForContent("contentId", 'F')] - public IEnumerable GetNotificationOptions(int contentId) - { - var notifications = new List(); - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var actionList = ActionsResolver.Current.Actions; - foreach (var a in actionList) - { - if (a.ShowInNotifier) - { - NotifySetting n = new NotifySetting - { - Name = ui.Text("actions", a.Alias), - Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), - NotifyCode = a.Letter.ToString() - }; - notifications.Add(n); - } - } - return notifications; - } - - public void PostNotificationOptions(int contentId, string notifyOptions = "") - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var node = new CMSNode(contentId); - - global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Binders; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Events; +using Constants = Umbraco.Core.Constants; +using umbraco.cms.businesslogic; +using System.Collections; +using umbraco; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); + } + + /// + /// Updates the permissions for a content item for a particular user group + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to update + /// + [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] + public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { + if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be alowed to set granular permissions? + + var content = Services.ContentService.GetById(saveModel.ContentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //current permissions explicitly assigned to this content item + var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + + //loop through each user group + foreach (var userGroup in allUserGroups) + { + //check if there's a permission set posted up for this user group + IEnumerable groupPermissions; + if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) + { + //create a string collection of the assigned letters + var groupPermissionCodes = groupPermissions.ToArray(); + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + //for this group/node which will go back to the defaults + if (groupPermissionCodes.Length == 0) + { + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored + else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) + { + //only remove them if they are actually currently assigned + if (contentPermissions.ContainsKey(userGroup.Id)) + { + //remove these permissions from this node for this group since the ones being assigned are the same as the defaults + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + } + //if they are different we need to update, otherwise there's nothing to update + else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) + { + + Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); + } + } + } + + return GetDetailedPermissions(content, allUserGroups); + } + + /// + /// Returns the user group permissions for user groups assigned to this node + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to view + /// + [EnsureUserPermissionForContent("contentId", 'R')] + public IEnumerable GetDetailedPermissions(int contentId) + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be able to see detailed permissions? + + var allUserGroups = Services.UserService.GetAllUserGroups(); + + return GetDetailedPermissions(content, allUserGroups); + } + + private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + { + //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); + + var defaultPermissionsAsDictionary = defaultPermissionsByGroup + .ToDictionary(x => Convert.ToInt32(x.Id), x => x); + + //get the actual assigned permissions + var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); + + //iterate over assigned and update the defaults with the real values + foreach (var assignedGroupPermission in assignedPermissionsByGroup) + { + var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; + + //clone the default permissions model to the assigned ones + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + } + + return defaultPermissionsByGroup; + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] { "A" }; + content.IsBlueprint = true; + + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + + /// + /// Gets the content json for the content id + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id, [FromUri]bool ignoreUserStartNodes = false) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); + // translate the content type name if applicable + mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if(mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId, int parentId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + blueprint.ParentId = parentId; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + string includeProperties, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children.Select(content => + Mapper.Map>(content, + opts => + { + // if there's a list of property aliases to map - we will make sure to store this in the mapping context. + if (String.IsNullOrWhiteSpace(includeProperties) == false) + { + opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); + } + })); + + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; + } + + /// + /// Checks a nodes permission for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + /// + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); + + Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("blueprints/createdBlueprintHeading"), + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint( + [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + return PostSaveInternal(contentItem, + content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) + { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = saveMethod(contentItem.PersistedContent); + + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } + + //return the updated model + var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + contentItem.ReleaseDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentSavedText") + ); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); + break; + } + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + display.PersistedContent = contentItem.PersistedContent; + + return display; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(); + + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + } + + /// + /// Change the sort order for content + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + try + { + var contentService = Services.ContentService; + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + expireDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentPublishedText") + ); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } + + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// If set to true, user and group start node permissions will be ignored. + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null, + bool ignoreUserStartNodes = false) + { + if (storage == null) throw new ArgumentNullException("storage"); + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + if(ignoreUserStartNodes) + { + return true; + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasContentRootAccess(entityService) + : (nodeId == Constants.System.RecycleBinContent) + ? user.HasContentBinAccess(entityService) + : user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + { + return true; + } + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + [EnsureUserPermissionForContent("contentId", 'F')] + public IEnumerable GetNotificationOptions(int contentId) + { + var notifications = new List(); + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var actionList = ActionsResolver.Current.Actions; + foreach (var a in actionList) + { + if (a.ShowInNotifier) + { + NotifySetting n = new NotifySetting + { + Name = ui.Text("actions", a.Alias), + Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), + NotifyCode = a.Letter.ToString() + }; + notifications.Add(n); + } + } + return notifications; + } + + public void PostNotificationOptions(int contentId, string notifyOptions = "") + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var node = new CMSNode(contentId); + + global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); + } + } +} diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 8bde435ef6..045b4e6f0d 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -82,9 +82,10 @@ namespace Umbraco.Web.Editors /// /// 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, string searchFrom = null) + public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, bool? ignoreUserStartNodes = false) { //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? @@ -92,7 +93,7 @@ namespace Umbraco.Web.Editors if (string.IsNullOrEmpty(query)) return Enumerable.Empty(); - return ExamineSearch(query, type, searchFrom); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes != null && ignoreUserStartNodes.Value); } /// @@ -534,7 +535,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -562,7 +564,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.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false) : Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); } @@ -598,9 +600,9 @@ namespace Umbraco.Web.Editors } } - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type) + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, bool ignoreUserStartNodes = false) { - return GetResultForAncestors(id, type); + return GetResultForAncestors(id, type, ignoreUserStartNodes); } public IEnumerable GetAll(UmbracoEntityTypes type, string postFilter, [FromUri]IDictionary postFilterParams) @@ -614,11 +616,12 @@ 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) { long total; - return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom); + return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom, ignoreUserStartNodes); } @@ -645,7 +648,7 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType) + private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, bool ignoreUserStartNodes = false) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) @@ -654,35 +657,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(); } return ids.Length == 0 diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e3f2365565..97e24ee6b8 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -264,11 +264,12 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { //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); @@ -321,6 +322,7 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Guid id, @@ -329,12 +331,13 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { var entity = Services.EntityService.GetByKey(id); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter, ignoreUserStartNodes); } throw new HttpResponseException(HttpStatusCode.NotFound); } @@ -349,6 +352,7 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Udi id, @@ -357,7 +361,8 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -365,7 +370,7 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.GetByKey(guidUdi.Guid); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter, ignoreUserStartNodes); } } @@ -381,7 +386,8 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { foreach (var type in new[] { typeof(int), typeof(Guid) }) { diff --git a/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs index 5b99264113..0039385e5f 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs @@ -15,10 +15,11 @@ namespace Umbraco.Web.PropertyEditors { InternalPreValues = new Dictionary { - {"startNodeId", "-1"}, + {"startNodeId", "-1"}, {"showOpenButton", "0"}, {"showEditButton", "0"}, {"showPathOnHover", "0"}, + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -39,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors { public ContentPickerPreValueEditor() { - //create the fields + //create the fields Fields.Add(new PreValueField() { Key = "showOpenButton", @@ -48,6 +49,13 @@ namespace Umbraco.Web.PropertyEditors Description = "Opens the node in a dialog" }); Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNodeId", View = "treepicker", @@ -60,4 +68,4 @@ namespace Umbraco.Web.PropertyEditors } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index af30b4ceeb..4a8803a099 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -136,6 +136,9 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public string Rte { get; set; } + + [PreValueField("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; } } #region Application event handler, used to bind to events on startup diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs index 2d9ee68b3b..6aa7ab4a54 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs @@ -57,6 +57,13 @@ namespace Umbraco.Web.PropertyEditors Description = "Do not allow folders to be picked." }); Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNodeId", View = "mediapicker", @@ -69,4 +76,4 @@ namespace Umbraco.Web.PropertyEditors } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs index f43ecd48be..f57a9951b6 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.PropertyEditors {"showOpenButton", "0"}, {"showEditButton", "0"}, {"showPathOnHover", "0"}, + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -38,6 +39,13 @@ namespace Umbraco.Web.PropertyEditors { //create the fields Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNode", View = "treesource", @@ -118,4 +126,4 @@ namespace Umbraco.Web.PropertyEditors } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index a058a16b8a..f3ae317efa 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -32,6 +32,13 @@ namespace Umbraco.Web.PropertyEditors { public MultiUrlPickerPreValueEditor() { + Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); Fields.Add(new PreValueField { Key = "minNumber", diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs index 541dccaa4e..a96c0724ff 100644 --- a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.PropertyEditors { InternalPreValues = new Dictionary { + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -32,8 +33,11 @@ namespace Umbraco.Web.PropertyEditors internal class RelatedLinksPreValueEditor : PreValueEditor { + [PreValueField("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; } + [PreValueField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")] - public int Maximum { get; set; } + public int Maximum { get; set; } } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs index cd08a8ad45..69445bc304 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs @@ -23,6 +23,14 @@ namespace Umbraco.Web.PropertyEditors Key = "editor" }); + Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Name = "Hide Label", diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 38347de9ed..9714c6b946 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -27,13 +27,14 @@ namespace Umbraco.Web.Search /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// public IEnumerable ExamineSearch( UmbracoHelper umbracoHelper, string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, out long totalFound, string searchFrom = null) + long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); @@ -61,12 +62,12 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(appContext.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, appContext.Services.EntityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, appContext.Services.EntityService); break; case UmbracoEntityTypes.Document: type = "content"; var allContentStartNodes = umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(appContext.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, appContext.Services.EntityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, appContext.Services.EntityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -203,7 +204,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("sb"); if (entityService == null) throw new ArgumentNullException("entityService"); @@ -228,7 +229,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 af38e21546..2624c89b56 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; } @@ -91,7 +91,7 @@ namespace Umbraco.Web.Trees { bool hasPathAccess; var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); - if (entityIsAncestorOfStartNodes == false) + if (IgnoreUserStartNodes(queryStrings) == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); @@ -101,7 +101,7 @@ namespace Umbraco.Web.Trees //the node so we need to return null; return null; } - if (hasPathAccess == false) + if (IgnoreUserStartNodes(queryStrings) == false && hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; } @@ -141,7 +141,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) { LogHelper.Warn("User " + Security.CurrentUser.Username + " does not have access to node with id " + id); return nodes; @@ -158,7 +158,7 @@ namespace Umbraco.Web.Trees // get child entities - if id is root, but user's start nodes do not contain the // root node, this returns the start nodes instead of root's children - var entities = GetChildEntities(id).ToList(); + var entities = GetChildEntities(id, queryStrings).ToList(); nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); // if the user does not have access to the root node, what we have is the start nodes, @@ -190,7 +190,7 @@ namespace Umbraco.Web.Trees protected abstract UmbracoObjectTypes UmbracoObjectType { get; } - protected IEnumerable GetChildEntities(string id) + protected IEnumerable GetChildEntities(string id, FormDataCollection queryStrings) { // try to parse id as an integer else use GetEntityFromId // which will grok Guids, Udis, etc and let use obtain the id diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 98eb2ec8bc..2ea41ff128 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -349,6 +349,16 @@ namespace Umbraco.Web.Trees return queryStrings.GetValue(TreeQueryStringParameters.IsDialog); } + /// + /// 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 7f6fa28187..c79b9f8781 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,7 +8,8 @@ public const string IsDialog = "isDialog"; 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"; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 18880b9f96..64fe9a4b65 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -52,7 +52,7 @@ namespace Umbraco.Web.WebApi.Filters _paramName = paramName; _permissionToCheck = ActionBrowse.Instance.Letter; - } + } public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) : this(paramName) @@ -72,6 +72,9 @@ namespace Umbraco.Web.WebApi.Filters //not logged in throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); } + + var ignoreUserStartNodes = actionContext.ActionArguments.ContainsKey("ignoreUserStartNodes") && + bool.Parse(actionContext.ActionArguments.GetValueAsString("ignoreUserStartNodes")); int nodeId; if (_nodeId.HasValue == false) @@ -124,9 +127,11 @@ namespace Umbraco.Web.WebApi.Filters actionContext.Request.Properties, UmbracoContext.Current.Security.CurrentUser, ApplicationContext.Current.Services.UserService, - ApplicationContext.Current.Services.ContentService, - ApplicationContext.Current.Services.EntityService, - nodeId, _permissionToCheck.HasValue ? new[]{_permissionToCheck.Value}: null)) + ApplicationContext.Current.Services.ContentService, + ApplicationContext.Current.Services.EntityService, + nodeId, + _permissionToCheck.HasValue ? new[]{_permissionToCheck.Value}: null, + ignoreUserStartNodes: ignoreUserStartNodes)) { base.OnActionExecuting(actionContext); } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index beb67f3395..23d4fb871c 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -3,11 +3,13 @@ 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.Web.Models.ContentEditing; +using Umbraco.Web.Trees; namespace Umbraco.Web.WebApi.Filters { @@ -77,7 +79,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) From d51daa2a8a8a4f372e91314b9b35ead6a0af666a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 13:25:55 +0100 Subject: [PATCH 02/47] Make sure the correct user is registered in the log when emptying the recycle bin --- src/Umbraco.Core/Services/ContentService.cs | 12 ++++++++++-- src/Umbraco.Core/Services/IContentService.cs | 8 ++++++++ src/Umbraco.Web/Editors/ContentController.cs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ba777ef59e..6e4fd54de5 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1741,7 +1741,15 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public void EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) { using (new WriteLock(Locker)) { @@ -1771,7 +1779,7 @@ namespace Umbraco.Core.Services recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", userId, Constants.System.RecycleBinContent); uow.Commit(); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6d0ca051a1..0587e70548 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -438,8 +438,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")] void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + /// /// Rollback an object to a previous version. /// This will create a new version, which is a copy of all the old data. diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 5f3022b2a9..beff2c2615 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -850,7 +850,7 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] public HttpResponseMessage EmptyRecycleBin() { - Services.ContentService.EmptyRecycleBin(); + Services.ContentService.EmptyRecycleBin(Security.CurrentUser.Id); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } From 011d406301a4e635cdba9c1bbc1440128c791bb9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 25 Mar 2019 11:59:41 +0100 Subject: [PATCH 03/47] https://github.com/umbraco/Umbraco-CMS/issues/5019 - Changed log level from error to debug. This will spam the log if we request with invalid parameters --- src/Umbraco.Core/UriExtensions.cs | 646 +++++++++++++++--------------- 1 file changed, 323 insertions(+), 323 deletions(-) diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index dd3ae8c10c..2d9d644514 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -1,323 +1,323 @@ -using System; -using System.IO; -using System.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; - -namespace Umbraco.Core -{ - /// - /// Provides extension methods to . - /// - public static class UriExtensions - { - /// - /// Checks if the current uri is a back office request - /// - /// - /// - /// The current application path or VirtualPath - /// - /// - /// - /// There are some special routes we need to check to properly determine this: - /// - /// If any route has an extension in the path like .aspx = back office - /// - /// These are def back office: - /// /Umbraco/RestServices = back office - /// /Umbraco/BackOffice = back office - /// If it's not any of the above, and there's no extension then we cannot determine if it's back office or front-end - /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice - /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. - /// - /// These are def front-end: - /// /Umbraco/Surface = front-end - /// /Umbraco/Api = front-end - /// But if we've got this far we'll just have to assume it's front-end anyways. - /// - /// - internal static bool IsBackOfficeRequest(this Uri url, string applicationPath) - { - applicationPath = applicationPath ?? string.Empty; - - var fullUrlPath = url.AbsolutePath.TrimStart(new[] {'/'}); - var appPath = applicationPath.TrimStart(new[] {'/'}); - var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); - - //check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); - //if not, then def not back office - if (isUmbracoPath == false) return false; - - //if its the normal /umbraco path - if (urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea) - || urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea + "/")) - { - return true; - } - - //check for a file extension - var extension = Path.GetExtension(url.LocalPath); - //has an extension, def back office - if (extension.IsNullOrWhiteSpace() == false) return true; - //check for special case asp.net calls like: - // /umbraco/webservices/legacyAjaxCalls.asmx/js which will return a null file extension but are still considered extension'd requests - if (urlPath.InvariantContains(".asmx/") - || urlPath.InvariantContains(".aspx/") - || urlPath.InvariantContains(".ashx/") - || urlPath.InvariantContains(".axd/") - || urlPath.InvariantContains(".svc/")) - { - return true; - } - - //check for special back office paths - if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/BackOffice/") - || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/RestServices/")) - { - return true; - } - - //check for special front-end paths - if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Surface/") - || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Api/")) - { - return false; - } - - //if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by - // checking how many parts the route has, for example, all PluginController routes will be routed like - // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} - // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a - // plugin controller for the front-end. - if (urlPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).Length >= 3) - { - return false; - } - - //if its anything else we can assume it's back office - return true; - } - - /// - /// Checks if the current uri is an install request - /// - /// - /// - internal static bool IsInstallerRequest(this Uri url) - { - var authority = url.GetLeftPart(UriPartial.Authority); - var afterAuthority = url.GetLeftPart(UriPartial.Query) - .TrimStart(authority) - .TrimStart("/"); - - //check if this is in the umbraco back office - return afterAuthority.InvariantStartsWith(IOHelper.ResolveUrl("~/install").TrimStart("/")); - } - - /// - /// Checks if the uri is a request for the default back office page - /// - /// - /// - internal static bool IsDefaultBackOfficeRequest(this Uri url) - { - if (url.AbsolutePath.InvariantEquals(GlobalSettings.Path.TrimEnd("/")) - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/')) - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default") - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default/")) - { - return true; - } - return false; - } - - /// - /// This is a performance tweak to check if this not an ASP.Net server file - /// .Net will pass these requests through to the module when in integrated mode. - /// We want to ignore all of these requests immediately. - /// - /// - /// - internal static bool IsClientSideRequest(this Uri url) - { - try - { - var ext = Path.GetExtension(url.LocalPath); - if (ext.IsNullOrWhiteSpace()) return false; - var toInclude = new[] {".aspx", ".ashx", ".asmx", ".axd", ".svc"}; - return toInclude.Any(ext.InvariantEquals) == false; - } - catch (ArgumentException ex) - { - LogHelper.Error(typeof(UriExtensions), "Failed to determine if request was client side", ex); - return false; - } - } - - /// - /// Rewrites the path of uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) - : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); - } - - /// - /// Rewrites the path and query of a uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The new query, which must be empty or begin with a question mark. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path, string query) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - if (query.Length > 0 && query.StartsWith("?") == false) - throw new ArgumentException("Query must start with a question mark.", "query"); - if (query == "?") - query = ""; - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) - : new Uri(path + query, UriKind.Relative); - } - - /// - /// Gets the absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePath(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.AbsolutePath; - - // cannot get .AbsolutePath on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); - var path = pos > 0 ? s.Substring(0, pos) : s; - return path; - } - - /// - /// Gets the decoded, absolute path of the uri. - /// - /// The uri. - /// The absolute path of the uri. - /// Only for absolute uris. - public static string GetAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); - } - - /// - /// Gets the decoded, absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.GetSafeAbsolutePath()); - } - - /// - /// Rewrites the path of the uri so it ends with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri EndPathWithSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); - return uri; - } - - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(path + "/" + uri.Query, UriKind.Relative); - - return uri; - } - - /// - /// Rewrites the path of the uri so it does not end with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri TrimPathEndSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd('/') + uri.Query); - } - else - { - if (path != "/") - uri = new Uri(path.TrimEnd('/') + uri.Query, UriKind.Relative); - } - return uri; - } - - /// - /// Transforms a relative uri into an absolute uri. - /// - /// The relative uri. - /// The base absolute uri. - /// The absolute uri. - public static Uri MakeAbsolute(this Uri uri, Uri baseUri) - { - if (uri.IsAbsoluteUri) - throw new ArgumentException("Uri is already absolute.", "uri"); - - return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); - } - - static string GetSafeQuery(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.Query; - - // cannot get .Query on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); - - return query; - } - - /// - /// Removes the port from the uri. - /// - /// The uri. - /// The same uri, without its port. - public static Uri WithoutPort(this Uri uri) - { - return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); - } - } -} +using System; +using System.IO; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods to . + /// + public static class UriExtensions + { + /// + /// Checks if the current uri is a back office request + /// + /// + /// + /// The current application path or VirtualPath + /// + /// + /// + /// There are some special routes we need to check to properly determine this: + /// + /// If any route has an extension in the path like .aspx = back office + /// + /// These are def back office: + /// /Umbraco/RestServices = back office + /// /Umbraco/BackOffice = back office + /// If it's not any of the above, and there's no extension then we cannot determine if it's back office or front-end + /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice + /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. + /// + /// These are def front-end: + /// /Umbraco/Surface = front-end + /// /Umbraco/Api = front-end + /// But if we've got this far we'll just have to assume it's front-end anyways. + /// + /// + internal static bool IsBackOfficeRequest(this Uri url, string applicationPath) + { + applicationPath = applicationPath ?? string.Empty; + + var fullUrlPath = url.AbsolutePath.TrimStart(new[] {'/'}); + var appPath = applicationPath.TrimStart(new[] {'/'}); + var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); + + //check if this is in the umbraco back office + var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); + //if not, then def not back office + if (isUmbracoPath == false) return false; + + //if its the normal /umbraco path + if (urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea) + || urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea + "/")) + { + return true; + } + + //check for a file extension + var extension = Path.GetExtension(url.LocalPath); + //has an extension, def back office + if (extension.IsNullOrWhiteSpace() == false) return true; + //check for special case asp.net calls like: + // /umbraco/webservices/legacyAjaxCalls.asmx/js which will return a null file extension but are still considered extension'd requests + if (urlPath.InvariantContains(".asmx/") + || urlPath.InvariantContains(".aspx/") + || urlPath.InvariantContains(".ashx/") + || urlPath.InvariantContains(".axd/") + || urlPath.InvariantContains(".svc/")) + { + return true; + } + + //check for special back office paths + if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/BackOffice/") + || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/RestServices/")) + { + return true; + } + + //check for special front-end paths + if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Surface/") + || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Api/")) + { + return false; + } + + //if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by + // checking how many parts the route has, for example, all PluginController routes will be routed like + // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} + // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a + // plugin controller for the front-end. + if (urlPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).Length >= 3) + { + return false; + } + + //if its anything else we can assume it's back office + return true; + } + + /// + /// Checks if the current uri is an install request + /// + /// + /// + internal static bool IsInstallerRequest(this Uri url) + { + var authority = url.GetLeftPart(UriPartial.Authority); + var afterAuthority = url.GetLeftPart(UriPartial.Query) + .TrimStart(authority) + .TrimStart("/"); + + //check if this is in the umbraco back office + return afterAuthority.InvariantStartsWith(IOHelper.ResolveUrl("~/install").TrimStart("/")); + } + + /// + /// Checks if the uri is a request for the default back office page + /// + /// + /// + internal static bool IsDefaultBackOfficeRequest(this Uri url) + { + if (url.AbsolutePath.InvariantEquals(GlobalSettings.Path.TrimEnd("/")) + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/')) + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default") + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default/")) + { + return true; + } + return false; + } + + /// + /// This is a performance tweak to check if this not an ASP.Net server file + /// .Net will pass these requests through to the module when in integrated mode. + /// We want to ignore all of these requests immediately. + /// + /// + /// + internal static bool IsClientSideRequest(this Uri url) + { + try + { + var ext = Path.GetExtension(url.LocalPath); + if (ext.IsNullOrWhiteSpace()) return false; + var toInclude = new[] {".aspx", ".ashx", ".asmx", ".axd", ".svc"}; + return toInclude.Any(ext.InvariantEquals) == false; + } + catch (ArgumentException) + { + LogHelper.Debug(typeof(UriExtensions), "Failed to determine if request was client side. Due to invalid characters in path: {0}", () => url.LocalPath); + return false; + } + } + + /// + /// Rewrites the path of uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path) + { + if (path.StartsWith("/") == false) + throw new ArgumentException("Path must start with a slash.", "path"); + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) + : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); + } + + /// + /// Rewrites the path and query of a uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The new query, which must be empty or begin with a question mark. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path, string query) + { + if (path.StartsWith("/") == false) + throw new ArgumentException("Path must start with a slash.", "path"); + if (query.Length > 0 && query.StartsWith("?") == false) + throw new ArgumentException("Query must start with a question mark.", "query"); + if (query == "?") + query = ""; + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) + : new Uri(path + query, UriKind.Relative); + } + + /// + /// Gets the absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePath(this Uri uri) + { + if (uri.IsAbsoluteUri) + return uri.AbsolutePath; + + // cannot get .AbsolutePath on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); + var path = pos > 0 ? s.Substring(0, pos) : s; + return path; + } + + /// + /// Gets the decoded, absolute path of the uri. + /// + /// The uri. + /// The absolute path of the uri. + /// Only for absolute uris. + public static string GetAbsolutePathDecoded(this Uri uri) + { + return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); + } + + /// + /// Gets the decoded, absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePathDecoded(this Uri uri) + { + return System.Web.HttpUtility.UrlDecode(uri.GetSafeAbsolutePath()); + } + + /// + /// Rewrites the path of the uri so it ends with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri EndPathWithSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/" && path.EndsWith("/") == false) + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); + return uri; + } + + if (path != "/" && path.EndsWith("/") == false) + uri = new Uri(path + "/" + uri.Query, UriKind.Relative); + + return uri; + } + + /// + /// Rewrites the path of the uri so it does not end with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri TrimPathEndSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/") + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd('/') + uri.Query); + } + else + { + if (path != "/") + uri = new Uri(path.TrimEnd('/') + uri.Query, UriKind.Relative); + } + return uri; + } + + /// + /// Transforms a relative uri into an absolute uri. + /// + /// The relative uri. + /// The base absolute uri. + /// The absolute uri. + public static Uri MakeAbsolute(this Uri uri, Uri baseUri) + { + if (uri.IsAbsoluteUri) + throw new ArgumentException("Uri is already absolute.", "uri"); + + return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); + } + + static string GetSafeQuery(this Uri uri) + { + if (uri.IsAbsoluteUri) + return uri.Query; + + // cannot get .Query on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); + + return query; + } + + /// + /// Removes the port from the uri. + /// + /// The uri. + /// The same uri, without its port. + public static Uri WithoutPort(this Uri uri) + { + return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); + } + } +} From ff9179a15f10a8f2efe84fc325ee8834954ec222 Mon Sep 17 00:00:00 2001 From: Dave de Moel Date: Wed, 27 Mar 2019 10:33:21 +0100 Subject: [PATCH 04/47] Changed check to use localeCompare --- .../src/common/services/tree.service.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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..a593e24bfe 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 @@ -165,8 +165,9 @@ 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) { + // localeCompare returns 0 when strings are equal so we need to add one to make it true. + return item.alias.localeCompare(treeAlias, undefined, { ignorePunctuation: true }) + 1; }); return found ? found.packageFolder : undefined; From bb43fc310aaae9a42a4230c54f32f4ca9e09203c Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 27 Mar 2019 17:11:21 +0100 Subject: [PATCH 05/47] Add service-level loading of preview Xml document --- .../Repositories/ContentRepository.cs | 69 +++++++++++++++++-- .../Interfaces/IContentRepository.cs | 4 +- src/Umbraco.Core/Services/ContentService.cs | 11 +++ src/Umbraco.Core/Services/IContentService.cs | 2 + 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 4f19dc9241..be5cc20cb6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -199,7 +199,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -468,7 +468,7 @@ namespace Umbraco.Core.Persistence.Repositories entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set entity.Path = nodeDto.Path; entity.SortOrder = sortOrder; - entity.Level = level; + entity.Level = level; //Create the Content specific data - cmsContent var contentDto = dto.ContentVersionDto.ContentDto; @@ -644,7 +644,7 @@ namespace Umbraco.Core.Persistence.Repositories contentVersionDto.Id = contentVerDto.Id; Database.Update(contentVersionDto); } - + Database.Update(dto); } @@ -752,7 +752,7 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetBlueprints(IQuery query) { Func, Sql> translate = t => t.Translate(); - + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); var translatorFull = new SqlTranslator(sqlFull, query); var sqlIds = GetBaseQuery(BaseQueryType.Ids); @@ -835,6 +835,61 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", } + public XmlDocument BuildPreviewXmlCache() + { + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + public int CountPublished(string contentTypeAlias = null) { if (contentTypeAlias.IsNullOrWhiteSpace()) @@ -868,9 +923,9 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", /// /// /// - /// + /// public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { + { _permissionRepository.AssignEntityPermission(entity, permission, groupIds); } @@ -882,7 +937,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", public EntityPermissionCollection GetPermissionsForEntity(int entityId) { return _permissionRepository.GetPermissionsForEntity(entityId); - } + } /// /// Adds/updates content/published xml diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 5edd73f760..689c12efc6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -19,6 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories /// XmlDocument BuildXmlCache(); + XmlDocument BuildPreviewXmlCache(); + /// /// Get the count of published items /// @@ -96,4 +98,4 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdatePreviewXml(IContent content, Func xml); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ba777ef59e..2fe1bb4d5b 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2192,6 +2192,17 @@ namespace Umbraco.Core.Services } } + public XmlDocument BuildPreviewXmlCache() + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var result = repository.BuildPreviewXmlCache(); + uow.Commit(); + return result; + } + } + /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6d0ca051a1..7512064ed3 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -124,6 +124,8 @@ namespace Umbraco.Core.Services /// XmlDocument BuildXmlCache(); + XmlDocument BuildPreviewXmlCache(); + /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// From 0676726bfcf2343e729af869420975ab9242701b Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 28 Mar 2019 08:42:20 +0100 Subject: [PATCH 06/47] Refactor preview --- .../Cache/UnpublishedPageCacheRefresher.cs | 17 +- .../PublishedContentCache.cs | 2 + .../umbraco.presentation/content.cs | 207 ++++++++++++++++++ .../umbraco/preview/PreviewContent.cs | 76 ++++++- 4 files changed, 292 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index 6404d60b40..bf58454bfc 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Models; using System.Linq; using Newtonsoft.Json; +using umbraco.cms.businesslogic.web; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Sync; @@ -44,7 +45,7 @@ namespace Umbraco.Web.Cache return jsonObject; } - + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds) { var items = contentIds.Select(x => new JsonPayload @@ -61,12 +62,12 @@ namespace Umbraco.Web.Cache #region Sub classes internal enum OperationType - { + { Deleted } internal class JsonPayload - { + { public int Id { get; set; } public OperationType Operation { get; set; } } @@ -79,6 +80,7 @@ namespace Umbraco.Web.Cache ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(); base.RefreshAll(); } @@ -87,6 +89,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); + var d = new Document(id); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(id); } @@ -97,6 +102,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(id); base.Remove(id); } @@ -106,6 +112,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); + var d = new Document(instance); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(instance); } @@ -116,6 +125,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(instance.Id); base.Remove(instance); } @@ -132,6 +142,7 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); ClearRepositoryCacheItemById(payload.Id); content.Instance.UpdateSortOrder(payload.Id); + content.Instance.ClearPreviewXmlContent(payload.Id); } DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 59772e8237..e9342b6136 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -523,6 +523,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (preview) { + if (PreviewContent.IsSinglePreview) + return content.Instance.PreviewXmlContent; var previewContent = PreviewContentCache.GetOrCreateValue(context); // will use the ctor with no parameters previewContent.EnsureInitialized(context.UmbracoUser, StateHelper.Cookies.Preview.GetValue(), true, () => { diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ccd2e009a7..deb3f0be17 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; +using System.Threading; using System.Web; using System.Xml; using umbraco.BusinessLogic; @@ -13,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.web; using umbraco.DataLayer; using umbraco.presentation.nodeFactory; +using umbraco.presentation.preview; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -275,6 +277,8 @@ namespace umbraco safeXml.AcceptChanges(); } + + ClearPreviewXmlContent(); } /// @@ -474,6 +478,8 @@ namespace umbraco safeXml.AcceptChanges(); } } + + ClearPreviewXmlContent(id); } /// @@ -668,6 +674,21 @@ namespace umbraco _persisterTask = _persisterTask.Touch(); // _persisterTask != null because SyncToXmlFile == true } + private static bool HasSchema(string contentTypeAlias, XmlDocument xml) + { + string subset = null; + + // get current doctype + var n = xml.FirstChild; + while (n.NodeType != XmlNodeType.DocumentType && n.NextSibling != null) + n = n.NextSibling; + if (n.NodeType == XmlNodeType.DocumentType) + subset = ((XmlDocumentType)n).InternalSubset; + + // ensure it contains the content type + return subset != null && subset.Contains(string.Format("", contentTypeAlias)); + } + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -1260,5 +1281,191 @@ namespace umbraco } #endregion + + #region Preview + + private const string PreviewCacheKey = "umbraco.content.preview"; + + internal void ClearPreviewXmlContent() + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + runtimeCache.ClearCacheItem(PreviewCacheKey); + } + + internal void ClearPreviewXmlContent(int id) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // Check if node present, before cloning + var x = xml.GetElementById(id.ToString()); + if (x == null) + return; + + // Find the document in the xml cache + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + } + + internal void UpdatePreviewXmlContent(Document d) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + var pnode = GetPreviewOrPublishedNode(d, xml, true); + var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); + pattr.Value = d.sortOrder.ToString(); + AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + } + + private void AddOrUpdatePreviewXmlNode(int id, int level, int parentId, XmlNode docNode) + { + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // sanity checks + if (id != docNode.AttributeValue("id")) + throw new ArgumentException("Values of id and docNode/@id are different."); + if (parentId != docNode.AttributeValue("parentID")) + throw new ArgumentException("Values of parentId and docNode/@parentID are different."); + + // find the document in the cache + XmlNode currentNode = xml.GetElementById(id.ToInvariantString()); + + // if the document is not there already then it's a new document + // we must make sure that its document type exists in the schema + if (currentNode == null && UseLegacySchema == false) + { + if (HasSchema(docNode.Name, xml) == false) + { + runtimeCache.ClearCacheItem(PreviewCacheKey); + return; + } + } + + // find the parent + XmlNode parentNode = level == 1 + ? xml.DocumentElement + : xml.GetElementById(parentId.ToInvariantString()); + + // no parent = cannot do anything + if (parentNode == null) + return; + + // insert/move the node under the parent + if (currentNode == null) + { + // document not there, new node, append + currentNode = docNode; + parentNode.AppendChild(currentNode); + } + else + { + // document found... we could just copy the currentNode children nodes over under + // docNode, then remove currentNode and insert docNode... the code below tries to + // be clever and faster, though only benchmarking could tell whether it's worth the + // pain... + + // first copy current parent ID - so we can compare with target parent + var moving = currentNode.AttributeValue("parentID") != parentId; + + if (docNode.Name == currentNode.Name) + { + // name has not changed, safe to just update the current node + // by transfering values eg copying the attributes, and importing the data elements + TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode); + + // if moving, move the node to the new parent + // else it's already under the right parent + // (but maybe the sort order has been updated) + if (moving) + parentNode.AppendChild(currentNode); // remove then append to parentNode + } + else + { + // name has changed, must use docNode (with new name) + // move children nodes from currentNode to docNode (already has properties) + var children = currentNode.SelectNodes(ChildNodesXPath); + if (children == null) throw new Exception("oops"); + foreach (XmlNode child in children) + docNode.AppendChild(child); // remove then append to docNode + + // and put docNode in the right place - if parent has not changed, then + // just replace, else remove currentNode and insert docNode under the right parent + // (but maybe not at the right position due to sort order) + if (moving) + { + if (currentNode.ParentNode == null) throw new Exception("oops"); + currentNode.ParentNode.RemoveChild(currentNode); + parentNode.AppendChild(docNode); + } + else + { + // replacing might screw the sort order + parentNode.ReplaceChild(docNode, currentNode); + } + + currentNode = docNode; + } + } + + // if the nodes are not ordered, must sort + // (see U4-509 + has to work with ReplaceChild too) + //XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue("sortOrder")); + + // but... + // if we assume that nodes are always correctly sorted + // then we just need to ensure that currentNode is at the right position. + // should be faster that moving all the nodes around. + XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + } + + // UpdateSortOrder is meant to update the Xml cache sort order on Save, 'cos that change + // should be applied immediately, even though the Xml cache is not updated on Saves - we + // don't have to do it for preview Xml since it is always fully updated - OTOH we have + // to ensure it *is* updated, in UnpublishedPageCacheRefresher + + private XmlDocument LoadPreviewXmlContent() + { + try + { + LogHelper.Info("Loading preview content from database..."); + var xml = ApplicationContext.Current.Services.ContentService.BuildPreviewXmlCache(); + LogHelper.Debug("Done loading preview content"); + return xml; + } + catch (Exception ee) + { + LogHelper.Error("Error loading preview content", ee); + } + + // An error of some sort must have stopped us from successfully generating + // the content tree, so lets return null signifying there is no content available + return null; + } + + public XmlDocument PreviewXmlContent + { + get + { + if (PreviewContent.IsSinglePreview == false) + throw new InvalidOperationException(); + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, + removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + } + } + + #endregion } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 3346fa608c..815db997f0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Globalization; using System.IO; using System.Xml; @@ -11,11 +12,57 @@ using Umbraco.Core.Logging; namespace umbraco.presentation.preview { - //TODO : Migrate this to a new API! + public enum PreviewMode + { + Unknown = 0, // default value + Vintage, + SinglePreview + } public class PreviewContent { - // zb-00004 #29956 : refactor cookies names & handling + private static PreviewMode _previewMode; + private const PreviewMode DefaultPreviewMode = PreviewMode.SinglePreview; + private static int _singlePreviewCacheDurationSeconds = -1; + private const int DefaultSinglePreviewCacheDurationSeconds = 60; + + public static PreviewMode PreviewMode + { + get + { + if (_previewMode != PreviewMode.Unknown) + return _previewMode; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.Mode"]; + if (setting.IsNullOrWhiteSpace()) + return _previewMode = DefaultPreviewMode; + if (Enum.TryParse(setting, false, out _previewMode)) + return _previewMode; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.Mode appSetting, {setting} is not a valid value. " + + "Valid values are: Vintage (default), SinglePreview."); + } + } + + public static int SinglePreviewCacheDurationSeconds + { + get + { + if (_singlePreviewCacheDurationSeconds >= 0) + return _singlePreviewCacheDurationSeconds; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.SinglePreview.CacheDurationSeconds"]; + if (setting.IsNullOrWhiteSpace()) + return _singlePreviewCacheDurationSeconds = DefaultSinglePreviewCacheDurationSeconds; + if (int.TryParse(setting, out _singlePreviewCacheDurationSeconds)) + return _singlePreviewCacheDurationSeconds; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.SinglePreview.CacheDurationSeconds appSetting, {setting} is not a valid value. " + + "Valid values are positive integers."); + } + } + + public static bool IsSinglePreview => PreviewMode == PreviewMode.SinglePreview; public XmlDocument XmlContent { get; set; } public Guid PreviewSet { get; set; } @@ -35,6 +82,8 @@ namespace umbraco.presentation.preview public void EnsureInitialized(User user, string previewSet, bool validate, Action initialize) { + if (IsSinglePreview) return; + lock (_initLock) { if (_initialized) return; @@ -53,17 +102,19 @@ namespace umbraco.presentation.preview public PreviewContent(Guid previewSet) { - ValidPreviewSet = UpdatePreviewPaths(previewSet, true); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, true); } public PreviewContent(User user, Guid previewSet, bool validate) { _userId = user.Id; - ValidPreviewSet = UpdatePreviewPaths(previewSet, validate); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, validate); } public void PrepareDocument(User user, Document documentObject, bool includeSubs) { + if (IsSinglePreview) return; + _userId = user.Id; // clone xml @@ -144,6 +195,8 @@ namespace umbraco.presentation.preview /// public bool ValidatePreviewPath() { + if (IsSinglePreview) return true; + if (!File.Exists(PreviewsetPath)) return false; @@ -154,12 +207,21 @@ namespace umbraco.presentation.preview public void LoadPreviewset() { - XmlContent = new XmlDocument(); - XmlContent.Load(PreviewsetPath); + if (IsSinglePreview) + { + XmlContent = content.Instance.PreviewXmlContent; + } + else + { + XmlContent = new XmlDocument(); + XmlContent.Load(PreviewsetPath); + } } public void SavePreviewSet() { + if (IsSinglePreview) return; + //make sure the preview folder exists first var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview)); if (!dir.Exists) @@ -211,7 +273,7 @@ namespace umbraco.presentation.preview public static void ClearPreviewCookie() { // zb-00004 #29956 : refactor cookies names & handling - if (UmbracoContext.Current.UmbracoUser != null) + if (!IsSinglePreview && UmbracoContext.Current.UmbracoUser != null) { if (StateHelper.Cookies.Preview.HasValue) { From 9bcd70589c82ee1c880e91a42fd753d211c22865 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 1 Apr 2019 08:21:01 +0200 Subject: [PATCH 07/47] Alt. preview: thread safety --- .../umbraco.presentation/content.cs | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index deb3f0be17..484014656c 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1285,13 +1285,17 @@ namespace umbraco #region Preview private const string PreviewCacheKey = "umbraco.content.preview"; + private readonly object _previewLock = new object(); internal void ClearPreviewXmlContent() { if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - runtimeCache.ClearCacheItem(PreviewCacheKey); + lock (_previewLock) + { + runtimeCache.ClearCacheItem(PreviewCacheKey); + } } internal void ClearPreviewXmlContent(int id) @@ -1299,17 +1303,21 @@ namespace umbraco if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var xml = runtimeCache.GetCacheItem(PreviewCacheKey); - if (xml == null) return; - // Check if node present, before cloning - var x = xml.GetElementById(id.ToString()); - if (x == null) - return; + lock (_previewLock) + { + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; - // Find the document in the xml cache - // The document already exists in cache, so repopulate it - x.ParentNode.RemoveChild(x); + // Check if node present, before cloning + var x = xml.GetElementById(id.ToString()); + if (x == null) + return; + + // Find the document in the xml cache + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + } } internal void UpdatePreviewXmlContent(Document d) @@ -1317,13 +1325,17 @@ namespace umbraco if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var xml = runtimeCache.GetCacheItem(PreviewCacheKey); - if (xml == null) return; - var pnode = GetPreviewOrPublishedNode(d, xml, true); - var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); - pattr.Value = d.sortOrder.ToString(); - AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + lock (_previewLock) + { + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + var pnode = GetPreviewOrPublishedNode(d, xml, true); + var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); + pattr.Value = d.sortOrder.ToString(); + AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + } } private void AddOrUpdatePreviewXmlNode(int id, int level, int parentId, XmlNode docNode) @@ -1461,8 +1473,12 @@ namespace umbraco throw new InvalidOperationException(); var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, - removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + + lock (_previewLock) + { + return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, + removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + } } } From e6d75f27de0866f7188d58aefe3c8c005f8d8c86 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 1 Apr 2019 08:23:48 +0200 Subject: [PATCH 08/47] Alt. preview: rename modes --- .../umbraco/preview/PreviewContent.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 815db997f0..c8e1ac145c 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -15,14 +15,14 @@ namespace umbraco.presentation.preview public enum PreviewMode { Unknown = 0, // default value - Vintage, - SinglePreview + Legacy, + Default } public class PreviewContent { private static PreviewMode _previewMode; - private const PreviewMode DefaultPreviewMode = PreviewMode.SinglePreview; + private const PreviewMode DefaultPreviewMode = PreviewMode.Default; private static int _singlePreviewCacheDurationSeconds = -1; private const int DefaultSinglePreviewCacheDurationSeconds = 60; @@ -62,7 +62,7 @@ namespace umbraco.presentation.preview } } - public static bool IsSinglePreview => PreviewMode == PreviewMode.SinglePreview; + public static bool IsSinglePreview => PreviewMode == PreviewMode.Default; public XmlDocument XmlContent { get; set; } public Guid PreviewSet { get; set; } From fb1f2c8af4f5d490d2f445e2fdd11b3f15c4efaf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 2 Apr 2019 08:12:46 +0200 Subject: [PATCH 09/47] Add content type alias to content "general" info box --- .../src/views/components/content/umb-content-node-info.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 0ed3986fe3..b37743bf01 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -198,9 +198,10 @@ style="max-width: 100%; margin-bottom: 0;" icon="node.icon" name="node.contentTypeName" + alias="documentType.alias" allow-open="allowOpen" on-open="openDocumentType(documentType)" - open-url="previewOpenUrl"> + open-url="previewOpenUrl"> From d2e04614c6b60b91df24b9b15565a86aa705725e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 5 Apr 2019 20:55:38 +0200 Subject: [PATCH 10/47] Added UDI support for GetById and fixed bug not using specified preview mode --- .../ContextualPublishedCache.cs | 166 +++++++++++++----- 1 file changed, 119 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index 429e2a2f9e..15a931957c 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Xml; @@ -13,14 +12,10 @@ namespace Umbraco.Web.PublishedCache /// public abstract class ContextualPublishedCache { - //TODO: We need to add: - //* GetById(Guid contentId) - //* GetById(UDI contentId) - protected readonly UmbracoContext UmbracoContext; /// - /// Initializes a new instance of the with a context. + /// Initializes a new instance of the with a context. /// /// The context. protected ContextualPublishedCache(UmbracoContext umbracoContext) @@ -32,8 +27,12 @@ namespace Umbraco.Web.PublishedCache /// Gets a content identified by its unique identifier. /// /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on context. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IPublishedContent GetById(int contentId) { return GetById(UmbracoContext.InPreviewMode, contentId); @@ -43,35 +42,84 @@ namespace Umbraco.Web.PublishedCache /// Gets a content identified by its unique identifier. /// /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on context. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IPublishedContent GetById(Guid contentId) { return GetById(UmbracoContext.InPreviewMode, contentId); } + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// + public IPublishedContent GetById(Udi contentId) + { + return GetById(UmbracoContext.InPreviewMode, contentId); + } + /// /// Gets a content identified by its unique identifier. /// /// A value indicating whether to consider unpublished content. /// The content unique identifier. - /// The content, or null. + /// + /// The content, or null. + /// public abstract IPublishedContent GetById(bool preview, int contentId); - // same with Guid - // cannot make this public nor abstract without breaking backward compatibility + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content key. + /// + /// The content, or null. + /// public virtual IPublishedContent GetById(bool preview, Guid contentKey) { - // original implementation - override in concrete classes - var intId = UmbracoContext.Application.Services.EntityService.GetIdForKey(contentKey, UmbracoObjectTypes.Document); - return GetById(intId.Success ? intId.Result : -1); + var contentIdAttempt = UmbracoContext.Application.Services.EntityService.GetIdForKey(contentKey, UmbracoObjectTypes.Document); + + return GetById(preview, contentIdAttempt.Success ? contentIdAttempt.Result : -1); + } + + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content identifier. + /// + /// The content, or null. + /// + /// UDIs for content items must be + public virtual IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("UDIs for content items must be " + typeof(GuidUdi)); + + return GetById(preview, guidUdi.Guid); } /// /// Gets content at root. /// - /// The contents. - /// Considers published or unpublished content depending on context. + /// + /// The contents. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IEnumerable GetAtRoot() { return GetAtRoot(UmbracoContext.InPreviewMode); @@ -81,7 +129,9 @@ namespace Umbraco.Web.PublishedCache /// Gets contents at root. /// /// A value indicating whether to consider unpublished content. - /// The contents. + /// + /// The contents. + /// public abstract IEnumerable GetAtRoot(bool preview); /// @@ -89,10 +139,11 @@ namespace Umbraco.Web.PublishedCache /// /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// Considers published or unpublished content depending on context. /// @@ -106,10 +157,11 @@ namespace Umbraco.Web.PublishedCache /// /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// Considers published or unpublished content depending on context. /// @@ -124,10 +176,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); @@ -138,10 +191,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); @@ -151,7 +205,9 @@ namespace Umbraco.Web.PublishedCache /// /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// /// If is null, or is empty, or contains only one single /// value which itself is null, then variables are ignored. @@ -186,10 +242,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); @@ -200,10 +257,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); @@ -211,8 +269,12 @@ namespace Umbraco.Web.PublishedCache /// /// Gets an XPath navigator that can be used to navigate content. /// - /// The XPath navigator. - /// Considers published or unpublished content depending on context. + /// + /// The XPath navigator. + /// + /// + /// Considers published or unpublished content depending on context. + /// public XPathNavigator GetXPathNavigator() { return GetXPathNavigator(UmbracoContext.InPreviewMode); @@ -222,20 +284,28 @@ namespace Umbraco.Web.PublishedCache /// Gets an XPath navigator that can be used to navigate content. /// /// A value indicating whether to consider unpublished content. - /// The XPath navigator. + /// + /// The XPath navigator. + /// public abstract XPathNavigator GetXPathNavigator(bool preview); /// - /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator - /// and that navigator is a NavigableNavigator. + /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator and that navigator is a NavigableNavigator. /// + /// + /// true if the XPathNavigator is navigable; otherwise, false. + /// public abstract bool XPathNavigatorIsNavigable { get; } /// /// Gets a value indicating whether the underlying non-contextual cache contains content. /// - /// A value indicating whether the underlying non-contextual cache contains content. - /// Considers published or unpublished content depending on context. + /// + /// A value indicating whether the underlying non-contextual cache contains content. + /// + /// + /// Considers published or unpublished content depending on context. + /// public bool HasContent() { return HasContent(UmbracoContext.InPreviewMode); @@ -245,7 +315,9 @@ namespace Umbraco.Web.PublishedCache /// Gets a value indicating whether the underlying non-contextual cache contains content. /// /// A value indicating whether to consider unpublished content. - /// A value indicating whether the underlying non-contextual cache contains content. + /// + /// A value indicating whether the underlying non-contextual cache contains content. + /// public abstract bool HasContent(bool preview); } } From 14f9445ffb1c0ec8950b67ad2ff7d80f4d78e292 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 12 Apr 2019 15:34:06 +0200 Subject: [PATCH 11/47] Ensure entities type in content caches GetById(Udi) --- .../PublishedCache/ContextualPublishedCache.cs | 10 +++++++++- .../PublishedCache/ContextualPublishedContentCache.cs | 3 +++ .../PublishedCache/ContextualPublishedMediaCache.cs | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index 15a931957c..8cbb0bb0fc 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -106,11 +106,19 @@ namespace Umbraco.Web.PublishedCache { var guidUdi = contentId as GuidUdi; if (guidUdi == null) - throw new InvalidOperationException("UDIs for content items must be " + typeof(GuidUdi)); + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != UdiEntityType) + throw new ArgumentException($"Udi entity type must be \"{UdiEntityType}\".", nameof(contentId)); return GetById(preview, guidUdi.Guid); } + /// + /// Gets the entity type. + /// + protected abstract string UdiEntityType { get; } + /// /// Gets content at root. /// diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs index c927644817..4475f6893e 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -21,6 +22,8 @@ namespace Umbraco.Web.PublishedCache : base(umbracoContext, cache) { } + protected override string UdiEntityType => Constants.UdiEntityType.Document; + public override IPublishedContent GetById(bool preview, Guid contentKey) { if (InnerCache is PublishedContentCache cc) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs index 0b4ccc18c4..6ab50f14fa 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -21,6 +22,8 @@ namespace Umbraco.Web.PublishedCache : base(umbracoContext, cache) { } + protected override string UdiEntityType => Constants.UdiEntityType.Media; + public override IPublishedContent GetById(bool preview, Guid contentKey) { if (InnerCache is PublishedMediaCache cc) From d4ba1298d59673f4c93c1f615db47262c541a829 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 12 Apr 2019 16:05:43 +0200 Subject: [PATCH 12/47] Add GetById(Udi) to published caches --- .../PublishedContentCache.cs | 3 +++ .../PublishedMediaCache.cs | 3 +++ .../PublishedContent/SolidPublishedSnapshot.cs | 3 +++ .../PublishedCache/IPublishedCache.cs | 18 ++++++++++++++++++ .../PublishedCache/NuCache/ContentCache.cs | 12 ++++++++++++ .../PublishedCache/NuCache/MediaCache.cs | 15 +++++++++++++++ .../PublishedCache/PublishedCacheBase.cs | 18 +++++++++--------- 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 5fc0d628c9..2a144f3aaa 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -381,6 +381,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 71490465d0..0c7ee98c6d 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 86017be820..9828a14597 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/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index ff459a2d9b..3cd7b924fb 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 0e74ea919f..4bd3fcf247 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -238,6 +238,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 f7bdb4400f..112ccd9931 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 b0fe1a4240..b88ae26704 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); From 9953aea156098e098d325b437deebd63365c4283 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 14 Apr 2019 21:03:24 +0200 Subject: [PATCH 13/47] Make tree item annotation "has pending changes" take precendence over "is listview" --- .../src/less/components/tree/umb-tree.less | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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..9aaf5f6f3a 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 @@ -236,13 +236,6 @@ body.touch .umb-tree { } } -.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { - content: "\e25a"; - color: @green; - font-size: 20px; - margin-left: -25px; -} - .is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; @@ -250,6 +243,13 @@ body.touch .umb-tree { margin-left: -20px; } +.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { + content: "\e25a"; + color: @green; + font-size: 20px; + margin-left: -25px; +} + .protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e256"; color: @red; From 3f5ccb302d9514bbc6e76e8c60ba5d1aff3e3e16 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 15 Apr 2019 21:10:09 +0200 Subject: [PATCH 14/47] Make the tree annotations easier to see --- .../src/less/components/tree/umb-tree.less | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) 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..bff9c1a53e 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,60 @@ 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; + + &::before { + font-family: 'icomoon'; + position: absolute; + top: -4px; + } + } + } + + &.current > .umb-tree-item__inner > .umb-tree-item__annotation { + background-color: @pinkLight; } } .has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e25a"; color: @green; - font-size: 20px; - margin-left: -25px; + font-size: 23px; + margin-left: 16px; + left: -21px; } .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; } .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 { From 52bc4b04b35a964e1a4a675aa449873248638655 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 15 Apr 2019 21:16:59 +0200 Subject: [PATCH 15/47] Fix annotation hover background color --- .../src/less/components/tree/umb-tree.less | 4 ++++ 1 file changed, 4 insertions(+) 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 bff9c1a53e..95c2ac68d7 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 @@ -245,6 +245,10 @@ body.touch .umb-tree { top: -4px; } } + + &:hover > .umb-tree-item__annotation { + background-color: @ui-option-hover; + } } &.current > .umb-tree-item__inner > .umb-tree-item__annotation { From 40d2898fb88ac09a56b75780b99143ee20aa3e51 Mon Sep 17 00:00:00 2001 From: Ben Palmer Date: Tue, 16 Apr 2019 09:21:24 +0100 Subject: [PATCH 16/47] Make DropDownFlexibleConfiguration class public to allow configuration to be set when creating a new data type --- .../PropertyEditors/DropDownFlexibleConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 +} From 7db0440b5c08927a5c753fc3e49a0446ee7fc2ea Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 16 Apr 2019 10:11:16 +0200 Subject: [PATCH 17/47] Merge branch 'v7/dev' into v8/dev # Conflicts: # src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs # src/Umbraco.Core/Services/ContentService.cs # src/Umbraco.Core/Services/IContentService.cs # src/Umbraco.Core/UriExtensions.cs # src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js # src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js # src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js # src/Umbraco.Web.UI.Client/src/common/services/search.service.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html # src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html # src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html # src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html # src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html # src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs # src/Umbraco.Web/Editors/ContentController.cs # src/Umbraco.Web/Editors/EntityController.cs # src/Umbraco.Web/Editors/MediaController.cs # src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs # src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs # src/Umbraco.Web/Search/UmbracoTreeSearcher.cs # src/Umbraco.Web/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web/Trees/TreeControllerBase.cs # src/Umbraco.Web/Trees/TreeQueryStringParameters.cs # src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs # src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs # src/Umbraco.Web/umbraco.presentation/content.cs # src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs --- .../tree/umbtreesearchbox.directive.js | 7 ++ .../src/common/resources/content.resource.js | 17 ++- .../src/common/resources/entity.resource.js | 41 +++++-- .../src/common/resources/media.resource.js | 4 +- .../src/common/services/search.service.js | 20 +++- .../src/common/services/tinymce.service.js | 30 ++++- .../linkpicker/linkpicker.controller.js | 27 ++++- .../linkpicker/linkpicker.html | 2 + .../mediapicker/mediapicker.controller.js | 37 ++++-- .../treepicker/treepicker.controller.js | 4 + .../treepicker/treepicker.html | 3 +- .../contentpicker/contentpicker.controller.js | 5 +- .../contentpicker/contentpicker.html | 1 + .../grid/editors/media.controller.js | 17 ++- .../mediapicker/mediapicker.controller.js | 43 ++++--- .../multiurlpicker.controller.js | 3 +- .../relatedlinks/relatedlinks.controller.js | 2 + .../propertyeditors/rte/rte.prevalues.html | 8 ++ src/Umbraco.Web.UI/favicon.ico | Bin 0 -> 15406 bytes src/Umbraco.Web/Editors/EntityController.cs | 108 ++++++++++++------ src/Umbraco.Web/Editors/MediaController.cs | 61 +++++++++- .../ContentPickerConfiguration.cs | 3 + .../PropertyEditors/GridConfiguration.cs | 3 + .../MediaPickerConfiguration.cs | 3 + .../MultiNodePickerConfiguration.cs | 3 + .../MultiUrlPickerConfiguration.cs | 3 + .../PropertyEditors/RichTextConfiguration.cs | 3 + src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 39 ++++++- .../Trees/ContentTreeControllerBase.cs | 11 +- src/Umbraco.Web/Trees/TreeControllerBase.cs | 10 ++ .../Trees/TreeQueryStringParameters.cs | 1 + ...EnsureUserPermissionForContentAttribute.cs | 42 ++++--- .../FilterAllowedOutgoingMediaAttribute.cs | 9 +- 33 files changed, 449 insertions(+), 121 deletions(-) create mode 100644 src/Umbraco.Web.UI/favicon.ico 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/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/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/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..3c4f5a7d73 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 @@ -1143,11 +1143,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 +1175,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/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/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/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/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/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.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 6314e0b31e..13515ca7a9 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 @@ -28,6 +28,14 @@ + +
+ +
+
+
× diff --git a/src/Umbraco.Web.UI/favicon.ico b/src/Umbraco.Web.UI/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c0749ddf7f1b68606b0672aa8d709779d6b0817a GIT binary patch literal 15406 zcmeHO4YX869ls>@X@QnjYWV^}e8GFWcjn6B>$!Vp$z%56M^t7dom1(l=#bin)j2&i z%2cetQnXWw@WhR`dv_loS%x3T=@5!3T48}ABp~u32!W{m{mFjID={PHn zcbrjx0SYiG^>`%u1tQ&A671(ejz3-!e0M3w_mqUUCh+;Jpo5KxB9d=yeIwzOa^69d z*GvU8UkL>J<&v{Tyh*1+xTs%h!ZFqfcCStYvfWSzC-|%Q-VXWyMJ332FW^aaRk=tG zlA)AsI~DaW%Qk}X`Rt&>d!H^Jy?1BYPA(IlUOLp5LWYkP_rA`HZM16z)^d*zh$3x!-RGoUF4+rm@kUkM|e^u3i_8Qr@ z-hLH*enBC}CRW?E{!(4Rcl~0PpI>dq`0Ds2;a*tIdS_JIL->KQvDYEG0?bH`@XS5h_~kO z`cmV!1MlnA7=>wo*VCxm-dlfgQHOh)91~pz#n1KxZ^EVDvP@<$^iU4-i~JV+ZnF6b zcI7Y8kh)NPG4p>JeVZA^)#;Qn?g+sA6tq@B7uz7iHhe!EYW5#!)>_XXqK0P4kjqjdIw$fjI3P10+pM<#&o>lv( z`ijVL%z3-Wj}$Yh7DcDg=dz4582$M^`u0)`ADb-uFt4tjHKN|H+GOdT#NG?rc&HzI z>*OL|(Cw#BvGJw$qR)oQxoWoaI=YR?$2XO;?H`Jhf%=VcTi+Pn+S*~q#2z*#`tP| z97k3BEGO@-NW%=y<00!iU>V2(s+6m^aI#>3RF~aR_|&6CQ2dL2`Ygyp>qhJx1k4Vszvn2tZlAePIv_ZV>kK1J z*hAhyfQw%~(|QbG)cPFDpJ+jb-T85?$0->wueO+YATId@?6(?s)xltGRQRPFJG-vU zq6300X5CNK@cDg4eBs>>-K>ddyIbKy#%~&7;^gqz532Zr|IUOj*HGbYG_}D#x@-9C zN;6E1EeP-W8a}_>gpYph#MnG*YJ*+^g^&36M@E>K{!=`t@WErA5g)b+86G#aq5h9m ze7O$C^wl5-Y;cCcr@okY$aPM5J;t^iUtPu5V`Bp^BfemlR`CUY&;$b+;kVgUrZ&jD zt%{#*-2M@FOi}pYL-F+?^A_>k4aE%4nA*Vq)t(G{7tttXIqk&`!H=)wX~OMcCt}|a zwfAIacOuS$t^eCdORy}_pjZqr|1rWCX44^btWPLNEMpG>6HKjd+61B;NEZh*9qZ{T)U)L;v~@hui`7tQGE+zvUd8 z)_=MQZA7publ&vd={RqI4(=Iv8TfLtr!ar|E{M}<9R5B~GEi)5i=il=F(5JszHXu5 z!%Y1S*Y)b5d1HZf7BR$o!hGVsJ7Euqt)^lA^?hC-hCyt3xr$@r_(iqJx#{yx%;ilD z*u`eVRzEI`Z#yB1Zlm?ohe9@e24d?gp_4MoV~}S96Xjh(mqKSu{%j{}rC{ zBq@}sD9y-|fQ<1s4>_`ON81PBXORbVzlAw|2jagKu&Z?_n=y`CFosW~eq~vB3yN8G zMKPNev>(ss*pR;NfsDc=uN*{qE6Dj3#7gs|Uy9Fzh1WvLIj@6V-UZ)>9Aw+*PR#4% z?E4OxXKV=Ng)8wbVuyy;Z*47;xO_ioJclwTKd$|xzDpL5xoF~;V~fK59BhK-c37Xy z`9=-_b~UXlGg8KP@e(C(rMxZV?t&ff9WdE@bpjp-U{gi-!?DRFjE1EBHDd9N6gv*I z>^1tti*GF;pAaosd<(4$GT(wRt>^6KqD);$w;xbrZ}u6zRe!1pAzr2>30`n>k=*-X^<@vXKwEzhESJ{PiA z&X*$XGK$AQ)?-c!R*3Vh*iU~Pc9VENtHwzBCLe2%kA11{&BwXo>>|#TD4$COIhSh? z@6H0;T+sg$p0tM08mw$2(UW{Dczj*Ab@iTS!-k;!pNVK9R*>s2@-KyqdtndGaul7U z&pggu!S9oZA-d4F^-{JZ{eeGRYRFf^Pkk?;?EA33+}dl4eab(F`$*^X7?W9;r#Lf9 zFeW~`sZ)!W=sXKDJ!!=yUyA&r%7KLC=+8IhOmJTPW%R+mP9k2?&wSwPzZiSYKstf! z7^4Ly87Ws!d1bl>K1U%` z17-$(YJ7MPOSS-*^|7`9U4rKViiIhc2flxeg@-PR$XU%t464gtp43KX2+!E{WwL1* z<7}d}Qm09Fggx?Ypc|i8!#xp%jVq;mzWc*YZO+iR!5>@uZPN>AtY6mJ zdW_zOQkGqcvDzPpD|4TsJsV`)Puu%Swoo4nlHRO%u{K;A6FS4Im)|=NvRlt};CJa9 zt#%(Ik{{m{E0OY4`VPE1X-&5A!dSgPb4}*~`Xw7) z;&#DT>-%`z{TKt;jJbK8CUp2`z#&&q8%yIm);v1zQ}TCW9$?%aGQ!$u8Qb|fl`|(E zJ@SqZ?s6GnHCZe7y%Nkp#D36;l79|!j>cNB%1YnHzt+Bi?kXwz(ud~wQ#Sf^t`7a% z<%j-}qia|6Y3+gjSKH_(ZKqg@;x9#?^0|=99*YUeFU6e4*YvZk$LDm7QU2r|7~%aT zseCp#_~#9C+uGiy=+jz_KE-n!A~I!yoG`BM~wDEc^K zX3(oWPvi6Dg{X7(n(h(%crTWP`%y)|B0nqkPv+V@q`9QiE<;za&*yCPq1W3H7n~1R z%UbR2#h&EzIuGE@w$V@AuANB}j~;>h3v`E6*P-bhXR!+>ju{5|_gG+YkB?neC%^Ab zx8Nk&Q%M@ay;i4P&LXdBxnBm|A1Gm+TY6^eai9@D7ioNO?^U-^toa{lmi%4?Fv@ca8!CCmE)97Ad{CXs?H^tr`{Jw^AGs=os9fjnKlLvGj#=i8HBIXpzRKObV z1=VQq4Ml(Sa3w!IVT03~CQl=>_W>6B8#?2!%N3Meorg zPJY3kU6>g|$Ewy}DU;^#jT?`M`CRe!&xi9AwoX9y$?&sl4{IMPvOh!rXJV5#+)nFL zNu(xY9L)CsW9M0Od=32J&SHk06V7pkWsUnLTcVuV|9DPQe)}ImCY_6uEomFgX zA28taTIL+tZF+t-shn|e(zoJ&mE;;nb~!Niv$lYJQuwZQoLr5z$^0Z@joyCjK%8#R zcSrSA&QIJ$66^`&a=xqQAR3jcQTWBUcLm?{8N^+`!gD41vpbQml4D2z`$Dk_y9Kf4 zMP#3i@^2PA?H?%jPj_hKnX&KDnHTPr$o>85%G~Dzu<4)1ONa4 literal 0 HcmV?d00001 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/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 3b7142397b..6710cf59f6 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -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); } } 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/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..67099dc58a 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); + var ignoreUserStartNodes = bool.Parse(queryStringCollection["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) From bdac2a4a41338680104179bc4545e5592a4b4f24 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 18 Mar 2019 13:27:00 +0100 Subject: [PATCH 18/47] Use umb-checkbox for user filters --- .../src/views/users/views/users/users.html | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) 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 @@ -
- - -
+ +
From 4107f475880e6175729211b5a80489a3489e88d6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 20 Mar 2019 14:17:14 +0100 Subject: [PATCH 19/47] Add optional culture parameter to UrlAbsolute extension --- src/Umbraco.Web/PublishedContentExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 54afb7abbd..b10381bb94 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -41,8 +41,9 @@ namespace Umbraco.Web /// Gets the absolute url for the content. /// /// The content. + /// The culture to get the url for (defaults to current culture) /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content) + public static string UrlAbsolute(this IPublishedContent content, string culture = null) { // adapted from PublishedContentBase.Url switch (content.ItemType) @@ -52,7 +53,7 @@ namespace Umbraco.Web throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); if (Current.UmbracoContext.UrlProvider == null) throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true); + return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true, culture); case PublishedItemType.Media: throw new NotSupportedException("AbsoluteUrl is not supported for media types."); default: From a48d97e6314ad70403880df5c28deb6354f9442c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 21 Mar 2019 12:27:11 +0100 Subject: [PATCH 20/47] Create explicit overloads for UrlAbsolute --- src/Umbraco.Web/PublishedContentExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index b10381bb94..dea4f17eef 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -41,9 +41,16 @@ namespace Umbraco.Web /// Gets the absolute url for the content. /// /// The content. - /// The culture to get the url for (defaults to current culture) /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content, string culture = null) + public static string UrlAbsolute(this IPublishedContent content) => content.UrlAbsolute(null); + + /// + /// Gets the absolute url for the content. + /// + /// The content. + /// The culture to get the url for + /// The absolute url for the content. + public static string UrlAbsolute(this IPublishedContent content, string culture) { // adapted from PublishedContentBase.Url switch (content.ItemType) From 2d32863fc957581575e519893e7b4b023cca1c87 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 14:06:16 +0100 Subject: [PATCH 21/47] Audit current user when moving media + clean up a few audit related things --- src/Umbraco.Web/Editors/MediaController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 97e24ee6b8..fe238219d7 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); + var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.CurrentUser.Id); var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); //remove this tab if it exists: umbContainerView @@ -422,7 +422,7 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundMedia.IsInRecycleBin() == false) { - var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, Security.CurrentUser.Id); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -432,7 +432,7 @@ namespace Umbraco.Web.Editors } else { - var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); + var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, Security.CurrentUser.Id); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -456,7 +456,7 @@ namespace Umbraco.Web.Editors var destinationParentID = move.ParentId; var sourceParentID = toMove.ParentId; - var moveResult = Services.MediaService.WithResult().Move(toMove, move.ParentId); + var moveResult = Services.MediaService.WithResult().Move(toMove, move.ParentId, Security.CurrentUser.Id); if (sourceParentID == destinationParentID) { @@ -525,7 +525,7 @@ namespace Umbraco.Web.Editors } //save the item - var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); //return the updated model var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); From 2a930e5a6663c6b94d21fe3c81104fe6b016f863 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 18:45:22 +0100 Subject: [PATCH 22/47] Log the ID of the current user when emptying the media recycle bin --- src/Umbraco.Core/Services/IMediaService.cs | 8 ++++++++ src/Umbraco.Core/Services/MediaService.cs | 12 ++++++++++-- src/Umbraco.Web/Editors/MediaController.cs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index f5f136b789..4e3189bf72 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -289,8 +289,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")] void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index b95a8a258c..873cff2bf5 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -981,7 +981,15 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public void EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) { using (new WriteLock(Locker)) { @@ -1006,7 +1014,7 @@ namespace Umbraco.Core.Services recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - Audit(uow, AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, Constants.System.RecycleBinMedia); + Audit(uow, AuditType.Delete, "Empty Media Recycle Bin performed by user", userId, Constants.System.RecycleBinMedia); uow.Commit(); } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index fe238219d7..1cc561ce9f 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -583,7 +583,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage EmptyRecycleBin() { - Services.MediaService.EmptyRecycleBin(); + Services.MediaService.EmptyRecycleBin(Security.CurrentUser.Id); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } From eb10d71bff78155af9f32faef453c765e42ce21b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 17 Apr 2019 16:47:23 +0200 Subject: [PATCH 23/47] Don't just Parse, the value might be null, have to use TryParse --- .../WebApi/Filters/EnsureUserPermissionForContentAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 67099dc58a..0c53838592 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -118,7 +118,7 @@ namespace Umbraco.Web.WebApi.Filters } var queryStringCollection = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); - var ignoreUserStartNodes = bool.Parse(queryStringCollection["ignoreUserStartNodes"]); + bool.TryParse(queryStringCollection["ignoreUserStartNodes"], out var ignoreUserStartNodes); if (ignoreUserStartNodes == false) { var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, From 73e4c6022dba6574a989355241976fd6c8da7807 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Mar 2019 11:34:49 +0100 Subject: [PATCH 24/47] Update the title of the macro view picker --- .../src/views/macros/views/macro.settings.controller.js | 8 +++++++- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) 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/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index e75ff6d83d..9c2b15d5ab 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1338,6 +1338,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/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 372bc3158d..f1ac5dd6dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1615,6 +1615,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..7ba4d05786 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1625,6 +1625,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 From 29a542a0ddded21521b09963d210f68538d223bd Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Mar 2019 13:20:30 +0100 Subject: [PATCH 25/47] Support stylesheets in subfolders in the RTE --- .../src/common/services/tinymce.service.js | 9 +++++- .../rte/rte.prevalues.controller.js | 28 ++++++++++++------- .../propertyeditors/rte/rte.prevalues.html | 4 +-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 3c4f5a7d73..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) { 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 13515ca7a9..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/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index c9b4695708..1688553bb9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -104,12 +104,7 @@ - - + @@ -211,4 +206,4 @@ - \ No newline at end of file + From f55e05da79ca827b192a03d6d1271501bced0b66 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 13:38:53 +0100 Subject: [PATCH 35/47] Don't explode when passing bad parameters to log resource --- .../src/common/resources/log.resource.js | 4 ++-- src/Umbraco.Web/Editors/LogController.cs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) 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/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); From 59df7f1399e85c29aac0c462793e4640a4816cd9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 15:45:32 +0100 Subject: [PATCH 36/47] Ensure consistency in the way panels are spaced --- .../src/less/components/html/umb-expansion-panel.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/umb-box.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 +} From 8b6fbee47e5e5a3a18ea84b68c0421a61676197d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:00:51 +0200 Subject: [PATCH 37/47] V8: Add back button to listviews in media (#5116) --- src/Umbraco.Web.UI.Client/src/views/media/edit.html | 4 +++- .../src/views/media/media.edit.controller.js | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) 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) { From 32104fff412e3972391c7d941d230cde0eb2d25f Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:21:38 +0200 Subject: [PATCH 38/47] V8: Add back button to listviews for members (#5158) * Add back button to edit member * Don't set illegal location (with querystring params) --- .../src/views/member/edit.html | 6 ++++-- .../src/views/member/member.edit.controller.js | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) 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); From e180bb672212332556ac8ac17573a42c6311ba27 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:44:29 +0200 Subject: [PATCH 39/47] V8: Fix broken listview pagination when using the back link (#5149) --- .../components/umbpagination.directive.js | 4 +++ .../common/services/listviewhelper.service.js | 31 +++++++++++++++++-- .../grid/grid.listviewlayout.controller.js | 2 +- .../list/list.listviewlayout.controller.js | 5 ++- 4 files changed, 36 insertions(+), 6 deletions(-) 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/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 8aaddbf98f..e317c509df 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/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) { From 687cc4630fad30bce84ec622b1d716434fa47cbc Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 22 Apr 2019 10:47:00 +0200 Subject: [PATCH 40/47] Fix missing semicolon --- .../src/common/services/listviewhelper.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e317c509df..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 @@ -581,7 +581,7 @@ $location.path(path); for (var p in params) { - $location.search(p, params[p]) + $location.search(p, params[p]); } } From 344b7731ce9c5213ddbafa6f792625f8a6d34344 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 22 Apr 2019 12:10:53 +0200 Subject: [PATCH 41/47] Fix JS unit tests --- .../src/common/services/tree.service.js | 12 ++++++++++-- .../test/unit/common/services/tree-service.spec.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) 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 73893739a6..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 */ @@ -166,8 +175,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { - // localeCompare returns 0 when strings are equal, so return false if there's no match - return item.alias.localeCompare(treeAlias, undefined, { ignorePunctuation: true }) !== 0; + return invariantEquals(item.alias, treeAlias); }); return found ? found.packageFolder : undefined; 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); From 655befde420faf427f72f1b39cc585cc15a8641d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 8 Apr 2019 21:18:09 +0200 Subject: [PATCH 42/47] Read image dimensions via GDI if EXIF fails with an exception --- src/Umbraco.Web/Media/ImageHelper.cs | 31 +++++++++++----------------- 1 file changed, 12 insertions(+), 19 deletions(-) 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); } - - - } } From 072c0140554ac826468996750629e210d1c5ec1c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 8 Apr 2019 20:59:12 +0200 Subject: [PATCH 43/47] Read image dimensions via GDI if EXIF fails with an exception --- src/Umbraco.Core/IO/MediaFileSystem.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index a4305506bb..d843b95709 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -377,21 +377,28 @@ namespace Umbraco.Core.IO 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); } } - catch (Exception) + catch { - //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); } #endregion From c7b4d1b206c5773a500fa45417e967f989259d6c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Apr 2019 07:41:17 +0200 Subject: [PATCH 44/47] Always show file names for media/files without thumbnail --- .../src/views/components/umb-media-grid.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}}
From a969ff6e10b9e32810a74af893f41352b0c7289a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Apr 2019 22:21:13 +0200 Subject: [PATCH 45/47] Make sure Nested Content scaffolds are listed in defined sort order --- .../views/common/overlays/itempicker/itempicker.controller.js | 4 ++++ .../src/views/common/overlays/itempicker/itempicker.html | 2 +- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) 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 @@