diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index a0e68d23a7..60e5ddfd04 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -7,7 +7,12 @@ /// public static class PropertyEditors { - /// + /// + /// Used to prefix generic properties that are internal content properties + /// + public const string InternalGenericPropertiesPrefix = "_umb_"; + + /// /// Guid for the Checkbox list datatype. /// public const string CheckBoxList = "B4471851-82B6-4C75-AFA4-39FA9C6A75E9"; diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index c852111f9f..5f5e41b1ef 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -367,7 +367,7 @@ namespace Umbraco.Core /// internal static string ToXmlString(this object value, Type type) { - if (type == typeof(string)) return ((string)value).IsNullOrWhiteSpace() ? "" : (string)value; + if (type == typeof(string)) return (value.ToString().IsNullOrWhiteSpace() ? "" : value.ToString()); if (type == typeof(bool)) return XmlConvert.ToString((bool)value); if (type == typeof(byte)) return XmlConvert.ToString((byte)value); if (type == typeof(char)) return XmlConvert.ToString((char)value); diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index c9237e61d4..97ca510160 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -1,130 +1,134 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Umbraco.Core.Configuration; -using Umbraco.Core.Events; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.UnitOfWork; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Represents a repository specific to the Recycle Bins - /// available for Content and Media. - /// - internal class RecycleBinRepository - { - private readonly IDatabaseUnitOfWork _unitOfWork; - - public RecycleBinRepository(IDatabaseUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public bool EmptyRecycleBin(Guid nodeObjectType) - { - var db = _unitOfWork.Database; - - //Issue query to get all trashed content or media that has the Upload field as a property - //The value for each field is stored in a list: FilesToDelete() - //Alias: Constants.Conventions.Media.File and ControlId: Constants.PropertyEditors.UploadField - var sql = new Sql(); - sql.Select("DISTINCT(dataNvarchar)") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.PropertyTypeId, right => right.Id) - .InnerJoin().On(left => left.DataTypeId, right => right.DataTypeId) - .Where("umbracoNode.trashed = '1' AND umbracoNode.nodeObjectType = @NodeObjectType AND dataNvarchar IS NOT NULL AND (cmsPropertyType.Alias = @FileAlias OR cmsDataType.controlId = @ControlId)", - new { FileAlias = Constants.Conventions.Media.File, NodeObjectType = nodeObjectType, ControlId = Constants.PropertyEditors.UploadField }); - - var files = db.Fetch(sql); - - //Construct and execute delete statements for all trashed items by 'nodeObjectType' - var deletes = new List - { - FormatDeleteStatement("umbracoUser2NodeNotify", "nodeId"), - FormatDeleteStatement("umbracoUser2NodePermission", "nodeId"), - FormatDeleteStatement("umbracoRelation", "parentId"), - FormatDeleteStatement("umbracoRelation", "childId"), - FormatDeleteStatement("cmsTagRelationship", "nodeId"), - FormatDeleteStatement("umbracoDomains", "domainRootStructureID"), - FormatDeleteStatement("cmsDocument", "NodeId"), - FormatDeleteStatement("cmsPropertyData", "contentNodeId"), - FormatDeleteStatement("cmsPreviewXml", "nodeId"), - FormatDeleteStatement("cmsContentVersion", "ContentId"), - FormatDeleteStatement("cmsContentXml", "nodeID"), - FormatDeleteStatement("cmsContent", "NodeId"), - "DELETE FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType" - }; - - //Wraps in transaction - this improves performance and also ensures - // that if any of the deletions fails that the whole thing is rolled back. - using (var trans = db.GetTransaction()) - { - try - { - foreach (var delete in deletes) - { - db.Execute(delete, new { NodeObjectType = nodeObjectType }); - } - - trans.Complete(); - - //Trigger (internal) event with list of files to delete - RecycleBinEmptied - RecycleBinEmptied.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, files), this); - - return true; - } - catch (Exception ex) - { - trans.Dispose(); - LogHelper.Error("An error occurred while emptying the Recycle Bin: " + ex.Message, ex); - return false; - } - } - } - - public bool DeleteFiles(IEnumerable files) - { - try - { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - Parallel.ForEach(files, file => - { - if (UmbracoSettings.UploadAllowDirectories) - { - var relativeFilePath = fs.GetRelativePath(file); - var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); - fs.DeleteDirectory(parentDirectory, true); - } - else - { - fs.DeleteFile(file, true); - } - }); - - return true; - } - catch (Exception ex) - { - LogHelper.Error("An error occurred while deleting files attached to deleted nodes: " + ex.Message, ex); - return false; - } - } - - private string FormatDeleteStatement(string tableName, string keyName) - { - return - string.Format( - "DELETE FROM {0} FROM {0} as TB1 INNER JOIN umbracoNode as TB2 ON TB1.{1} = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType", - tableName, keyName); - } - - /// - /// Occurs after RecycleBin was been Emptied - /// - internal static event TypedEventHandler RecycleBinEmptied; - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository specific to the Recycle Bins + /// available for Content and Media. + /// + internal class RecycleBinRepository + { + private readonly IDatabaseUnitOfWork _unitOfWork; + + public RecycleBinRepository(IDatabaseUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public bool EmptyRecycleBin(Guid nodeObjectType) + { + var db = _unitOfWork.Database; + + //Issue query to get all trashed content or media that has the Upload field as a property + //The value for each field is stored in a list: FilesToDelete() + //Alias: Constants.Conventions.Media.File and ControlId: Constants.PropertyEditors.UploadField + var sql = new Sql(); + sql.Select("DISTINCT(dataNvarchar)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.PropertyTypeId, right => right.Id) + .InnerJoin().On(left => left.DataTypeId, right => right.DataTypeId) + .Where("umbracoNode.trashed = '1' AND umbracoNode.nodeObjectType = @NodeObjectType AND dataNvarchar IS NOT NULL AND (cmsPropertyType.Alias = @FileAlias OR cmsDataType.controlId = @ControlId)", + new { FileAlias = Constants.Conventions.Media.File, NodeObjectType = nodeObjectType, ControlId = Constants.PropertyEditors.UploadField }); + + var files = db.Fetch(sql); + + //Construct and execute delete statements for all trashed items by 'nodeObjectType' + var deletes = new List + { + FormatDeleteStatement("umbracoUser2NodeNotify", "nodeId"), + FormatDeleteStatement("umbracoUser2NodePermission", "nodeId"), + FormatDeleteStatement("umbracoRelation", "parentId"), + FormatDeleteStatement("umbracoRelation", "childId"), + FormatDeleteStatement("cmsTagRelationship", "nodeId"), + FormatDeleteStatement("umbracoDomains", "domainRootStructureID"), + FormatDeleteStatement("cmsDocument", "NodeId"), + FormatDeleteStatement("cmsPropertyData", "contentNodeId"), + FormatDeleteStatement("cmsPreviewXml", "nodeId"), + FormatDeleteStatement("cmsContentVersion", "ContentId"), + FormatDeleteStatement("cmsContentXml", "nodeID"), + FormatDeleteStatement("cmsContent", "NodeId"), + "UPDATE umbracoNode SET parentID = '-20' WHERE trashed = '1' AND nodeObjectType = @NodeObjectType", + "DELETE FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType" + }; + + //Wraps in transaction - this improves performance and also ensures + // that if any of the deletions fails that the whole thing is rolled back. + using (var trans = db.GetTransaction()) + { + try + { + foreach (var delete in deletes) + { + db.Execute(delete, new { NodeObjectType = nodeObjectType }); + } + + trans.Complete(); + + //Trigger (internal) event with list of files to delete - RecycleBinEmptied + RecycleBinEmptied.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, files), this); + + return true; + } + catch (Exception ex) + { + trans.Dispose(); + LogHelper.Error("An error occurred while emptying the Recycle Bin: " + ex.Message, ex); + return false; + } + } + } + + public bool DeleteFiles(IEnumerable files) + { + try + { + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + Parallel.ForEach(files, file => + { + if (UmbracoSettings.UploadAllowDirectories) + { + var relativeFilePath = fs.GetRelativePath(file); + var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); + fs.DeleteDirectory(parentDirectory, true); + } + else + { + fs.DeleteFile(file, true); + } + }); + + return true; + } + catch (Exception ex) + { + LogHelper.Error("An error occurred while deleting files attached to deleted nodes: " + ex.Message, ex); + return false; + } + } + + private string FormatDeleteStatement(string tableName, string keyName) + { + //This query works with sql ce and sql server: + //DELETE FROM umbracoUser2NodeNotify WHERE umbracoUser2NodeNotify.nodeId IN + //(SELECT nodeId FROM umbracoUser2NodeNotify as TB1 INNER JOIN umbracoNode as TB2 ON TB1.nodeId = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = 'C66BA18E-EAF3-4CFF-8A22-41B16D66A972') + return + string.Format( + "DELETE FROM {0} WHERE {0}.{1} IN (SELECT TB1.{1} FROM {0} as TB1 INNER JOIN umbracoNode as TB2 ON TB1.{1} = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + tableName, keyName); + } + + /// + /// Occurs after RecycleBin was been Emptied + /// + internal static event TypedEventHandler RecycleBinEmptied; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueEditor.cs b/src/Umbraco.Core/PropertyEditors/ValueEditor.cs index 258444ba69..c0d1aa39ec 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueEditor.cs @@ -102,6 +102,14 @@ namespace Umbraco.Core.PropertyEditors } } + /// + /// Set this to true if the property editor is for display purposes only + /// + public virtual bool IsReadOnly + { + get { return false; } + } + /// /// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor /// diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index 0474cd9b06..a0f48e7414 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -1,190 +1,207 @@ -angular.module('umbraco.mocks'). - factory('mocksUtils', ['$cookieStore', function($cookieStore) { - 'use strict'; - - //by default we will perform authorization - var doAuth = true; - - return { - - /** Creats a mock content object */ - getMockContent: function(id) { - var node = { - name: "My content with id: " + id, - updateDate: new Date().toIsoDateTimeString(), - publishDate: new Date().toIsoDateTimeString(), - createDate: new Date().toIsoDateTimeString(), - id: id, - parentId: 1234, - icon: "icon-file-alt", - owner: { name: "Administrator", id: 0 }, - updater: { name: "Per Ploug Krogslund", id: 1 }, - - tabs: [ - { - label: "Child documents", - id: 1, - active: true, - properties: [ - { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } - ] - }, - { - label: "Content", - id: 2, - properties: [ - { alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" }, - { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

askjdkasj lasjd

