diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 92bdf3c790..23a6bcfe5b 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models @@ -47,13 +48,11 @@ namespace Umbraco.Core.Models public UmbracoEntity() { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); } public UmbracoEntity(bool trashed) { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); Trashed = trashed; } @@ -61,7 +60,6 @@ namespace Umbraco.Core.Models public UmbracoEntity(UInt64 trashed) { AdditionalData = new Dictionary(); - UmbracoProperties = new List(); Trashed = trashed == 1; } @@ -287,15 +285,31 @@ namespace Umbraco.Core.Models } } + public override object DeepClone() + { + var clone = (UmbracoEntity) base.DeepClone(); + + //This ensures that any value in the dictionary that is deep cloneable is cloned too + foreach (var key in clone.AdditionalData.Keys.ToArray()) + { + var deepCloneable = clone.AdditionalData[key] as IDeepCloneable; + if (deepCloneable != null) + { + clone.AdditionalData[key] = deepCloneable.DeepClone(); + } + } + + return clone; + } + /// - /// Some entities may expose additional data that other's might not, this custom data will be available in this collection + /// A struction that can be contained in the additional data of an UmbracoEntity representing + /// a user defined property /// - public IList UmbracoProperties { get; set; } - - internal class UmbracoProperty : IDeepCloneable + public class EntityProperty : IDeepCloneable { public string PropertyEditorAlias { get; set; } - public string Value { get; set; } + public object Value { get; set; } public object DeepClone() { //Memberwise clone on Entity will work since it doesn't have any deep elements @@ -304,7 +318,7 @@ namespace Umbraco.Core.Models return clone; } - protected bool Equals(UmbracoProperty other) + protected bool Equals(EntityProperty other) { return PropertyEditorAlias.Equals(other.PropertyEditorAlias) && string.Equals(Value, other.Value); } @@ -314,7 +328,7 @@ namespace Umbraco.Core.Models if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((UmbracoProperty) obj); + return Equals((EntityProperty) obj); } public override int GetHashCode() diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index db1a99bc45..f33ae1af2f 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -44,7 +44,6 @@ namespace Umbraco.Core.Persistence.Factories ContentTypeAlias = asDictionary.ContainsKey("alias") ? (d.alias ?? string.Empty) : string.Empty, ContentTypeIcon = asDictionary.ContainsKey("icon") ? (d.icon ?? string.Empty) : string.Empty, ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty, - UmbracoProperties = new List() }; var publishedVersion = default(Guid); @@ -87,7 +86,6 @@ namespace Umbraco.Core.Persistence.Factories ContentTypeAlias = dto.Alias ?? string.Empty, ContentTypeIcon = dto.Icon ?? string.Empty, ContentTypeThumbnail = dto.Thumbnail ?? string.Empty, - UmbracoProperties = new List() }; entity.IsPublished = dto.PublishedVersion != default(Guid) || (dto.NewestVersion != default(Guid) && dto.PublishedVersion == dto.NewestVersion); @@ -98,12 +96,13 @@ namespace Umbraco.Core.Persistence.Factories { foreach (var propertyDto in dto.UmbracoPropertyDtos) { - entity.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty - { - PropertyEditorAlias = - propertyDto.PropertyEditorAlias, - Value = propertyDto.UmbracoFile - }); + entity.AdditionalData[propertyDto.PropertyAlias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = propertyDto.PropertyEditorAlias, + Value = propertyDto.NTextValue.IsNullOrWhiteSpace() + ? propertyDto.NVarcharValue + : propertyDto.NTextValue.ConvertToJsonIfPossible() + }; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 7336e36ab7..02f64aa0cf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -66,15 +66,31 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); + var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); + + if (isMedia) + { + //for now treat media differently + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( + new UmbracoEntityRelator().Map, sql); - return entity; + return entities.FirstOrDefault(); + } + else + { + var nodeDto = _work.Database.FirstOrDefault(sql); + if (nodeDto == null) + return null; + + var factory = new UmbracoEntityFactory(); + var entity = factory.BuildEntityFromDynamic(nodeDto); + + return entity; + } + + } public virtual IUmbracoEntity Get(int id) @@ -94,16 +110,31 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; + var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); + + if (isMedia) + { + //for now treat media differently + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( + new UmbracoEntityRelator().Map, sql); - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); + return entities.FirstOrDefault(); + } + else + { + var nodeDto = _work.Database.FirstOrDefault(sql); + if (nodeDto == null) + return null; - return entity; + var factory = new UmbracoEntityFactory(); + var entity = factory.BuildEntityFromDynamic(nodeDto); + + return entity; + } + + } public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) @@ -119,8 +150,8 @@ namespace Umbraco.Core.Persistence.Repositories { bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); - var sql = GetBaseWhere(GetBase, isContent, isMedia, string.Empty, objectTypeId).Append(GetGroupBy(isContent, isMedia)); - + var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, string.Empty); + var factory = new UmbracoEntityFactory(); if (isMedia) @@ -166,24 +197,28 @@ namespace Umbraco.Core.Persistence.Repositories bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var wheres = string.Concat(" AND ", string.Join(" AND ", ((Query)query).WhereClauses())); + var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, wheres, objectTypeId); + var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().Append(GetGroupBy(isContent, isMedia)); + var entitySql = translator.Translate(); var factory = new UmbracoEntityFactory(); if (isMedia) { + var mediaSql = GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false)), wheres); + //treat media differently for now //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields var entities = _work.Database.Fetch( - new UmbracoEntityRelator().Map, sql); + new UmbracoEntityRelator().Map, mediaSql); return entities; } else { //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData - var dtos = _work.Database.Fetch(sql); + var dtos = _work.Database.Fetch(entitySql.Append(GetGroupBy(isContent, false))); return dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); } } @@ -193,6 +228,62 @@ namespace Umbraco.Core.Persistence.Repositories #region Sql Statements + protected Sql GetFullSqlForEntityType(Guid key, bool isContent, bool isMedia, Guid objectTypeId) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + protected Sql GetFullSqlForEntityType(int id, bool isContent, bool isMedia, Guid objectTypeId) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectTypeId, string additionalWhereClause) + { + var entitySql = GetBaseWhere(GetBase, isContent, isMedia, additionalWhereClause, objectTypeId); + + if (isMedia == false) return entitySql.Append(GetGroupBy(isContent, false)); + + return GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))); + } + + private Sql GetFullSqlForMedia(Sql entitySql, string additionWhereStatement = "") + { + //this will add any dataNvarchar property to the output which can be added to the additional properties + + var joinSql = new Sql() + .Select("contentNodeId, versionId, dataNvarchar, dataNtext, propertyEditorAlias, alias as propertyTypeAlias") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .InnerJoin() + .On(dto => dto.Id, dto => dto.PropertyTypeId) + .InnerJoin() + .On(dto => dto.DataTypeId, dto => dto.DataTypeId) + .Where("umbracoNode.nodeObjectType = @nodeObjectType" + additionWhereStatement, new {nodeObjectType = Constants.ObjectTypes.Media}); + + //We're going to create a query to query against the entity SQL + // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join + // the entitySql query, we have to join the wrapped query to get the ntext in the result + + var wrappedSql = new Sql("SELECT * FROM (") + .Append(entitySql) + .Append(new Sql(") tmpTbl LEFT JOIN (")) + .Append(joinSql) + .Append(new Sql(") as property ON id = property.contentNodeId")) + .OrderBy("sortOrder"); + + return wrappedSql; + } + protected virtual Sql GetBase(bool isContent, bool isMedia, string additionWhereStatement = "") { var columns = new List @@ -221,13 +312,9 @@ namespace Umbraco.Core.Persistence.Repositories columns.Add("contenttype.isContainer"); } - if (isMedia) - { - columns.Add("property.dataNvarchar as umbracoFile"); - columns.Add("property.propertyEditorAlias"); - } + //Creates an SQL query to return a single row for the entity - var sql = new Sql() + var entitySql = new Sql() .Select(columns.ToArray()) .From("umbracoNode umbracoNode") .LeftJoin("umbracoNode parent").On("parent.parentID = umbracoNode.id"); @@ -235,7 +322,7 @@ namespace Umbraco.Core.Persistence.Repositories if (isContent || isMedia) { - sql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") + entitySql.InnerJoin("cmsContent content").On("content.nodeId = umbracoNode.id") .LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType") .LeftJoin( "(SELECT nodeId, versionId FROM cmsDocument WHERE published = 1 GROUP BY nodeId, versionId) as published") @@ -245,18 +332,7 @@ namespace Umbraco.Core.Persistence.Repositories .On("umbracoNode.id = latest.nodeId"); } - if (isMedia) - { - sql.LeftJoin( - "(SELECT contentNodeId, versionId, dataNvarchar, propertyEditorAlias FROM cmsPropertyData " + - "INNER JOIN umbracoNode ON cmsPropertyData.contentNodeId = umbracoNode.id " + - "INNER JOIN cmsPropertyType ON cmsPropertyType.id = cmsPropertyData.propertytypeid " + - "INNER JOIN cmsDataType ON cmsPropertyType.dataTypeId = cmsDataType.nodeId "+ - "WHERE umbracoNode.nodeObjectType = '" + Constants.ObjectTypes.Media + "'" + additionWhereStatement + ") as property") - .On("umbracoNode.id = property.contentNodeId"); - } - - return sql; + return entitySql; } protected virtual Sql GetBaseWhere(Func baseQuery, bool isContent, bool isMedia, string additionWhereStatement, Guid nodeObjectType) @@ -298,7 +374,7 @@ namespace Umbraco.Core.Persistence.Repositories return sql; } - protected virtual Sql GetGroupBy(bool isContent, bool isMedia) + protected virtual Sql GetGroupBy(bool isContent, bool isMedia, bool includeSort = true) { var columns = new List { @@ -324,19 +400,18 @@ namespace Umbraco.Core.Persistence.Repositories columns.Add("contenttype.thumbnail"); columns.Add("contenttype.isContainer"); } + + var sql = new Sql() + .GroupBy(columns.ToArray()); - if (isMedia) + if (includeSort) { - columns.Add("property.dataNvarchar"); - columns.Add("property.propertyEditorAlias"); + sql = sql.OrderBy("umbracoNode.sortOrder"); } - var sql = new Sql() - .GroupBy(columns.ToArray()) - .OrderBy("umbracoNode.sortOrder"); return sql; } - + #endregion /// @@ -377,15 +452,21 @@ namespace Umbraco.Core.Persistence.Repositories [ResultColumn] public List UmbracoPropertyDtos { get; set; } } - + [ExplicitColumns] internal class UmbracoPropertyDto { [Column("propertyEditorAlias")] public string PropertyEditorAlias { get; set; } - [Column("umbracoFile")] - public string UmbracoFile { get; set; } + [Column("propertyTypeAlias")] + public string PropertyAlias { get; set; } + + [Column("dataNvarchar")] + public string NVarcharValue { get; set; } + + [Column("dataNtext")] + public string NTextValue { get; set; } } /// @@ -412,16 +493,15 @@ namespace Umbraco.Core.Persistence.Repositories // Is this the same UmbracoEntity as the current one we're processing if (Current != null && Current.Key == a.uniqueID) { - // Yes, just add this UmbracoProperty to the current UmbracoEntity's collection - if (Current.UmbracoProperties == null) + // Add this UmbracoProperty to the current additional data + Current.AdditionalData[p.PropertyAlias] = new UmbracoEntity.EntityProperty { - Current.UmbracoProperties = new List(); - } - Current.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty - { - PropertyEditorAlias = p.PropertyEditorAlias, - Value = p.UmbracoFile - }); + PropertyEditorAlias = p.PropertyEditorAlias, + Value = p.NTextValue.IsNullOrWhiteSpace() + ? p.NVarcharValue + : p.NTextValue.ConvertToJsonIfPossible() + }; + // Return null to indicate we're not done with this UmbracoEntity yet return null; } @@ -437,14 +517,13 @@ namespace Umbraco.Core.Persistence.Repositories Current = _factory.BuildEntityFromDynamic(a); //add the property/create the prop list if null - Current.UmbracoProperties = new List - { - new UmbracoEntity.UmbracoProperty - { - PropertyEditorAlias = p.PropertyEditorAlias, - Value = p.UmbracoFile - } - }; + Current.AdditionalData[p.PropertyAlias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = p.PropertyEditorAlias, + Value = p.NTextValue.IsNullOrWhiteSpace() + ? p.NVarcharValue + : p.NTextValue.ConvertToJsonIfPossible() + }; // Return the now populated previous UmbracoEntity (or null if first time through) return prev; diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 395f9cea0c..d1b20cce30 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Web; using System.Xml; +using Newtonsoft.Json; using Umbraco.Core.Configuration; using System.Web.Security; using Umbraco.Core.Strings; @@ -52,6 +53,28 @@ namespace Umbraco.Core || (input.StartsWith("[") && input.EndsWith("]")); } + /// + /// Returns a JObject/JArray instance if the string can be converted to json, otherwise returns the string + /// + /// + /// + internal static object ConvertToJsonIfPossible(this string input) + { + if (input.DetectIsJson() == false) + { + return input; + } + try + { + var obj = JsonConvert.DeserializeObject(input); + return obj; + } + catch (Exception ex) + { + return input; + } + } + internal static string ReplaceNonAlphanumericChars(this string input, char replacement) { //any character that is not alphanumeric, convert to a hyphen diff --git a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs index ac70364e67..52513d551d 100644 --- a/src/Umbraco.Tests/Models/UmbracoEntityTests.cs +++ b/src/Umbraco.Tests/Models/UmbracoEntityTests.cs @@ -47,12 +47,13 @@ namespace Umbraco.Tests.Models }; item.AdditionalData.Add("test1", 3); item.AdditionalData.Add("test2", "valuie"); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + + item.AdditionalData.Add("test3", new UmbracoEntity.EntityProperty() { Value = "test", PropertyEditorAlias = "TestPropertyEditor" }); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test4", new UmbracoEntity.EntityProperty() { Value = "test2", PropertyEditorAlias = "TestPropertyEditor2" @@ -82,12 +83,6 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.UpdateDate, item.UpdateDate); Assert.AreEqual(clone.AdditionalData.Count, item.AdditionalData.Count); Assert.AreEqual(clone.AdditionalData, item.AdditionalData); - Assert.AreEqual(clone.UmbracoProperties.Count, item.UmbracoProperties.Count); - for (var i = 0; i < clone.UmbracoProperties.Count; i++) - { - Assert.AreNotSame(clone.UmbracoProperties[i], item.UmbracoProperties[i]); - Assert.AreEqual(clone.UmbracoProperties[i], item.UmbracoProperties[i]); - } //This double verifies by reflection var allProps = clone.GetType().GetProperties(); @@ -125,12 +120,12 @@ namespace Umbraco.Tests.Models }; item.AdditionalData.Add("test1", 3); item.AdditionalData.Add("test2", "valuie"); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test3", new UmbracoEntity.EntityProperty() { Value = "test", PropertyEditorAlias = "TestPropertyEditor" }); - item.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty() + item.AdditionalData.Add("test4", new UmbracoEntity.EntityProperty() { Value = "test2", PropertyEditorAlias = "TestPropertyEditor2" diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 82cc2c7560..9826e9fc3f 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.Document); + var entities = service.GetAll(UmbracoObjectTypes.Document).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var objectTypeId = new Guid(Constants.ObjectTypes.Document); - var entities = service.GetAll(objectTypeId); + var entities = service.GetAll(objectTypeId).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -57,7 +57,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(); + var entities = service.GetAll().ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(4)); @@ -69,7 +69,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetChildren(-1, UmbracoObjectTypes.Document); + var entities = service.GetChildren(-1, UmbracoObjectTypes.Document).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -92,7 +92,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.DocumentType); + var entities = service.GetAll(UmbracoObjectTypes.DocumentType).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -104,7 +104,7 @@ namespace Umbraco.Tests.Services var service = ServiceContext.EntityService; var objectTypeId = new Guid(Constants.ObjectTypes.DocumentType); - var entities = service.GetAll(objectTypeId); + var entities = service.GetAll(objectTypeId).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -115,7 +115,7 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(); + var entities = service.GetAll().ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(1)); @@ -126,16 +126,16 @@ namespace Umbraco.Tests.Services { var service = ServiceContext.EntityService; - var entities = service.GetAll(UmbracoObjectTypes.Media); + var entities = service.GetAll(UmbracoObjectTypes.Media).ToArray(); Assert.That(entities.Any(), Is.True); Assert.That(entities.Count(), Is.EqualTo(3)); - //Assert.That(entities.Any(x => ((UmbracoEntity)x).UmbracoFile != string.Empty), Is.True); + Assert.That( entities.Any( x => - ((UmbracoEntity) x).UmbracoProperties.Any( - y => y.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); + x.AdditionalData.Any(y => y.Value is UmbracoEntity.EntityProperty + && ((UmbracoEntity.EntityProperty)y.Value).PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); } private static bool _isSetup = false; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 2e06bc1656..67dbcecc54 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -129,30 +129,79 @@ function mediaHelper(umbRequestHelper) { _mediaFileResolvers[propertyEditorAlias] = func; }, + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFileFromEntity + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media entity returned with the entityResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ + resolveFileFromEntity : function(mediaEntity, thumbnail) { + + if (!angular.isObject(mediaEntity.metaData)) { + throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData"; + } + + var values = _.values(mediaEntity.metaData); + for (var i = 0; i < values.length; i++) { + var val = values[i]; + if (angular.isObject(val) && val.PropertyEditorAlias) { + for (var resolver in _mediaFileResolvers) { + if (val.PropertyEditorAlias === resolver) { + //we need to format a property variable that coincides with how the property would be structured + // if it came from the mediaResource just to keep things slightly easier for the file resolvers. + var property = { value: val.Value }; + + return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail); + } + } + } + } + + return ""; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#resolveFile + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Gets the media file url for a media object returned with the mediaResource + * + * @param {object} mediaEntity A media Entity returned from the entityResource + * @param {boolean} thumbnail Whether to return the thumbnail url or normal url + */ /*jshint loopfunc: true */ resolveFile : function(mediaItem, thumbnail){ - var _props = []; - function _iterateProps(props){ - var result = null; + + function iterateProps(props){ + var res = null; for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(property){ return property.editor === resolver; }); + var property = _.find(props, function(prop){ return prop.editor === resolver; }); if(property){ - result = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); + res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail); break; } } - return result; + return res; } //we either have properties raw on the object, or spread out on tabs var result = ""; if(mediaItem.properties){ - result = _iterateProps(mediaItem.properties); + result = iterateProps(mediaItem.properties); }else if(mediaItem.tabs){ for(var tab in mediaItem.tabs) { if(mediaItem.tabs[tab].properties){ - result = _iterateProps(mediaItem.tabs[tab].properties); + result = iterateProps(mediaItem.tabs[tab].properties); if(result){ break; } @@ -164,26 +213,26 @@ function mediaHelper(umbRequestHelper) { /*jshint loopfunc: true */ hasFilePropertyType : function(mediaItem){ - function _iterateProps(props){ - var result = false; + function iterateProps(props){ + var res = false; for(var resolver in _mediaFileResolvers) { - var property = _.find(props, function(property){ return property.editor === resolver; }); + var property = _.find(props, function(prop){ return prop.editor === resolver; }); if(property){ - result = true; + res = true; break; } } - return result; + return res; } //we either have properties raw on the object, or spread out on tabs var result = false; if(mediaItem.properties){ - result = _iterateProps(mediaItem.properties); + result = iterateProps(mediaItem.properties); }else if(mediaItem.tabs){ for(var tab in mediaItem.tabs) { if(mediaItem.tabs[tab].properties){ - result = _iterateProps(mediaItem.tabs[tab].properties); + result = iterateProps(mediaItem.tabs[tab].properties); if(result){ break; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 0224853e7b..7aa65473a6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -125,11 +125,15 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag angular.module("umbraco") .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) .run(function(mediaHelper, umbRequestHelper){ - if(mediaHelper && mediaHelper.registerFileResolver){ + if (mediaHelper && mediaHelper.registerFileResolver) { + + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver("Umbraco.UploadField", function(property, entity, thumbnail){ if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { + var thumbnailUrl = umbRequestHelper.getApiUrl( "imagesApiBaseUrl", "GetBigThumbnail", diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index abcdb0852f..7189ca504a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -98,11 +98,14 @@ angular.module('umbraco') }) .run(function (mediaHelper, umbRequestHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { + + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { if (property.value.src) { if (thumbnail === true) { - return property.value.src + "?width=600&mode=max"; + return property.value.src + "?width=500&mode=max"; } else { return property.value.src; @@ -114,6 +117,7 @@ angular.module('umbraco') if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { + var thumbnailUrl = umbRequestHelper.getApiUrl( "imagesApiBaseUrl", "GetBigThumbnail", diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index e17a03473c..0a1066cda3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //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("Umbraco.PropertyEditors.MediaPickerController", - function($rootScope, $scope, dialogService, mediaResource, mediaHelper, $timeout) { + function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout) { //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; @@ -17,18 +17,24 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if ($scope.model.value) { var ids = $scope.model.value.split(','); - mediaResource.getByIds(ids).then(function (medias) { - //img.media = media; + //NOTE: We need to use the entityResource NOT the mediaResource here because + // the mediaResource has server side auth configured for which the user must have + // access to the media section, if they don't they'll get auth errors. The entityResource + // acts differently in that it allows access if the user has access to any of the apps that + // might require it's use. Therefore we need to use the metatData property to get at the thumbnail + // value. + + entityResource.getByIds(ids, "Media").then(function (medias) { + _.each(medias, function (media, i) { //only show non-trashed items - if(media.parentId >= -1){ - if(!media.thumbnail){ - media.thumbnail = mediaHelper.resolveFile(media, true); + if (media.parentId >= -1) { + + if (!media.thumbnail) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - //media.src = mediaHelper.getImagePropertyValue({ imageModel: media }); - //media.thumbnail = mediaHelper.getThumbnailFromPath(media.src); $scope.images.push(media); $scope.ids.push(media.id); } @@ -60,10 +66,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl _.each(data, function(media, i) { - if(!media.thumbnail){ - media.thumbnail = mediaHelper.resolveFile(media, true); + if (!media.thumbnail) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - + $scope.images.push(media); $scope.ids.push(media.id); }); diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 52d60e566f..507c229122 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -531,8 +531,9 @@ namespace Umbraco.Web.Editors var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { - return ids.Select(id => Mapper.Map(Services.EntityService.Get(id, objectType.Value))) + var result = ids.Select(id => Mapper.Map(Services.EntityService.Get(id, objectType.Value))) .WhereNotNull(); + return result; } //now we need to convert the unknown ones switch (entityType) diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 39b872af6d..54938be76d 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -104,6 +104,8 @@ namespace Umbraco.Web.Editors return GetResized(imagePath, width, Convert.ToString(width)); } + //TODO: We should delegate this to ImageProcessing + /// /// Gets a resized image - if the requested max width is greater than the original image, only the original image will be returned. /// diff --git a/src/Umbraco.Web/WebServices/FolderBrowserService.cs b/src/Umbraco.Web/WebServices/FolderBrowserService.cs index 2c8cff527d..bf5a3f1bd5 100644 --- a/src/Umbraco.Web/WebServices/FolderBrowserService.cs +++ b/src/Umbraco.Web/WebServices/FolderBrowserService.cs @@ -32,9 +32,14 @@ namespace Umbraco.Web.WebServices var entities = service.GetChildren(parentId, UmbracoObjectTypes.Media); foreach (UmbracoEntity entity in entities) { - var uploadFieldProperty = entity.UmbracoProperties.FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); - - var thumbnailUrl = uploadFieldProperty == null ? "" : ThumbnailProvidersResolver.Current.GetThumbnailUrl(uploadFieldProperty.Value); + var uploadFieldProperty = entity.AdditionalData + .Select(x => x.Value as UmbracoEntity.EntityProperty) + .Where(x => x != null) + .FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); + + //var uploadFieldProperty = entity.UmbracoProperties.FirstOrDefault(x => x.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias); + + var thumbnailUrl = uploadFieldProperty == null ? "" : ThumbnailProvidersResolver.Current.GetThumbnailUrl((string)uploadFieldProperty.Value); var item = new { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs index 0b48f7a2e1..f72457d867 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/BaseMediaTree.cs @@ -21,28 +21,28 @@ namespace umbraco.cms.presentation.Trees { [Obsolete("This is no longer used and will be removed from the codebase in the future")] - public abstract class BaseMediaTree : BaseTree - { + public abstract class BaseMediaTree : BaseTree + { private User _user; - public BaseMediaTree(string application) - : base(application) - { - - } + public BaseMediaTree(string application) + : base(application) + { + + } + + /// + /// Returns the current User. This ensures that we don't instantiate a new User object + /// each time. + /// + protected User CurrentUser + { + get + { + return (_user == null ? (_user = UmbracoEnsuredPage.CurrentUser) : _user); + } + } - /// - /// Returns the current User. This ensures that we don't instantiate a new User object - /// each time. - /// - protected User CurrentUser - { - get - { - return (_user == null ? (_user = UmbracoEnsuredPage.CurrentUser) : _user); - } - } - public override void RenderJS(ref StringBuilder Javascript) { if (!string.IsNullOrEmpty(this.FunctionToCall)) @@ -54,7 +54,7 @@ namespace umbraco.cms.presentation.Trees else if (!this.IsDialog) { Javascript.Append( - @" + @" function openMedia(id) { " + ClientTools.Scripts.GetContentFrame() + ".location.href = 'editMedia.aspx?id=' + id;" + @" } @@ -98,12 +98,12 @@ function openMedia(id) { // to call so that is fine. var entities = Services.EntityService.GetChildren(m_id, UmbracoObjectTypes.Media).ToArray(); - + foreach (UmbracoEntity entity in entities) { var e = entity; var xNode = PerformNodeRender(e.Id, entity.Name, e.HasChildren, e.ContentTypeIcon, e.ContentTypeAlias, () => GetLinkValue(e)); - + OnBeforeNodeRender(ref tree, ref xNode, EventArgs.Empty); if (xNode != null) { @@ -111,7 +111,7 @@ function openMedia(id) { OnAfterNodeRender(ref tree, ref xNode, EventArgs.Empty); } } - } + } } private XmlTreeNode PerformNodeRender(int nodeId, string nodeName, bool hasChildren, string icon, string contentTypeAlias, Func getLinkValue) @@ -164,28 +164,28 @@ function openMedia(id) { return xNode; } - + /// - /// Returns the value for a link in WYSIWYG mode, by default only media items that have a - /// DataTypeUploadField are linkable, however, a custom tree can be created which overrides - /// this method, or another GUID for a custom data type can be added to the LinkableMediaDataTypes - /// list on application startup. - /// - /// - /// - /// + /// Returns the value for a link in WYSIWYG mode, by default only media items that have a + /// DataTypeUploadField are linkable, however, a custom tree can be created which overrides + /// this method, or another GUID for a custom data type can be added to the LinkableMediaDataTypes + /// list on application startup. + /// + /// + /// + /// public virtual string GetLinkValue(Media dd, string nodeLink) { var props = dd.GenericProperties; - foreach (Property p in props) - { - Guid currId = p.PropertyType.DataTypeDefinition.DataType.Id; - if (LinkableMediaDataTypes.Contains(currId) && string.IsNullOrEmpty(p.Value.ToString()) == false) - { - return p.Value.ToString(); - } - } + foreach (Property p in props) + { + Guid currId = p.PropertyType.DataTypeDefinition.DataType.Id; + if (LinkableMediaDataTypes.Contains(currId) && string.IsNullOrEmpty(p.Value.ToString()) == false) + { + return p.Value.ToString(); + } + } return ""; } @@ -200,29 +200,34 @@ function openMedia(id) { /// internal virtual string GetLinkValue(UmbracoEntity entity) { - foreach (var property in entity.UmbracoProperties) + foreach (var property in entity.AdditionalData + .Select(x => x.Value as UmbracoEntity.EntityProperty) + .Where(x => x != null)) { + + //required for backwards compatibility with v7 with changing the GUID -> alias var controlId = LegacyPropertyEditorIdToAliasConverter.GetLegacyIdFromAlias(property.PropertyEditorAlias, LegacyPropertyEditorIdToAliasConverter.NotFoundLegacyIdResponseBehavior.ReturnNull); if (controlId != null) { - if (LinkableMediaDataTypes.Contains(controlId.Value) && - string.IsNullOrEmpty(property.Value) == false) - return property.Value; - } + if (LinkableMediaDataTypes.Contains(controlId.Value) + && string.IsNullOrEmpty((string)property.Value) == false) + + return property.Value.ToString(); + } } return ""; } - /// - /// By default, any media type that is to be "linkable" in the WYSIWYG editor must contain - /// a DataTypeUploadField data type which will ouput the value for the link, however, if - /// a developer wants the WYSIWYG editor to link to a custom media type, they will either have - /// to create their own media tree and inherit from this one and override the GetLinkValue - /// or add another GUID to the LinkableMediaDataType list on application startup that matches - /// the GUID of a custom data type. The order of property types on the media item definition will determine the output value. - /// - public static List LinkableMediaDataTypes { get; protected set; } + /// + /// By default, any media type that is to be "linkable" in the WYSIWYG editor must contain + /// a DataTypeUploadField data type which will ouput the value for the link, however, if + /// a developer wants the WYSIWYG editor to link to a custom media type, they will either have + /// to create their own media tree and inherit from this one and override the GetLinkValue + /// or add another GUID to the LinkableMediaDataType list on application startup that matches + /// the GUID of a custom data type. The order of property types on the media item definition will determine the output value. + /// + public static List LinkableMediaDataTypes { get; protected set; } /// /// Returns true if we can use the EntityService to render the tree or revert to the original way @@ -245,5 +250,5 @@ function openMedia(id) { } } - } + } }