" }, - { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, - { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, - { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, - { alias: "content", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" } - ] - }, - { - label: "Sample Editor", - id: 3, - properties: [ - { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, - { alias: "tags", label: "Tags", view: "tags", value: "" } - ] - }, - { - label: "Grid", - id: 4, - properties: [ - { alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true } - ] - }, { - label: "Generic Properties", - id: 0, - properties: [ - { - label: 'Created by', - description: 'Original author', - value: 1, - view: "readonlyvalue", - alias: "_umb_createdby" - }, - { - label: 'Created', - description: 'Time this document was created', - value: new Date().toIsoDateTimeString(), - view: "readonlyvalue", - //NOTE: No need for filters because the date is a formatted string already because - // that is how it comes from the server as a pre-formatted json string - //config: {filter: 'date', format: 'medium'}, - alias: "_umb_createdate" - }, - { - label: 'Updated', - description: 'Time this document was last updated', - value: new Date().toIsoDateTimeString(), - view: "readonlyvalue", - alias: "_umb_updatedate" - }, - { - label: 'Id', - value: 1234, - view: "readonlyvalue", - alias: "_umb_id" - }, - { - label: 'Document Type', - value: "Home page", - view: "readonlyvalue", - alias: "_umb_doctype" - }, - { - label: 'Template', - value: "1234", - view: "templatepicker", - alias: "_umb_template" - }, - { - alias: "test", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "", - config: { - fields: [ - { alias: "embedded", label: "Embbeded", view: "textstring", value: "" }, - { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" }, - { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" }, - { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" } - ] - } - } - ] - } - ] - }; - - return node; - }, - - getMockEntity : function(id){ - return {name: "hello", id: id, icon: "icon-file"}; - }, - - /** generally used for unit tests, calling this will disable the auth check and always return true */ - disableAuth: function() { - doAuth = false; - }, - - /** generally used for unit tests, calling this will enabled the auth check */ - enabledAuth: function() { - doAuth = true; - }, - - /** Checks for our mock auth cookie, if it's not there, returns false */ - checkAuth: function () { - if (doAuth) { - var mockAuthCookie = $cookieStore.get("mockAuthCookie"); - if (!mockAuthCookie) { - return false; - } - return true; - } - else { - return true; - } - }, - - /** Creates/sets the auth cookie with a value indicating the user is now authenticated */ - setAuth: function() { - //set the cookie for loging - $cookieStore.put("mockAuthCookie", "Logged in!"); - }, - - /** removes the auth cookie */ - clearAuth: function() { - $cookieStore.remove("mockAuthCookie"); - }, - - urlRegex: function(url) { - url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - return new RegExp("^" + url); - }, - - getParameterByName: function(url, name) { - name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - - var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), - results = regex.exec(url); - - return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); - }, - - getParametersByName: function(url, name) { - name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - - var regex = new RegExp(name + "=([^&#]*)", "mg"), results = []; - var match; - - while ( ( match = regex.exec(url) ) !== null ) - { - results.push(decodeURIComponent(match[1].replace(/\+/g, " "))); - } - - return results; - } - }; - }]); \ No newline at end of file +angular.module('umbraco.mocks'). + factory('mocksUtils', ['$cookieStore', function($cookieStore) { + 'use strict'; + + //by default we will perform authorization + var doAuth = true; + + return { + + /** Creats a mock content object */ + getMockContent: function(id) { + var node = { + name: "My content with id: " + id, + updateDate: new Date().toIsoDateTimeString(), + publishDate: new Date().toIsoDateTimeString(), + createDate: new Date().toIsoDateTimeString(), + id: id, + parentId: 1234, + icon: "icon-file-alt", + owner: { name: "Administrator", id: 0 }, + updater: { name: "Per Ploug Krogslund", id: 1 }, + + tabs: [ + { + label: "Child documents", + id: 1, + active: true, + properties: [ + { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } + ] + }, + { + label: "Content", + id: 2, + properties: [ + { alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" }, + { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

askjdkasj lasjd

" }, + { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, + { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, + { alias: "content", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" } + ] + }, + { + label: "Sample Editor", + id: 3, + properties: [ + { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, + { alias: "tags", label: "Tags", view: "tags", value: "" } + ] + }, + { + label: "Grid", + id: 4, + properties: [ + { alias: "grid", label: "Grid", view: "grid", value: "test", hideLabel: true } + ] + }, { + label: "Generic Properties", + id: 0, + properties: [ + { + label: 'Id', + value: 1234, + view: "readonlyvalue", + alias: "_umb_id" + }, + { + label: 'Created by', + description: 'Original author', + value: "Administrator", + view: "readonlyvalue", + alias: "_umb_createdby" + }, + { + label: 'Created', + description: 'Date/time this document was created', + value: new Date().toIsoDateTimeString(), + view: "readonlyvalue", + alias: "_umb_createdate" + }, + { + label: 'Updated', + description: 'Date/time this document was created', + value: new Date().toIsoDateTimeString(), + view: "readonlyvalue", + alias: "_umb_updatedate" + }, + { + label: 'Document Type', + value: "Home page", + view: "readonlyvalue", + alias: "_umb_doctype" + }, + { + label: 'Publish at', + description: 'Date/time to publish this document', + value: new Date().toIsoDateTimeString(), + view: "datepicker", + alias: "_umb_releasedate" + }, + { + label: 'Unpublish at', + description: 'Date/time to un-publish this document', + value: new Date().toIsoDateTimeString(), + view: "datepicker", + alias: "_umb_expiredate" + }, + { + label: 'Template', + value: "{id: 1234, alias: 'myTemplate', name: 'My Template'}", + view: "templatepicker", + alias: "_umb_template" + }, + { + label: 'Link to document', + value: ["/testing" + id, "http://localhost/testing" + id, "http://mydomain.com/testing" + id].join(), + view: "urllist", + alias: "_umb_template" + }, + { + alias: "test", label: "Stuff", view: "test", value: "", + config: { + fields: [ + { alias: "embedded", label: "Embbeded", view: "textstring", value: "" }, + { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" }, + { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" }, + { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" } + ] + } + } + ] + } + ] + }; + + return node; + }, + + getMockEntity : function(id){ + return {name: "hello", id: id, icon: "icon-file"}; + }, + + /** generally used for unit tests, calling this will disable the auth check and always return true */ + disableAuth: function() { + doAuth = false; + }, + + /** generally used for unit tests, calling this will enabled the auth check */ + enabledAuth: function() { + doAuth = true; + }, + + /** Checks for our mock auth cookie, if it's not there, returns false */ + checkAuth: function () { + if (doAuth) { + var mockAuthCookie = $cookieStore.get("mockAuthCookie"); + if (!mockAuthCookie) { + return false; + } + return true; + } + else { + return true; + } + }, + + /** Creates/sets the auth cookie with a value indicating the user is now authenticated */ + setAuth: function() { + //set the cookie for loging + $cookieStore.put("mockAuthCookie", "Logged in!"); + }, + + /** removes the auth cookie */ + clearAuth: function() { + $cookieStore.remove("mockAuthCookie"); + }, + + urlRegex: function(url) { + url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + return new RegExp("^" + url); + }, + + getParameterByName: function(url, name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(url); + + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + }, + + getParametersByName: function(url, name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + + var regex = new RegExp(name + "=([^&#]*)", "mg"), results = []; + var match; + + while ( ( match = regex.exec(url) ) !== null ) + { + results.push(decodeURIComponent(match[1].replace(/\+/g, " "))); + } + + return results; + } + }; + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/media.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/media.mocks.js index cc20e0264e..c166289d42 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/media.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/media.mocks.js @@ -1,97 +1,97 @@ -angular.module('umbraco.mocks'). - factory('mediaMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { - 'use strict'; - - function returnNodebyId(status, data, headers) { - - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } - - var id = mocksUtils.getParameterByName(data, "id") || 1234; - - var node = { - name: "My content with id: " + id, - updateDate: new Date(), - publishDate: new Date(), - id: id, - parentId: 1234, - icon: "icon-file-alt", - owner: {name: "Administrator", id: 0}, - updater: {name: "Per Ploug Krogslund", id: 1}, - - tabs: [ - { - label: "Child documents", - alias: "tab00", - id: 0, - active: true, - properties: [ - { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } - ] - }, - { - label: "Content", - alias: "tab01", - id: 1, - properties: [ - { alias: "bodyText", label: "Body Text", description:"Here you enter the primary article contents", view: "rte", value: "

askjdkasj lasjd

" }, - { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, - { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, - { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, - { alias: "content", label: "Content picker", view: "contentpicker", value: "" } - ] - }, - { - label: "Sample Editor", - alias: "tab02", - id: 2, - properties: [ - { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, - { alias: "tags", label: "Tags", view: "tags", value: ""} - ] - }, - { - label: "Grid", - alias: "tab03", - id: 3, - properties: [ - { alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true } - ] - },{ - label: "WIP", - alias: "tab04", - id: 4, - properties: [ - { alias: "tes", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "", - - config: { - fields: [ - { alias: "embedded", label: "Embbeded", view: "textstring", value: ""}, - { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: ""}, - { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: ""}, - { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: ""} - ] - } - } - ] - } - ] - }; - return [200, node, null]; - } - - - - return { - register: function() { - $httpBackend - .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Media/GetById')) - .respond(returnNodebyId); - }, - expectGetById: function() { - $httpBackend - .expectGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Media/GetById')); - } - }; - }]); \ No newline at end of file +angular.module('umbraco.mocks'). + factory('mediaMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { + 'use strict'; + + function returnNodebyId(status, data, headers) { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var id = mocksUtils.getParameterByName(data, "id") || 1234; + + var node = { + name: "My content with id: " + id, + updateDate: new Date(), + publishDate: new Date(), + id: id, + parentId: 1234, + icon: "icon-file-alt", + owner: {name: "Administrator", id: 0}, + updater: {name: "Per Ploug Krogslund", id: 1}, + + tabs: [ + { + label: "Child documents", + alias: "tab00", + id: 0, + active: true, + properties: [ + { alias: "list", label: "List", view: "listview", value: "", hideLabel: true } + ] + }, + { + label: "Content", + alias: "tab01", + id: 1, + properties: [ + { alias: "bodyText", label: "Body Text", description:"Here you enter the primary article contents", view: "rte", value: "

askjdkasj lasjd

" }, + { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } }, + { alias: "media", label: "Media picker", view: "mediapicker", value: "" }, + { alias: "content", label: "Content picker", view: "contentpicker", value: "" } + ] + }, + { + label: "Sample Editor", + alias: "tab02", + id: 2, + properties: [ + { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } }, + { alias: "tags", label: "Tags", view: "tags", value: ""} + ] + }, + { + label: "Grid", + alias: "tab03", + id: 3, + properties: [ + { alias: "grid", label: "Grid", view: "grid", value: "test", hideLabel: true } + ] + },{ + label: "WIP", + alias: "tab04", + id: 4, + properties: [ + { alias: "tes", label: "Stuff", view: "test", value: "", + + config: { + fields: [ + { alias: "embedded", label: "Embbeded", view: "textstring", value: ""}, + { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: ""}, + { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: ""}, + { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: ""} + ] + } + } + ] + } + ] + }; + return [200, node, null]; + } + + + + return { + register: function() { + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Media/GetById')) + .respond(returnNodebyId); + }, + expectGetById: function() { + $httpBackend + .expectGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Media/GetById')); + } + }; + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 6b2a34f03b..abd1a436a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -1,847 +1,866 @@ -/*Contains multiple services for various helper tasks */ - -/** - * @ngdoc function - * @name umbraco.services.legacyJsLoader - * @function - * - * @description - * Used to lazy load in any JS dependencies that need to be manually loaded in - */ -function legacyJsLoader(assetsService, umbRequestHelper) { - return { - - /** Called to load in the legacy tree js which is required on startup if a user is logged in or - after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ - loadLegacyTreeJs: function(scope) { - return assetsService.loadJs(umbRequestHelper.getApiUrl("legacyTreeJs", "", ""), scope); - } - }; -} -angular.module('umbraco.services').factory('legacyJsLoader', legacyJsLoader); - - -/** - * @ngdoc service - * @name umbraco.services.angularHelper - * @function - * - * @description - * Some angular helper/extension methods - */ -function angularHelper($log, $q) { - return { - - /** - * @ngdoc function - * @name umbraco.services.angularHelper#rejectedPromise - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * In some situations we need to return a promise as a rejection, normally based on invalid data. This - * is a wrapper to do that so we can save one writing a bit of code. - * - * @param {object} objReject The object to send back with the promise rejection - */ - rejectedPromise: function (objReject) { - var deferred = $q.defer(); - //return an error object including the error message for UI - deferred.reject(objReject); - return deferred.promise; - }, - - /** - * @ngdoc function - * @name safeApply - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * This checks if a digest/apply is already occuring, if not it will force an apply call - */ - safeApply: function (scope, fn) { - if (scope.$$phase || scope.$root.$$phase) { - if (angular.isFunction(fn)) { - fn(); - } - } - else { - if (angular.isFunction(fn)) { - scope.$apply(fn); - } - else { - scope.$apply(); - } - } - }, - - /** - * @ngdoc function - * @name getCurrentForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * Returns the current form object applied to the scope or null if one is not found - */ - getCurrentForm: function (scope) { - - //NOTE: There isn't a way in angular to get a reference to the current form object since the form object - // is just defined as a property of the scope when it is named but you'll always need to know the name which - // isn't very convenient. If we want to watch for validation changes we need to get a form reference. - // The way that we detect the form object is a bit hackerific in that we detect all of the required properties - // that exist on a form object. - // - //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it - // is to inject the $element object and use: $element.inheritedData('$formController'); - - var form = null; - //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"]; - var requiredFormProps = ["$addControl", "$removeControl", "$setValidity", "$setDirty", "$setPristine"]; - - // a method to check that the collection of object prop names contains the property name expected - function propertyExists(objectPropNames) { - //ensure that every required property name exists on the current scope property - return _.every(requiredFormProps, function (item) { - - return _.contains(objectPropNames, item); - }); - } - - for (var p in scope) { - - if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { - //get the keys of the property names for the current property - var props = _.keys(scope[p]); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - continue; - } - - //ensure that every required property name exists on the current scope property - var containProperty = propertyExists(props); - - if (containProperty) { - form = scope[p]; - break; - } - } - } - - return form; - }, - - /** - * @ngdoc function - * @name validateHasForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if - * it does we return the form object. - */ - getRequiredCurrentForm: function(scope) { - var currentForm = this.getCurrentForm(scope); - if (!currentForm || !currentForm.$name) { - throw "The current scope requires a current form object (or ng-form) with a name assigned to it"; - } - return currentForm; - }, - - /** - * @ngdoc function - * @name getNullForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * Returns a null angular FormController, mostly for use in unit tests - * NOTE: This is actually the same construct as angular uses internally for creating a null form but they don't expose - * any of this publicly to us, so we need to create our own. - * - * @param {string} formName The form name to assign - */ - getNullForm: function(formName) { - return { - $addControl: angular.noop, - $removeControl: angular.noop, - $setValidity: angular.noop, - $setDirty: angular.noop, - $setPristine: angular.noop, - $name: formName - //NOTE: we don't include the 'properties', just the methods. - }; - } - }; -} -angular.module('umbraco.services').factory('angularHelper', angularHelper); - -/** -* @ngdoc service -* @name umbraco.services.umbPropertyEditorHelper -* @description A helper object used for property editors -**/ -function umbPropEditorHelper() { - return { - /** - * @ngdoc function - * @name getImagePropertyValue - * @methodOf umbraco.services.umbPropertyEditorHelper - * @function - * - * @description - * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one - * - * @param {string} input the view path currently stored for the property editor - */ - getViewPath: function (input) { - var path = String(input); - if (path.startsWith('/')) { - return path; - } - else { - var pathName = path.replace('.', '/'); - //i.e. views/propertyeditors/fileupload/fileupload.html - return "views/propertyeditors/" + pathName + "/" + pathName + ".html"; - } - } - }; -} -angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); - -/** -* @ngdoc service -* @name umbraco.services.umbImageHelper -* @description A helper object used for parsing image paths -**/ -function umbImageHelper() { - return { - /** Returns the actual image path associated with the image property if there is one */ - getImagePropertyVaue: function(options) { - if (!options && !options.imageModel && !options.scope) { - throw "The options objet does not contain the required parameters: imageModel, scope"; - } - if (options.imageModel.contentTypeAlias.toLowerCase() === "image") { - var imageProp = _.find(options.imageModel.properties, function (item) { - return item.alias === 'umbracoFile'; - }); - var imageVal; - - //our default images might store one or many images (as csv) - var split = imageProp.value.split(','); - var self = this; - imageVal = _.map(split, function(item) { - return { file: item, isImage: self.detectIfImageByExtension(item) }; - }); - - //for now we'll just return the first image in the collection. - //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. - if (imageVal.length && imageVal.length > 0 && imageVal[0].isImage) { - return imageVal[0].file; - } - } - return ""; - }, - /** formats the display model used to display the content to the model used to save the content */ - getThumbnail: function (options) { - - if (!options && !options.imageModel && !options.scope) { - throw "The options objet does not contain the required parameters: imageModel, scope"; - } - - var imagePropVal = this.getImagePropertyVaue(options); - if (imagePropVal !== "") { - return this.getThumbnailFromPath(imagePropVal); - } - return ""; - }, - getThumbnailFromPath: function(imagePath) { - var ext = imagePath.substr(imagePath.lastIndexOf('.')); - return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_thumb" + ".jpg"; - }, - detectIfImageByExtension: function(imagePath) { - var lowered = imagePath; - var ext = lowered.substr(lowered.lastIndexOf(".") + 1); - return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; - } - }; -} -angular.module('umbraco.services').factory('umbImageHelper', umbImageHelper); - -/** -* @ngdoc service -* @name umbraco.services.umbDataFormatter -* @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server -**/ -function umbDataFormatter() { - return { - /** formats the display model used to display the content to the model used to save the content */ - formatContentPostData: function (displayModel, action) { - //NOTE: the display model inherits from the save model so we can in theory just post up the display model but - // we don't want to post all of the data as it is unecessary. - var saveModel = { - id: displayModel.id, - properties: [], - name: displayModel.name, - contentTypeAlias : displayModel.contentTypeAlias, - parentId: displayModel.parentId, - //set the action on the save model - action: action - }; - _.each(displayModel.tabs, function (tab) { - - _.each(tab.properties, function (prop) { - - //don't include the custom generic tab properties - if (!prop.alias.startsWith("_umb_")) { - saveModel.properties.push({ - id: prop.id, - alias: prop.alias, - value: prop.value - }); - } - - }); - }); - - return saveModel; - } - }; -} -angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter); - -/** -* @ngdoc service -* @name umbraco.services.iconHelper -* @description A helper service for dealing with icons, mostly dealing with legacy tree icons -**/ -function iconHelper() { - - var converter = [ - { oldIcon: ".sprNew", newIcon: "plus" }, - { oldIcon: ".sprDelete", newIcon: "remove" }, - { oldIcon: ".sprMove", newIcon: "move" }, - { oldIcon: ".sprCopy", newIcon: "copy" }, - { oldIcon: ".sprSort", newIcon: "sort" }, - { oldIcon: ".sprPublish", newIcon: "globe" }, - { oldIcon: ".sprRollback", newIcon: "undo" }, - { oldIcon: ".sprProtect", newIcon: "lock" }, - { oldIcon: ".sprAudit", newIcon: "time" }, - { oldIcon: ".sprNotify", newIcon: "envelope" }, - { oldIcon: ".sprDomain", newIcon: "home" }, - { oldIcon: ".sprPermission", newIcon: "group" }, - { oldIcon: ".sprRefresh", newIcon: "refresh" }, - { oldIcon: ".sprBinEmpty", newIcon: "trash" }, - { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" }, - { oldIcon: ".sprImportDocumentType", newIcon: "upload-alt" }, - { oldIcon: ".sprLiveEdit", newIcon: "edit" }, - { oldIcon: ".sprCreateFolder", newIcon: "plus-sign-alt" }, - { oldIcon: ".sprPackage2", newIcon: "gift" }, - { oldIcon: ".sprLogout", newIcon: "signout" }, - { oldIcon: ".sprSave", newIcon: "save" }, - { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" }, - { oldIcon: ".sprToPublish", newIcon: "mail-forward" }, - { oldIcon: ".sprTranslate", newIcon: "comments" }, - { oldIcon: ".sprUpdate", newIcon: "save" }, - - { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" }, - { oldIcon: ".sprTreeDoc", newIcon: "icon-file-alt" }, - { oldIcon: ".sprTreeDoc2", newIcon: "icon-file" }, - { oldIcon: ".sprTreeDoc3", newIcon: "icon-file-text" }, - { oldIcon: ".sprTreeDoc4", newIcon: "icon-file-text-alt" }, - { oldIcon: ".sprTreeDoc5", newIcon: "icon-book" }, - { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" }, - { oldIcon: ".sprTreeFolder", newIcon: "icon-folder-close" }, - { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder-open" }, - { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" }, - { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" }, - { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" }, - - { oldIcon: ".sprTreeMember", newIcon: "icon-mail" }, - { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-group" }, - { oldIcon: ".sprTreeMemberType", newIcon: "icon-group" }, - - { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" }, - { oldIcon: ".sprTreePackage", newIcon: "icon-dropbox" }, - { oldIcon: ".sprTreeRepository", newIcon: "icon-github" }, - - //TODO: - /* - { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, - { oldIcon: ".sprTreeSettingCss", newIcon: "" }, - { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, - { oldIcon: ".sprTreeSettingDataType", newIcon: "" }, - { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, - { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, - { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, - { oldIcon: ".sprTreeSettingScript", newIcon: "" }, - { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, - { oldIcon: ".sprTreeSettingXml", newIcon: "" }, - { oldIcon: ".sprTreeStatistik", newIcon: "" }, - { oldIcon: ".sprTreeUser", newIcon: "" }, - { oldIcon: ".sprTreeUserGroup", newIcon: "" }, - { oldIcon: ".sprTreeUserType", newIcon: "" }, - */ - - - { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" }, - { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" }, - { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" }, - { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" }, - - - //tray icons - { oldIcon: ".traycontent", newIcon: "traycontent" }, - { oldIcon: ".traymedia", newIcon: "traymedia" }, - { oldIcon: ".traysettings", newIcon: "traysettings" }, - { oldIcon: ".traydeveloper", newIcon: "traydeveloper" }, - { oldIcon: ".trayusers", newIcon: "trayusers" }, - { oldIcon: ".traymember", newIcon: "traymember" }, - { oldIcon: ".traytranslation", newIcon: "traytranslation" } - ]; - - return { - - /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ - formatContentTypeThumbnails: function (contentTypes) { - for (var i = 0; i < contentTypes.length; i++) { - if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { - contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); - } - else { - contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;"; - //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - contentTypes[i].cssClass = "custom-file"; - } - } - return contentTypes; - }, - - /** If the icon is file based (i.e. it has a file path) */ - isFileBasedIcon: function (icon) { - //if it doesn't start with a '.' but contains one then we'll assume it's file based - if (!icon.startsWith('.') && icon.indexOf('.') > 1) { - return true; - } - return false; - }, - /** If the icon is legacy */ - isLegacyIcon: function (icon) { - if (icon.startsWith('.')) { - return true; - } - return false; - }, - /** If the tree node has a legacy icon */ - isLegacyTreeNodeIcon: function(treeNode){ - if (treeNode.iconIsClass) { - return this.isLegacyIcon(treeNode.icon); - } - return false; - }, - /** Converts the icon from legacy to a new one if an old one is detected */ - convertFromLegacyIcon: function (icon) { - if (this.isLegacyIcon(icon)) { - //its legacy so convert it if we can - var found = _.find(converter, function (item) { - return item.oldIcon.toLowerCase() === icon.toLowerCase(); - }); - return (found ? found.newIcon : icon); - } - return icon; - }, - /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ - convertFromLegacyTreeNodeIcon: function (treeNode) { - if (this.isLegacyTreeNodeIcon(treeNode)) { - return this.convertFromLegacyIcon(treeNode.icon); - } - return treeNode.icon; - } - }; -} -angular.module('umbraco.services').factory('iconHelper', iconHelper); - - - - -/** - * @ngdoc service - * @name umbraco.services.xmlhelper - * @function - * - * @description - * Used to convert legacy xml data to json and back again - */ -function xmlhelper() { - /* - Copyright 2011 Abdulla Abdurakhmanov - Original sources are available at https://code.google.com/p/x2js/ - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - - function X2JS() { - var VERSION = "1.0.11"; - var escapeMode = false; - - var DOMNodeTypes = { - ELEMENT_NODE : 1, - TEXT_NODE : 3, - CDATA_SECTION_NODE : 4, - DOCUMENT_NODE : 9 - }; - - function getNodeLocalName( node ) { - var nodeLocalName = node.localName; - if(nodeLocalName == null){ - nodeLocalName = node.baseName; - } // Yeah, this is IE!! - - if(nodeLocalName === null || nodeLocalName===""){ - nodeLocalName = node.nodeName; - } // =="" is IE too - - return nodeLocalName; - } - - function getNodePrefix(node) { - return node.prefix; - } - - function escapeXmlChars(str) { - if(typeof(str) === "string"){ - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); - }else{ - return str; - } - } - - function unescapeXmlChars(str) { - return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/'); - } - - function parseDOMChildren( node ) { - var result,child, childName; - - if(node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { - result = {}; - child = node.firstChild; - childName = getNodeLocalName(child); - result[childName] = parseDOMChildren(child); - return result; - } - else{ - - if(node.nodeType === DOMNodeTypes.ELEMENT_NODE) { - result = {}; - result.__cnt=0; - var nodeChildren = node.childNodes; - - // Children nodes - for(var cidx=0; cidx "; - } - - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } - - function jsonXmlSpecialElem ( jsonObj, jsonObjField ) { - if(endsWith(jsonObjField.toString(),("_asArray")) || jsonObjField.toString().indexOf("_")===0 || (jsonObj[jsonObjField] instanceof Function) ){ - return true; - }else{ - return false; - } - } - - function jsonXmlElemCount ( jsonObj ) { - var elementsCnt = 0; - if(jsonObj instanceof Object ) { - for( var it in jsonObj ) { - if(jsonXmlSpecialElem ( jsonObj, it) ){ - continue; - } - elementsCnt++; - } - } - return elementsCnt; - } - - function parseJSONAttributes ( jsonObj ) { - var attrList = []; - if(jsonObj instanceof Object ) { - for( var ait in jsonObj ) { - if(ait.toString().indexOf("__")=== -1 && ait.toString().indexOf("_")===0) { - attrList.push(ait); - } - } - } - - return attrList; - } - - function parseJSONTextAttrs ( jsonTxtObj ) { - var result =""; - - if(jsonTxtObj.__cdata!=null) { - result+=""; - } - - if(jsonTxtObj.__text!=null) { - if(escapeMode){ - result+=escapeXmlChars(jsonTxtObj.__text); - }else{ - result+=jsonTxtObj.__text; - } - } - return result; - } - - function parseJSONTextObject ( jsonTxtObj ) { - var result =""; - - if( jsonTxtObj instanceof Object ) { - result+=parseJSONTextAttrs ( jsonTxtObj ); - } - else{ - if(jsonTxtObj!=null) { - if(escapeMode){ - result+=escapeXmlChars(jsonTxtObj); - }else{ - result+=jsonTxtObj; - } - } - } - - - return result; - } - - function parseJSONArray ( jsonArrRoot, jsonArrObj, attrList ) { - var result = ""; - if(jsonArrRoot.length === 0) { - result+=startTag(jsonArrRoot, jsonArrObj, attrList, true); - } - else { - for(var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { - result+=startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); - result+=parseJSONObject(jsonArrRoot[arIdx]); - result+=endTag(jsonArrRoot[arIdx],jsonArrObj); - } - } - return result; - } - - function parseJSONObject ( jsonObj ) { - var result = ""; - - var elementsCnt = jsonXmlElemCount ( jsonObj ); - - if(elementsCnt > 0) { - for( var it in jsonObj ) { - if(jsonXmlSpecialElem ( jsonObj, it) ){ - continue; - } - - var subObj = jsonObj[it]; - var attrList = parseJSONAttributes( subObj ); - - if(subObj === null || subObj === undefined) { - result+=startTag(subObj, it, attrList, true); - }else{ - if(subObj instanceof Object) { - - if(subObj instanceof Array) { - result+=parseJSONArray( subObj, it, attrList ); - }else { - var subObjElementsCnt = jsonXmlElemCount ( subObj ); - if(subObjElementsCnt > 0 || subObj.__text!==null || subObj.__cdata!==null) { - result+=startTag(subObj, it, attrList, false); - result+=parseJSONObject(subObj); - result+=endTag(subObj,it); - }else{ - result+=startTag(subObj, it, attrList, true); - } - } - - }else { - result+=startTag(subObj, it, attrList, false); - result+=parseJSONTextObject(subObj); - result+=endTag(subObj,it); - } - } - } - } - result+=parseJSONTextObject(jsonObj); - - return result; - } - - this.parseXmlString = function(xmlDocStr) { - var xmlDoc; - if (window.DOMParser) { - var parser=new window.DOMParser(); - xmlDoc = parser.parseFromString( xmlDocStr, "text/xml" ); - } - else { - // IE :( - if(xmlDocStr.indexOf("") + 2 ); - } - xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async="false"; - xmlDoc.loadXML(xmlDocStr); - } - return xmlDoc; - }; - - this.xml2json = function (xmlDoc) { - return parseDOMChildren ( xmlDoc ); - }; - - this.xml_str2json = function (xmlDocStr) { - var xmlDoc = this.parseXmlString(xmlDocStr); - return this.xml2json(xmlDoc); - }; - - this.json2xml_str = function (jsonObj) { - return parseJSONObject ( jsonObj ); - }; - - this.json2xml = function (jsonObj) { - var xmlDocStr = this.json2xml_str (jsonObj); - return this.parseXmlString(xmlDocStr); - }; - - this.getVersion = function () { - return VERSION; - }; - - this.escapeMode = function(enabled) { - escapeMode = enabled; - }; - } - - var x2js = new X2JS(); - return { - /** Called to load in the legacy tree js which is required on startup if a user is logged in or - after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ - toJson: function(xml) { - var json = x2js.xml_str2json( xml ); - return json; - }, - fromJson: function(json) { - var xml = x2js.json2xml_str( json ); - return xml; - } - }; -} -angular.module('umbraco.services').factory('xmlhelper', xmlhelper); \ No newline at end of file +/*Contains multiple services for various helper tasks */ + +/** + * @ngdoc function + * @name umbraco.services.legacyJsLoader + * @function + * + * @description + * Used to lazy load in any JS dependencies that need to be manually loaded in + */ +function legacyJsLoader(assetsService, umbRequestHelper) { + return { + + /** Called to load in the legacy tree js which is required on startup if a user is logged in or + after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ + loadLegacyTreeJs: function(scope) { + return assetsService.loadJs(umbRequestHelper.getApiUrl("legacyTreeJs", "", ""), scope); + } + }; +} +angular.module('umbraco.services').factory('legacyJsLoader', legacyJsLoader); + + +/** + * @ngdoc service + * @name umbraco.services.angularHelper + * @function + * + * @description + * Some angular helper/extension methods + */ +function angularHelper($log, $q) { + return { + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#rejectedPromise + * @methodOf umbraco.services.angularHelper + * @function + * + * @description + * In some situations we need to return a promise as a rejection, normally based on invalid data. This + * is a wrapper to do that so we can save one writing a bit of code. + * + * @param {object} objReject The object to send back with the promise rejection + */ + rejectedPromise: function (objReject) { + var deferred = $q.defer(); + //return an error object including the error message for UI + deferred.reject(objReject); + return deferred.promise; + }, + + /** + * @ngdoc function + * @name safeApply + * @methodOf umbraco.services.angularHelper + * @function + * + * @description + * This checks if a digest/apply is already occuring, if not it will force an apply call + */ + safeApply: function (scope, fn) { + if (scope.$$phase || scope.$root.$$phase) { + if (angular.isFunction(fn)) { + fn(); + } + } + else { + if (angular.isFunction(fn)) { + scope.$apply(fn); + } + else { + scope.$apply(); + } + } + }, + + /** + * @ngdoc function + * @name getCurrentForm + * @methodOf umbraco.services.angularHelper + * @function + * + * @description + * Returns the current form object applied to the scope or null if one is not found + */ + getCurrentForm: function (scope) { + + //NOTE: There isn't a way in angular to get a reference to the current form object since the form object + // is just defined as a property of the scope when it is named but you'll always need to know the name which + // isn't very convenient. If we want to watch for validation changes we need to get a form reference. + // The way that we detect the form object is a bit hackerific in that we detect all of the required properties + // that exist on a form object. + // + //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it + // is to inject the $element object and use: $element.inheritedData('$formController'); + + var form = null; + //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"]; + var requiredFormProps = ["$addControl", "$removeControl", "$setValidity", "$setDirty", "$setPristine"]; + + // a method to check that the collection of object prop names contains the property name expected + function propertyExists(objectPropNames) { + //ensure that every required property name exists on the current scope property + return _.every(requiredFormProps, function (item) { + + return _.contains(objectPropNames, item); + }); + } + + for (var p in scope) { + + if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { + //get the keys of the property names for the current property + var props = _.keys(scope[p]); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + continue; + } + + //ensure that every required property name exists on the current scope property + var containProperty = propertyExists(props); + + if (containProperty) { + form = scope[p]; + break; + } + } + } + + return form; + }, + + /** + * @ngdoc function + * @name validateHasForm + * @methodOf umbraco.services.angularHelper + * @function + * + * @description + * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if + * it does we return the form object. + */ + getRequiredCurrentForm: function(scope) { + var currentForm = this.getCurrentForm(scope); + if (!currentForm || !currentForm.$name) { + throw "The current scope requires a current form object (or ng-form) with a name assigned to it"; + } + return currentForm; + }, + + /** + * @ngdoc function + * @name getNullForm + * @methodOf umbraco.services.angularHelper + * @function + * + * @description + * Returns a null angular FormController, mostly for use in unit tests + * NOTE: This is actually the same construct as angular uses internally for creating a null form but they don't expose + * any of this publicly to us, so we need to create our own. + * + * @param {string} formName The form name to assign + */ + getNullForm: function(formName) { + return { + $addControl: angular.noop, + $removeControl: angular.noop, + $setValidity: angular.noop, + $setDirty: angular.noop, + $setPristine: angular.noop, + $name: formName + //NOTE: we don't include the 'properties', just the methods. + }; + } + }; +} +angular.module('umbraco.services').factory('angularHelper', angularHelper); + +/** +* @ngdoc service +* @name umbraco.services.umbPropertyEditorHelper +* @description A helper object used for property editors +**/ +function umbPropEditorHelper() { + return { + /** + * @ngdoc function + * @name getImagePropertyValue + * @methodOf umbraco.services.umbPropertyEditorHelper + * @function + * + * @description + * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one + * + * @param {string} input the view path currently stored for the property editor + */ + getViewPath: function (input) { + var path = String(input); + if (path.startsWith('/')) { + return path; + } + else { + var pathName = path.replace('.', '/'); + //i.e. views/propertyeditors/fileupload/fileupload.html + return "views/propertyeditors/" + pathName + "/" + pathName + ".html"; + } + } + }; +} +angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); + +/** +* @ngdoc service +* @name umbraco.services.umbImageHelper +* @description A helper object used for parsing image paths +**/ +function umbImageHelper() { + return { + /** Returns the actual image path associated with the image property if there is one */ + getImagePropertyVaue: function(options) { + if (!options && !options.imageModel && !options.scope) { + throw "The options objet does not contain the required parameters: imageModel, scope"; + } + if (options.imageModel.contentTypeAlias.toLowerCase() === "image") { + var imageProp = _.find(options.imageModel.properties, function (item) { + return item.alias === 'umbracoFile'; + }); + var imageVal; + + //our default images might store one or many images (as csv) + var split = imageProp.value.split(','); + var self = this; + imageVal = _.map(split, function(item) { + return { file: item, isImage: self.detectIfImageByExtension(item) }; + }); + + //for now we'll just return the first image in the collection. + //TODO: we should enable returning many to be displayed in the picker if the uploader supports many. + if (imageVal.length && imageVal.length > 0 && imageVal[0].isImage) { + return imageVal[0].file; + } + } + return ""; + }, + /** formats the display model used to display the content to the model used to save the content */ + getThumbnail: function (options) { + + if (!options && !options.imageModel && !options.scope) { + throw "The options objet does not contain the required parameters: imageModel, scope"; + } + + var imagePropVal = this.getImagePropertyVaue(options); + if (imagePropVal !== "") { + return this.getThumbnailFromPath(imagePropVal); + } + return ""; + }, + getThumbnailFromPath: function(imagePath) { + var ext = imagePath.substr(imagePath.lastIndexOf('.')); + return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_thumb" + ".jpg"; + }, + detectIfImageByExtension: function(imagePath) { + var lowered = imagePath; + var ext = lowered.substr(lowered.lastIndexOf(".") + 1); + return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; + } + }; +} +angular.module('umbraco.services').factory('umbImageHelper', umbImageHelper); + +/** +* @ngdoc service +* @name umbraco.services.umbDataFormatter +* @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server +**/ +function umbDataFormatter() { + return { + + /** formats the display model used to display the content to the model used to save the content */ + formatContentPostData: function (displayModel, action) { + //NOTE: the display model inherits from the save model so we can in theory just post up the display model but + // we don't want to post all of the data as it is unecessary. + var saveModel = { + id: displayModel.id, + properties: [], + name: displayModel.name, + contentTypeAlias : displayModel.contentTypeAlias, + parentId: displayModel.parentId, + //set the action on the save model + action: action + }; + + _.each(displayModel.tabs, function (tab) { + + _.each(tab.properties, function (prop) { + + //don't include the custom generic tab properties + if (!prop.alias.startsWith("_umb_")) { + saveModel.properties.push({ + id: prop.id, + alias: prop.alias, + value: prop.value + }); + } + else { + //here we need to map some of our internal properties to the content save item + + switch (prop.alias) { + case "_umb_expiredate": + saveModel.expireDate = prop.value; + break; + case "_umb_releasedate": + saveModel.releaseDate = prop.value; + break; + case "_umb_template": + //this will be a json string + var json = angular.toJson(prop.value); + saveModel.templateAlias = json.alias; + break; + } + } + + }); + }); + + return saveModel; + } + }; +} +angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter); + +/** +* @ngdoc service +* @name umbraco.services.iconHelper +* @description A helper service for dealing with icons, mostly dealing with legacy tree icons +**/ +function iconHelper() { + + var converter = [ + { oldIcon: ".sprNew", newIcon: "plus" }, + { oldIcon: ".sprDelete", newIcon: "remove" }, + { oldIcon: ".sprMove", newIcon: "move" }, + { oldIcon: ".sprCopy", newIcon: "copy" }, + { oldIcon: ".sprSort", newIcon: "sort" }, + { oldIcon: ".sprPublish", newIcon: "globe" }, + { oldIcon: ".sprRollback", newIcon: "undo" }, + { oldIcon: ".sprProtect", newIcon: "lock" }, + { oldIcon: ".sprAudit", newIcon: "time" }, + { oldIcon: ".sprNotify", newIcon: "envelope" }, + { oldIcon: ".sprDomain", newIcon: "home" }, + { oldIcon: ".sprPermission", newIcon: "group" }, + { oldIcon: ".sprRefresh", newIcon: "refresh" }, + { oldIcon: ".sprBinEmpty", newIcon: "trash" }, + { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" }, + { oldIcon: ".sprImportDocumentType", newIcon: "upload-alt" }, + { oldIcon: ".sprLiveEdit", newIcon: "edit" }, + { oldIcon: ".sprCreateFolder", newIcon: "plus-sign-alt" }, + { oldIcon: ".sprPackage2", newIcon: "gift" }, + { oldIcon: ".sprLogout", newIcon: "signout" }, + { oldIcon: ".sprSave", newIcon: "save" }, + { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" }, + { oldIcon: ".sprToPublish", newIcon: "mail-forward" }, + { oldIcon: ".sprTranslate", newIcon: "comments" }, + { oldIcon: ".sprUpdate", newIcon: "save" }, + + { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" }, + { oldIcon: ".sprTreeDoc", newIcon: "icon-file-alt" }, + { oldIcon: ".sprTreeDoc2", newIcon: "icon-file" }, + { oldIcon: ".sprTreeDoc3", newIcon: "icon-file-text" }, + { oldIcon: ".sprTreeDoc4", newIcon: "icon-file-text-alt" }, + { oldIcon: ".sprTreeDoc5", newIcon: "icon-book" }, + { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" }, + { oldIcon: ".sprTreeFolder", newIcon: "icon-folder-close" }, + { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder-open" }, + { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" }, + { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" }, + { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" }, + + { oldIcon: ".sprTreeMember", newIcon: "icon-mail" }, + { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-group" }, + { oldIcon: ".sprTreeMemberType", newIcon: "icon-group" }, + + { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" }, + { oldIcon: ".sprTreePackage", newIcon: "icon-dropbox" }, + { oldIcon: ".sprTreeRepository", newIcon: "icon-github" }, + + //TODO: + /* + { oldIcon: ".sprTreeSettingAgent", newIcon: "" }, + { oldIcon: ".sprTreeSettingCss", newIcon: "" }, + { oldIcon: ".sprTreeSettingCssItem", newIcon: "" }, + { oldIcon: ".sprTreeSettingDataType", newIcon: "" }, + { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" }, + { oldIcon: ".sprTreeSettingDomain", newIcon: "" }, + { oldIcon: ".sprTreeSettingLanguage", newIcon: "" }, + { oldIcon: ".sprTreeSettingScript", newIcon: "" }, + { oldIcon: ".sprTreeSettingTemplate", newIcon: "" }, + { oldIcon: ".sprTreeSettingXml", newIcon: "" }, + { oldIcon: ".sprTreeStatistik", newIcon: "" }, + { oldIcon: ".sprTreeUser", newIcon: "" }, + { oldIcon: ".sprTreeUserGroup", newIcon: "" }, + { oldIcon: ".sprTreeUserType", newIcon: "" }, + */ + + + { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" }, + { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" }, + { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" }, + { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" }, + { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" }, + + + //tray icons + { oldIcon: ".traycontent", newIcon: "traycontent" }, + { oldIcon: ".traymedia", newIcon: "traymedia" }, + { oldIcon: ".traysettings", newIcon: "traysettings" }, + { oldIcon: ".traydeveloper", newIcon: "traydeveloper" }, + { oldIcon: ".trayusers", newIcon: "trayusers" }, + { oldIcon: ".traymember", newIcon: "traymember" }, + { oldIcon: ".traytranslation", newIcon: "traytranslation" } + ]; + + return { + + /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */ + formatContentTypeThumbnails: function (contentTypes) { + for (var i = 0; i < contentTypes.length; i++) { + if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) { + contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail); + } + else { + contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;"; + //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this + contentTypes[i].cssClass = "custom-file"; + } + } + return contentTypes; + }, + + /** If the icon is file based (i.e. it has a file path) */ + isFileBasedIcon: function (icon) { + //if it doesn't start with a '.' but contains one then we'll assume it's file based + if (!icon.startsWith('.') && icon.indexOf('.') > 1) { + return true; + } + return false; + }, + /** If the icon is legacy */ + isLegacyIcon: function (icon) { + if (icon.startsWith('.')) { + return true; + } + return false; + }, + /** If the tree node has a legacy icon */ + isLegacyTreeNodeIcon: function(treeNode){ + if (treeNode.iconIsClass) { + return this.isLegacyIcon(treeNode.icon); + } + return false; + }, + /** Converts the icon from legacy to a new one if an old one is detected */ + convertFromLegacyIcon: function (icon) { + if (this.isLegacyIcon(icon)) { + //its legacy so convert it if we can + var found = _.find(converter, function (item) { + return item.oldIcon.toLowerCase() === icon.toLowerCase(); + }); + return (found ? found.newIcon : icon); + } + return icon; + }, + /** If we detect that the tree node has legacy icons that can be converted, this will convert them */ + convertFromLegacyTreeNodeIcon: function (treeNode) { + if (this.isLegacyTreeNodeIcon(treeNode)) { + return this.convertFromLegacyIcon(treeNode.icon); + } + return treeNode.icon; + } + }; +} +angular.module('umbraco.services').factory('iconHelper', iconHelper); + + + + +/** + * @ngdoc service + * @name umbraco.services.xmlhelper + * @function + * + * @description + * Used to convert legacy xml data to json and back again + */ +function xmlhelper() { + /* + Copyright 2011 Abdulla Abdurakhmanov + Original sources are available at https://code.google.com/p/x2js/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + + function X2JS() { + var VERSION = "1.0.11"; + var escapeMode = false; + + var DOMNodeTypes = { + ELEMENT_NODE : 1, + TEXT_NODE : 3, + CDATA_SECTION_NODE : 4, + DOCUMENT_NODE : 9 + }; + + function getNodeLocalName( node ) { + var nodeLocalName = node.localName; + if(nodeLocalName == null){ + nodeLocalName = node.baseName; + } // Yeah, this is IE!! + + if(nodeLocalName === null || nodeLocalName===""){ + nodeLocalName = node.nodeName; + } // =="" is IE too + + return nodeLocalName; + } + + function getNodePrefix(node) { + return node.prefix; + } + + function escapeXmlChars(str) { + if(typeof(str) === "string"){ + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); + }else{ + return str; + } + } + + function unescapeXmlChars(str) { + return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(///g, '\/'); + } + + function parseDOMChildren( node ) { + var result,child, childName; + + if(node.nodeType === DOMNodeTypes.DOCUMENT_NODE) { + result = {}; + child = node.firstChild; + childName = getNodeLocalName(child); + result[childName] = parseDOMChildren(child); + return result; + } + else{ + + if(node.nodeType === DOMNodeTypes.ELEMENT_NODE) { + result = {}; + result.__cnt=0; + var nodeChildren = node.childNodes; + + // Children nodes + for(var cidx=0; cidx "; + } + + function endsWith(str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + } + + function jsonXmlSpecialElem ( jsonObj, jsonObjField ) { + if(endsWith(jsonObjField.toString(),("_asArray")) || jsonObjField.toString().indexOf("_")===0 || (jsonObj[jsonObjField] instanceof Function) ){ + return true; + }else{ + return false; + } + } + + function jsonXmlElemCount ( jsonObj ) { + var elementsCnt = 0; + if(jsonObj instanceof Object ) { + for( var it in jsonObj ) { + if(jsonXmlSpecialElem ( jsonObj, it) ){ + continue; + } + elementsCnt++; + } + } + return elementsCnt; + } + + function parseJSONAttributes ( jsonObj ) { + var attrList = []; + if(jsonObj instanceof Object ) { + for( var ait in jsonObj ) { + if(ait.toString().indexOf("__")=== -1 && ait.toString().indexOf("_")===0) { + attrList.push(ait); + } + } + } + + return attrList; + } + + function parseJSONTextAttrs ( jsonTxtObj ) { + var result =""; + + if(jsonTxtObj.__cdata!=null) { + result+=""; + } + + if(jsonTxtObj.__text!=null) { + if(escapeMode){ + result+=escapeXmlChars(jsonTxtObj.__text); + }else{ + result+=jsonTxtObj.__text; + } + } + return result; + } + + function parseJSONTextObject ( jsonTxtObj ) { + var result =""; + + if( jsonTxtObj instanceof Object ) { + result+=parseJSONTextAttrs ( jsonTxtObj ); + } + else{ + if(jsonTxtObj!=null) { + if(escapeMode){ + result+=escapeXmlChars(jsonTxtObj); + }else{ + result+=jsonTxtObj; + } + } + } + + + return result; + } + + function parseJSONArray ( jsonArrRoot, jsonArrObj, attrList ) { + var result = ""; + if(jsonArrRoot.length === 0) { + result+=startTag(jsonArrRoot, jsonArrObj, attrList, true); + } + else { + for(var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) { + result+=startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false); + result+=parseJSONObject(jsonArrRoot[arIdx]); + result+=endTag(jsonArrRoot[arIdx],jsonArrObj); + } + } + return result; + } + + function parseJSONObject ( jsonObj ) { + var result = ""; + + var elementsCnt = jsonXmlElemCount ( jsonObj ); + + if(elementsCnt > 0) { + for( var it in jsonObj ) { + if(jsonXmlSpecialElem ( jsonObj, it) ){ + continue; + } + + var subObj = jsonObj[it]; + var attrList = parseJSONAttributes( subObj ); + + if(subObj === null || subObj === undefined) { + result+=startTag(subObj, it, attrList, true); + }else{ + if(subObj instanceof Object) { + + if(subObj instanceof Array) { + result+=parseJSONArray( subObj, it, attrList ); + }else { + var subObjElementsCnt = jsonXmlElemCount ( subObj ); + if(subObjElementsCnt > 0 || subObj.__text!==null || subObj.__cdata!==null) { + result+=startTag(subObj, it, attrList, false); + result+=parseJSONObject(subObj); + result+=endTag(subObj,it); + }else{ + result+=startTag(subObj, it, attrList, true); + } + } + + }else { + result+=startTag(subObj, it, attrList, false); + result+=parseJSONTextObject(subObj); + result+=endTag(subObj,it); + } + } + } + } + result+=parseJSONTextObject(jsonObj); + + return result; + } + + this.parseXmlString = function(xmlDocStr) { + var xmlDoc; + if (window.DOMParser) { + var parser=new window.DOMParser(); + xmlDoc = parser.parseFromString( xmlDocStr, "text/xml" ); + } + else { + // IE :( + if(xmlDocStr.indexOf("") + 2 ); + } + xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async="false"; + xmlDoc.loadXML(xmlDocStr); + } + return xmlDoc; + }; + + this.xml2json = function (xmlDoc) { + return parseDOMChildren ( xmlDoc ); + }; + + this.xml_str2json = function (xmlDocStr) { + var xmlDoc = this.parseXmlString(xmlDocStr); + return this.xml2json(xmlDoc); + }; + + this.json2xml_str = function (jsonObj) { + return parseJSONObject ( jsonObj ); + }; + + this.json2xml = function (jsonObj) { + var xmlDocStr = this.json2xml_str (jsonObj); + return this.parseXmlString(xmlDocStr); + }; + + this.getVersion = function () { + return VERSION; + }; + + this.escapeMode = function(enabled) { + escapeMode = enabled; + }; + } + + var x2js = new X2JS(); + return { + /** Called to load in the legacy tree js which is required on startup if a user is logged in or + after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */ + toJson: function(xml) { + var json = x2js.xml_str2json( xml ); + return json; + }, + fromJson: function(json) { + var xml = x2js.json2xml_str( json ); + return xml; + } + }; +} +angular.module('umbraco.services').factory('xmlhelper', xmlhelper); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.js b/src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.js deleted file mode 100644 index ecbd09432c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.js +++ /dev/null @@ -1,42 +0,0 @@ -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') -.controller("uComponents.Editors.MNTPController", - - function($scope, dialogService, entityResource){ - $scope.ids = $scope.model.value.split(','); - $scope.renderModel = []; - - entityResource.getByIds($scope.ids).then(function(data){ - $(data).each(function(i, item){ - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); - }); - }); - - $scope.openContentPicker =function(){ - var d = dialogService.contentPicker({scope: $scope, callback: populate}); - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - $scope.ids.splice(index, 1); - $scope.model.value = $scope.ids.join(); - }; - - $scope.add =function(item){ - - if($scope.ids.indexOf(item.id) < 0){ - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}) - $scope.ids.push(item.id); - - $scope.model.value = $scope.ids.join(); - } - }; - - - function populate(data){ - $(data.selection).each(function(i, item){ - $scope.add(item); - }); - } -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.html b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.html similarity index 96% rename from src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.html rename to src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.html index 5763dce81c..0ab89bc058 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property editors/mntp/mntp.html +++ b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.html @@ -1,18 +1,18 @@ -
- - -

+
+ + +

\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.js b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.js similarity index 96% rename from src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.js rename to src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.js index ecbd09432c..b345d6f9e0 100644 --- a/src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.js +++ b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/mntp/mntp.js @@ -1,42 +1,42 @@ -//this controller simply tells the dialogs service to open a mediaPicker window -//with a specified callback, this callback will receive an object with a selection on it -angular.module('umbraco') -.controller("uComponents.Editors.MNTPController", - - function($scope, dialogService, entityResource){ - $scope.ids = $scope.model.value.split(','); - $scope.renderModel = []; - - entityResource.getByIds($scope.ids).then(function(data){ - $(data).each(function(i, item){ - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); - }); - }); - - $scope.openContentPicker =function(){ - var d = dialogService.contentPicker({scope: $scope, callback: populate}); - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - $scope.ids.splice(index, 1); - $scope.model.value = $scope.ids.join(); - }; - - $scope.add =function(item){ - - if($scope.ids.indexOf(item.id) < 0){ - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}) - $scope.ids.push(item.id); - - $scope.model.value = $scope.ids.join(); - } - }; - - - function populate(data){ - $(data.selection).each(function(i, item){ - $scope.add(item); - }); - } +//this controller simply tells the dialogs service to open a mediaPicker window +//with a specified callback, this callback will receive an object with a selection on it +angular.module('umbraco') +.controller("uComponents.Editors.MNTPController", + + function($scope, dialogService, entityResource){ + $scope.ids = $scope.model.value.split(','); + $scope.renderModel = []; + + entityResource.getByIds($scope.ids).then(function(data){ + $(data).each(function(i, item){ + $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); + }); + }); + + $scope.openContentPicker =function(){ + var d = dialogService.contentPicker({scope: $scope, callback: populate}); + }; + + $scope.remove =function(index){ + $scope.renderModel.splice(index, 1); + $scope.ids.splice(index, 1); + $scope.model.value = $scope.ids.join(); + }; + + $scope.add =function(item){ + + if($scope.ids.indexOf(item.id) < 0){ + $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}) + $scope.ids.push(item.id); + + $scope.model.value = $scope.ids.join(); + } + }; + + + function populate(data){ + $(data.selection).each(function(i, item){ + $scope.add(item); + }); + } }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/property editors/package.manifest b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/package.manifest similarity index 59% rename from src/Umbraco.Web.UI.Client/src/packages/property editors/package.manifest rename to src/Umbraco.Web.UI.Client/src/packages/propertyeditors/package.manifest index eef3ecd293..c7544dc3f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property editors/package.manifest +++ b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/package.manifest @@ -1,30 +1,30 @@ -{ - propertyEditors: [ - { - id: "7e062c13-7c41-4ad9-b389-41d88aeef87c", - name: "Multinode treepicker", - editor: { - view: "~/App_Plugins/property editors/mntp/mntp.html" - } - }, - { - id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", - name: "Related links", - editor: { - view: "~/App_Plugins/property editors/relatedlinks/relatedlinks.html" - } - }, - { - id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", - name: "Boolean", - editor: { - view: "~/Umbraco/views/propertyeditors/boolean/boolean.html" - } - } - ] - , - javascript: [ - '~/App_Plugins/property editors/relatedlinks/relatedlinks.js', - '~/App_Plugins/property editors/mntp/mntp.js' - ] -} \ No newline at end of file +{ + propertyEditors: [ + { + id: "7e062c13-7c41-4ad9-b389-41d88aeef87c", + name: "Multinode treepicker", + editor: { + view: "~/App_Plugins/propertyeditors/mntp/mntp.html" + } + }, + { + id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", + name: "Related links", + editor: { + view: "~/App_Plugins/propertyeditors/relatedlinks/relatedlinks.html" + } + }, + { + id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", + name: "Boolean", + editor: { + view: "~/Umbraco/views/propertyeditors/boolean/boolean.html" + } + } + ] + , + javascript: [ + '~/App_Plugins/propertyeditors/relatedlinks/relatedlinks.js', + '~/App_Plugins/propertyeditors/mntp/mntp.js' + ] +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/relatedlinks/relatedlinks.html b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/relatedlinks/relatedlinks.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/relatedlinks/relatedlinks.js b/src/Umbraco.Web.UI.Client/src/packages/propertyeditors/relatedlinks/relatedlinks.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 6097f57d6e..cb014917e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -1,4 +1,4 @@ - +
@@ -48,4 +48,4 @@ - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js new file mode 100644 index 0000000000..174e3816bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -0,0 +1,15 @@ +function booleanEditorController($scope, $rootScope, assetsService) { + + $scope.renderModel = { + value: false + }; + if ($scope.model && $scope.model.value && ($scope.model.value.toString() === "1" || angular.lowercase($scope.model.value) === "true")) { + $scope.renderModel.value = true; + } + + $scope.$watch("renderModel.value", function (newVal) { + $scope.model.value = newVal === true ? "1" : "0"; + }); + +} +angular.module("umbraco").controller("Umbraco.Editors.BooleanController", booleanEditorController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html index d01704ffe4..7f606e583c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html @@ -1 +1,3 @@ - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/readonlyvalue/readonlyvalue.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/readonlyvalue/readonlyvalue.controller.js index 6d57ec1030..268e53b841 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/readonlyvalue/readonlyvalue.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/readonlyvalue/readonlyvalue.controller.js @@ -1,8 +1,21 @@ -angular.module('umbraco').controller("Umbraco.Editors.ReadOnlyValueController", - function($rootScope, $scope, $filter){ - if ($scope.model.config && $scope.model.config.filter && $scope.model.config.format) { - $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.filter); - }else{ - $scope.displayvalue = $scope.model.value; - } -}); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.Editors.ReadOnlyValueController", + function($rootScope, $scope, $filter){ + + if ($scope.model.config && + angular.isArray($scope.model.config) && + $scope.model.config.length > 0 && + $scope.model.config[0] && + $scope.model.config.filter) + { + + if ($scope.model.config.format) { + $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format); + } + else { + $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value); + } + } + else { + $scope.displayvalue = $scope.model.value; + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/templatepicker/templatepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/templatepicker/templatepicker.html new file mode 100644 index 0000000000..c974776bae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/templatepicker/templatepicker.html @@ -0,0 +1,4 @@ +
+
TODO: Implement this picker
+
{{model.value | json}}
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js new file mode 100644 index 0000000000..9abc45d2f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js @@ -0,0 +1,11 @@ +angular.module('umbraco').controller("Umbraco.Editors.UrlListController", + function($rootScope, $scope, $filter) { + + $scope.renderModel = _.map($scope.model.value.split(","), function(item) { + return { + url: item, + urlTarget : ($scope.config && $scope.config.target) ? $scope.config.target : "_blank" + }; + }); + + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html new file mode 100644 index 0000000000..7255b5832f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html @@ -0,0 +1,7 @@ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.html b/src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.html deleted file mode 100644 index 5763dce81c..0000000000 --- a/src/Umbraco.Web.UI/App_Plugins/property editors/mntp/mntp.html +++ /dev/null @@ -1,18 +0,0 @@ -
- - -

-
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Plugins/property editors/package.manifest b/src/Umbraco.Web.UI/App_Plugins/property editors/package.manifest deleted file mode 100644 index eef3ecd293..0000000000 --- a/src/Umbraco.Web.UI/App_Plugins/property editors/package.manifest +++ /dev/null @@ -1,30 +0,0 @@ -{ - propertyEditors: [ - { - id: "7e062c13-7c41-4ad9-b389-41d88aeef87c", - name: "Multinode treepicker", - editor: { - view: "~/App_Plugins/property editors/mntp/mntp.html" - } - }, - { - id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", - name: "Related links", - editor: { - view: "~/App_Plugins/property editors/relatedlinks/relatedlinks.html" - } - }, - { - id: "71b8ad1a-8dc2-425c-b6b8-faa158075e63", - name: "Boolean", - editor: { - view: "~/Umbraco/views/propertyeditors/boolean/boolean.html" - } - } - ] - , - javascript: [ - '~/App_Plugins/property editors/relatedlinks/relatedlinks.js', - '~/App_Plugins/property editors/mntp/mntp.js' - ] -} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 9ccb07b99c..c64e035b79 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -192,7 +192,23 @@ namespace Umbraco.Web.Editors //TODO: We need to support 'send to publish' //TODO: We'll need to save the new template, publishat, etc... values here - + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + if (contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + contentItem.PersistedContent.Template = template; + } + } + MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -232,12 +248,12 @@ namespace Umbraco.Web.Editors if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { //save the item - Services.ContentService.Save(contentItem.PersistedContent); + Services.ContentService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); } else { //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = ((ContentService)Services.ContentService).SaveAndPublishInternal(contentItem.PersistedContent); + publishStatus = ((ContentService)Services.ContentService).SaveAndPublishInternal(contentItem.PersistedContent, (int)Security.CurrentUser.Id); } diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 6966e4d6db..967031a399 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -1,141 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Editors; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.Web.Editors -{ - /// - /// An abstract base controller used for media/content (and probably members) to try to reduce code replication. - /// - [OutgoingDateTimeFormat] - public abstract class ContentControllerBase : UmbracoAuthorizedJsonController - { - /// - /// Constructor - /// - protected ContentControllerBase() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - protected ContentControllerBase(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - protected HttpResponseMessage HandleContentNotFound(int id, bool throwException = true) - { - ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id)); - var errorResponse = Request.CreateErrorResponse( - HttpStatusCode.NotFound, - ModelState); - if (throwException) - { - throw new HttpResponseException(errorResponse); - } - return errorResponse; - } - - protected void UpdateName(ContentItemSave contentItem) - where TPersisted : IContentBase - { - //Don't update the name if it is empty - if (!contentItem.Name.IsNullOrWhiteSpace()) - { - contentItem.PersistedContent.Name = contentItem.Name; - } - } - - protected HttpResponseMessage PerformSort(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); - } - - return null; - } - - protected void MapPropertyValues(ContentItemSave contentItem) - where TPersisted : IContentBase - { - //Map the property values - foreach (var p in contentItem.ContentDto.Properties) - { - //get the dbo property - var dboProperty = contentItem.PersistedContent.Properties[p.Alias]; - - //create the property data to send to the property editor - var d = new Dictionary(); - //add the files if any - var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray(); - if (files.Any()) - { - d.Add("files", files); - } - var data = new ContentPropertyData(p.Value, d); - - //get the deserialized value from the property editor - if (p.PropertyEditor == null) - { - LogHelper.Warn("No property editor found for property " + p.Alias); - } - else - { - dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value); - } - } - } - - protected void HandleInvalidModelState(ContentItemDisplayBase display) - where TPersisted : IContentBase - where T : ContentPropertyBasic - { - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - if (!ModelState.IsValid) - { - display.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display)); - } - } - - /// - /// A helper method to attempt to get the instance from the request storage if it can be found there, - /// otherwise gets it from the callback specified - /// - /// - /// - /// - /// - /// This is useful for when filters have alraedy looked up a persisted entity and we don't want to have - /// to look it up again. - /// - protected TPersisted GetEntityFromRequest(Func getFromService) - where TPersisted : IContentBase - { - return Request.Properties.ContainsKey(typeof (TPersisted).ToString()) == false - ? getFromService() - : (TPersisted) Request.Properties[typeof (TPersisted).ToString()]; - } - - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Editors +{ + /// + /// An abstract base controller used for media/content (and probably members) to try to reduce code replication. + /// + [OutgoingDateTimeFormat] + public abstract class ContentControllerBase : UmbracoAuthorizedJsonController + { + /// + /// Constructor + /// + protected ContentControllerBase() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + protected ContentControllerBase(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + protected HttpResponseMessage HandleContentNotFound(int id, bool throwException = true) + { + ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id)); + var errorResponse = Request.CreateErrorResponse( + HttpStatusCode.NotFound, + ModelState); + if (throwException) + { + throw new HttpResponseException(errorResponse); + } + return errorResponse; + } + + protected void UpdateName(ContentItemSave contentItem) + where TPersisted : IContentBase + { + //Don't update the name if it is empty + if (!contentItem.Name.IsNullOrWhiteSpace()) + { + contentItem.PersistedContent.Name = contentItem.Name; + } + } + + protected HttpResponseMessage PerformSort(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); + } + + return null; + } + + protected void MapPropertyValues(ContentItemSave contentItem) + where TPersisted : IContentBase + { + //Map the property values + foreach (var p in contentItem.ContentDto.Properties) + { + //get the dbo property + var dboProperty = contentItem.PersistedContent.Properties[p.Alias]; + + //create the property data to send to the property editor + var d = new Dictionary(); + //add the files if any + var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray(); + if (files.Any()) + { + d.Add("files", files); + } + var data = new ContentPropertyData(p.Value, d); + + //get the deserialized value from the property editor + if (p.PropertyEditor == null) + { + LogHelper.Warn("No property editor found for property " + p.Alias); + } + else + { + var valueEditor = p.PropertyEditor.ValueEditor; + //don't persist any bound value if the editor is readonly + if (valueEditor.IsReadOnly == false) + { + dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value); + } + + } + } + } + + protected void HandleInvalidModelState(ContentItemDisplayBase display) + where TPersisted : IContentBase + where T : ContentPropertyBasic + { + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + if (!ModelState.IsValid) + { + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display)); + } + } + + /// + /// A helper method to attempt to get the instance from the request storage if it can be found there, + /// otherwise gets it from the callback specified + /// + /// + /// + /// + /// + /// This is useful for when filters have alraedy looked up a persisted entity and we don't want to have + /// to look it up again. + /// + protected TPersisted GetEntityFromRequest(Func getFromService) + where TPersisted : IContentBase + { + return Request.Properties.ContainsKey(typeof (TPersisted).ToString()) == false + ? getFromService() + : (TPersisted) Request.Properties[typeof (TPersisted).ToString()]; + } + + } +} diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 10046b64fc..3e0663792c 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -1,48 +1,49 @@ -using System.Collections.Generic; -using AutoMapper; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using System.Linq; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for using the list of sections - /// - [PluginController("UmbracoApi")] - public class EntityController : UmbracoAuthorizedJsonController - { - public IUmbracoEntity GetById(int id) - { - return Services.EntityService.Get(id); - } - - public IEnumerable GetByIds(int[] ids) - { - var list = new List(); - foreach(var id in ids) - list.Add(Services.EntityService.Get(id)); - - return list; - } - - - //public IEnumerable GetContentByIds(int[] ids) - //{ - // var list = new List(); - // foreach (var id in ids) - // list.Add((UmbracoEntity)Services.EntityService.Get(id)); - - // return list; - //} - - //public UmbracoEntity GetContentById(int id) - //{ - // return (UmbracoEntity)Services.EntityService.Get(id); - //} - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using System.Linq; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for using the list of sections + /// + [PluginController("UmbracoApi")] + public class EntityController : UmbracoAuthorizedJsonController + { + public IUmbracoEntity GetById(int id) + { + return Services.EntityService.Get(id); + } + + public IEnumerable GetByIds([FromUri]int[] ids) + { + if (ids == null) throw new ArgumentNullException("ids"); + + return ids.Select(id => Services.EntityService.Get(id)).Where(entity => entity != null).ToList(); + } + + + //public IEnumerable GetContentByIds(int[] ids) + //{ + // var list = new List(); + // foreach (var id in ids) + // list.Add((UmbracoEntity)Services.EntityService.Get(id)); + + // return list; + //} + + //public UmbracoEntity GetContentById(int id) + //{ + // return (UmbracoEntity)Services.EntityService.Get(id); + //} + } +} diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index af6e1f8512..7c04ecc434 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -157,7 +157,7 @@ namespace Umbraco.Web.Editors } //save the item - Services.MediaService.Save(contentItem.PersistedContent); + Services.MediaService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); //return the updated model var display = Mapper.Map(contentItem.PersistedContent); diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index e08540b242..57bfcbe006 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -5,6 +5,7 @@ using System.Runtime.Serialization; using System.Web.Http; using System.Web.Http.ModelBinding; using Umbraco.Core.Models; +using Umbraco.Core.Models.Validation; namespace Umbraco.Web.Models.ContentEditing { @@ -17,6 +18,34 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "publishDate")] public DateTime? PublishDate { get; set; } + + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } + + [DataMember(Name = "removeDate")] + public DateTime? ExpireDate { get; set; } + + [DataMember(Name = "template")] + public TemplateBasic Template { get; set; } + + [DataMember(Name = "urls")] + public string[] Urls { get; set; } } + + [DataContract(Name = "template", Namespace = "")] + public class TemplateBasic + { + [DataMember(Name = "id", IsRequired = true)] + [Required] + public int Id { get; set; } + + [DataMember(Name = "name", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string Name { get; set; } + + [DataMember(Name = "alias", IsRequired = true)] + [Required(AllowEmptyStrings = false)] + public string Alias { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs index 7cd7ae4ec6..676c694793 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -1,33 +1,39 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; -using Umbraco.Core.Models; - -namespace Umbraco.Web.Models.ContentEditing -{ - public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel - where T : ContentPropertyBasic - where TPersisted : IContentBase - { - protected ContentItemDisplayBase() - { - Notifications = new List(); - } - - /// - /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. - /// - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - - /// - /// This is used for validation of a content item. - /// - /// - /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will - /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the - /// updated model. - /// - [DataMember(Name = "modelState")] - public IDictionary Errors { get; set; } - } -} \ No newline at end of file +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.ContentEditing +{ + public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel + where T : ContentPropertyBasic + where TPersisted : IContentBase + { + protected ContentItemDisplayBase() + { + Notifications = new List(); + } + + /// + /// The name of the content type + /// + [DataMember(Name = "contentTypeName")] + public string ContentTypeName { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// + [DataMember(Name = "modelState")] + public IDictionary Errors { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs index b462c75280..9c04a3adcf 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; using System.ComponentModel.DataAnnotations; @@ -25,6 +26,18 @@ namespace Umbraco.Web.Models.ContentEditing [Required] public ContentSaveAction Action { get; set; } + /// + /// The template alias to save + /// + [DataMember(Name = "templateAlias")] + public string TemplateAlias { get; set; } + + [DataMember(Name = "releaseDate")] + public DateTime? ReleaseDate { get; set; } + + [DataMember(Name = "expireDate")] + public DateTime? ExpireDate { get; set; } + /// /// The collection of files uploaded /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index d9fab62343..bdcaa6de7c 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -1,13 +1,20 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using AutoMapper; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; +using umbraco; +using Umbraco.Web.Routing; namespace Umbraco.Web.Models.Mapping { + /// /// Declares how model mappings for content /// @@ -15,6 +22,7 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + //FROM IContent TO ContentItemDisplay config.CreateMap() .ForMember( @@ -29,11 +37,58 @@ namespace Umbraco.Web.Models.Mapping .ForMember( dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember( + dto => dto.ContentTypeName, + expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember( dto => dto.PublishDate, expression => expression.MapFrom(content => GetPublishedDate(content, applicationContext))) + .ForMember( + dto => dto.Template, + expression => expression.MapFrom(content => new TemplateBasic + { + Alias = content.Template.Alias, + Id = content.Template.Id, + Name = content.Template.Name + })) + .ForMember( + dto => dto.Urls, + expression => expression.MapFrom(content => + UmbracoContext.Current == null + ? new[] {"Cannot generate urls without a current Umbraco Context"} + : content.GetContentUrls())) .ForMember(display => display.Properties, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()); + .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) + .AfterMap((content, display) => TabsAndPropertiesResolver.MapGenericProperties( + content, display, + new ContentPropertyDisplay + { + Alias = string.Format("{0}releasedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "releaseDate"), + Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, + View = "datepicker" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}expiredate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "removeDate"), + Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, + View = "datepicker" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}template", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = "Template", //TODO: localize this? + Value = JsonConvert.SerializeObject(display.Template), + View = "templatepicker" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "urls"), + Value = string.Join(",", display.Urls), + View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + })); //FROM IContent TO ContentItemBasic config.CreateMap>() diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index f610c08926..fe3643da87 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -29,9 +29,13 @@ namespace Umbraco.Web.Models.Mapping expression => expression.MapFrom(content => content.ContentType.Icon)) .ForMember( dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) + expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember( + dto => dto.ContentTypeName, + expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember(display => display.Properties, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()); + .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) + .AfterMap((media, display) => TabsAndPropertiesResolver.MapGenericProperties(media, display)); //FROM IMedia TO ContentItemBasic config.CreateMap>() diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 324d9af352..b9d6e0c28d 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -1,68 +1,149 @@ -using System.Collections.Generic; -using System.Linq; -using AutoMapper; -using Umbraco.Core.Models; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Models.Mapping -{ - /// - /// Creates the tabs collection with properties assigned for display models - /// - internal class TabsAndPropertiesResolver : ValueResolver>> - { - protected override IEnumerable> ResolveCore(IContentBase content) - { - var aggregateTabs = new List>(); - - //now we need to aggregate the tabs and properties since we might have duplicate tabs (based on aliases) because - // of how content composition works. - foreach (var propertyGroups in content.PropertyGroups.GroupBy(x => x.Name)) - { - var aggregateProperties = new List(); - - //there will always be one group with a null parent id (the top-most) - //then we'll iterate over all of the groups and ensure the properties are - //added in order so that when they render they are rendered with highest leve - //parent properties first. - int? currentParentId = null; - for (var i = 0; i < propertyGroups.Count(); i++) - { - var current = propertyGroups.Single(x => x.ParentId == currentParentId); - aggregateProperties.AddRange( - Mapper.Map, IEnumerable>( - content.GetPropertiesForGroup(current))); - currentParentId = current.Id; - } - - //then we'll just use the root group's data to make the composite tab - var rootGroup = propertyGroups.Single(x => x.ParentId == null); - aggregateTabs.Add(new Tab - { - Id = rootGroup.Id, - Alias = rootGroup.Name, - Label = rootGroup.Name, - Properties = aggregateProperties, - IsActive = false - }); - } - - //now add the generic properties tab for any properties that don't belong to a tab - var orphanProperties = content.GetNonGroupedProperties(); - - //now add the generic properties tab - aggregateTabs.Add(new Tab - { - Id = 0, - Label = "Generic properties", - Alias = "Generic properties", - Properties = Mapper.Map, IEnumerable>(orphanProperties) - }); - - //set the first tab to active - aggregateTabs.First().IsActive = true; - - return aggregateTabs; - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; +using umbraco; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Creates the tabs collection with properties assigned for display models + /// + internal class TabsAndPropertiesResolver : ValueResolver>> + { + /// + /// Maps properties on to the generic properties tab + /// + /// + /// + /// + /// Any additional custom properties to assign to the generic properties tab. + /// + /// + /// The generic properties tab is mapped during AfterMap and is responsible for + /// setting up the properties such as Created date, udpated date, template selected, etc... + /// + public static void MapGenericProperties( + TPersisted content, + ContentItemDisplayBase display, + params ContentPropertyDisplay[] customProperties) + where TPersisted : IContentBase + { + var genericProps = display.Tabs.Single(x => x.Id == 0); + + //store the current props to append to the newly inserted ones + var currProps = genericProps.Properties.ToArray(); + + var labelEditor = PropertyEditorResolver.Current.GetById(new Guid(Constants.PropertyEditors.NoEdit)).ValueEditor.View; + + var contentProps = new List + { + new ContentPropertyDisplay + { + Alias = string.Format("{0}id", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = "Id", + Value = display.Id.ToInvariantString(), + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}creator", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "createBy"), + Description = "Original author", //TODO: Localize this + Value = display.Owner.Name, + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}createdate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "createDate"), + Description = "Date/time this document was created", //TODO: Localize this + Value = display.CreateDate.ToIsoString(), + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}updatedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "updateDate"), + Description = "Date/time this document was created", //TODO: Localize this + Value = display.UpdateDate.ToIsoString(), + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = ui.Text("content", "documentType"), + Value = display.ContentTypeName, + View = labelEditor + } + }; + + //add the custom ones + contentProps.AddRange(customProperties); + + //now add the user props + contentProps.AddRange(currProps); + + //re-assign + genericProps.Properties = contentProps; + } + + protected override IEnumerable> ResolveCore(IContentBase content) + { + var aggregateTabs = new List>(); + + //now we need to aggregate the tabs and properties since we might have duplicate tabs (based on aliases) because + // of how content composition works. + foreach (var propertyGroups in content.PropertyGroups.GroupBy(x => x.Name)) + { + var aggregateProperties = new List(); + + //there will always be one group with a null parent id (the top-most) + //then we'll iterate over all of the groups and ensure the properties are + //added in order so that when they render they are rendered with highest leve + //parent properties first. + int? currentParentId = null; + for (var i = 0; i < propertyGroups.Count(); i++) + { + var current = propertyGroups.Single(x => x.ParentId == currentParentId); + aggregateProperties.AddRange( + Mapper.Map, IEnumerable>( + content.GetPropertiesForGroup(current))); + currentParentId = current.Id; + } + + //then we'll just use the root group's data to make the composite tab + var rootGroup = propertyGroups.Single(x => x.ParentId == null); + aggregateTabs.Add(new Tab + { + Id = rootGroup.Id, + Alias = rootGroup.Name, + Label = rootGroup.Name, + Properties = aggregateProperties, + IsActive = false + }); + } + + //now add the generic properties tab for any properties that don't belong to a tab + var orphanProperties = content.GetNonGroupedProperties(); + + //now add the generic properties tab + aggregateTabs.Add(new Tab + { + Id = 0, + Label = "Generic properties", + Alias = "Generic properties", + Properties = Mapper.Map, IEnumerable>(orphanProperties) + }); + + //set the first tab to active + aggregateTabs.First().IsActive = true; + + return aggregateTabs; + } + } +} diff --git a/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs b/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs index 63eaaa5e9a..02ff701c8b 100644 --- a/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.Mvc /// Releases the specified controller. ///
/// The controller. - public void ReleaseController(IController controller) + public virtual void ReleaseController(IController controller) { _innerFactory.ReleaseController(controller); } diff --git a/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs new file mode 100644 index 0000000000..53c5ee94ad --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.NoEdit, "Label", "readonlyvalue")] + public class LabelPropertyEditor : PropertyEditor + { + + protected override ValueEditor CreateValueEditor() + { + var baseEditor = base.CreateValueEditor(); + + return new LabelValueEditor + { + View = baseEditor.View + }; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/LabelValueEditor.cs b/src/Umbraco.Web/PropertyEditors/LabelValueEditor.cs new file mode 100644 index 0000000000..2e514c4463 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/LabelValueEditor.cs @@ -0,0 +1,18 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Custom value editor to mark it as readonly + /// + internal class LabelValueEditor : ValueEditor + { + /// + /// This editor is for display purposes only, any values bound to it will not be saved back to the database + /// + public override bool IsReadOnly + { + get { return true; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextStringPropertyEditor.cs index 1218b31fef..313b8e4826 100644 --- a/src/Umbraco.Web/PropertyEditors/TextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextStringPropertyEditor.cs @@ -1,16 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - - [PropertyEditor(Constants.PropertyEditors.Textbox, "Textstring", "textstring")] - public class TextStringPropertyEditor : PropertyEditor - { - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.Textbox, "Textstring", "textstring")] + public class TextStringPropertyEditor : PropertyEditor + { + } +} diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 62d4142ec6..b8020ff04f 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Web.PublishedCache; +using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings; namespace Umbraco.Web.Routing { diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs new file mode 100644 index 0000000000..4bd1ac3c8c --- /dev/null +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; +using umbraco; + +namespace Umbraco.Web.Routing +{ + internal static class UrlProviderExtensions + { + /// + /// Gets the URLs for the content item + /// + /// + /// + /// + /// Use this when displaying URLs, if there are errors genertaing the urls the urls themselves will + /// contain the errors. + /// + public static IEnumerable GetContentUrls(this IContent content) + { + var urlProvider = UmbracoContext.Current.RoutingContext.UrlProvider; + var url = urlProvider.GetUrl(content.Id); + var urls = new List(); + if (url == "#") + { + // document as a published version yet it's url is "#" => a parent must be + // unpublished, walk up the tree until we find it, and report. + var parent = content; + do + { + parent = parent.ParentId > 0 ? content.Parent() : null; + } + while (parent != null && parent.Published); + + if (parent == null) // oops - internal error + urls.Add(ui.Text("content", "parentNotPublishedAnomaly", UmbracoContext.Current.Security.CurrentUser)); + else + urls.Add(ui.Text("content", "parentNotPublished", parent.Name, UmbracoContext.Current.Security.CurrentUser)); + } + else + { + urls.Add(url); + urls.AddRange(urlProvider.GetOtherUrls(content.Id)); + } + return urls; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2a730f08ba..9c34f2c0ff 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -300,6 +300,9 @@ + + +