using System; using System.Collections.Generic; using System.Dynamic; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Models.TemplateQuery; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Xml; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Web.BackOffice.ModelBinders; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { /// /// The API controller used for getting entity objects, basic name, icon, id representation of umbraco objects that are /// based on CMSNode /// /// /// /// This controller allows resolving basic entity data for various entities without placing the hard restrictions /// on users that may not have access /// to the sections these entities entities exist in. This is to allow pickers, etc... of data to work for all /// users. In some cases such as accessing /// Members, more explicit security checks are done. /// /// Some objects such as macros are not based on CMSNode /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [ParameterSwapControllerActionSelector(nameof(GetAncestors), "id", typeof(int), typeof(Guid))] [ParameterSwapControllerActionSelector(nameof(GetPagedChildren), "id", typeof(int), typeof(string))] [ParameterSwapControllerActionSelector(nameof(GetPath), "id", typeof(int), typeof(Guid), typeof(Udi))] [ParameterSwapControllerActionSelector(nameof(GetUrlAndAnchors), "id", typeof(int), typeof(Guid), typeof(Udi))] [ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))] [ParameterSwapControllerActionSelector(nameof(GetByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))] [ParameterSwapControllerActionSelector(nameof(GetUrl), "id", typeof(int), typeof(Udi))] public class EntityController : UmbracoAuthorizedJsonController { private static readonly string[] _postFilterSplitStrings = { "=", "==", "!=", "<>", ">", "<", ">=", "<=" }; private readonly AppCaches _appCaches; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IContentService _contentService; private readonly IContentTypeService _contentTypeService; private readonly IDataTypeService _dataTypeService; private readonly IEntityService _entityService; private readonly IFileService _fileService; private readonly ILocalizationService _localizationService; private readonly ILocalizedTextService _localizedTextService; private readonly IMacroService _macroService; private readonly IMediaTypeService _mediaTypeService; private readonly IPublishedContentQuery _publishedContentQuery; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly SearchableTreeCollection _searchableTreeCollection; private readonly IShortStringHelper _shortStringHelper; private readonly ISqlContext _sqlContext; private readonly UmbracoTreeSearcher _treeSearcher; private readonly ITreeService _treeService; private readonly IUmbracoMapper _umbracoMapper; private readonly IUserService _userService; public EntityController( ITreeService treeService, UmbracoTreeSearcher treeSearcher, SearchableTreeCollection searchableTreeCollection, IPublishedContentQuery publishedContentQuery, IShortStringHelper shortStringHelper, IEntityService entityService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IPublishedUrlProvider publishedUrlProvider, IContentService contentService, IUmbracoMapper umbracoMapper, IDataTypeService dataTypeService, ISqlContext sqlContext, ILocalizedTextService localizedTextService, IFileService fileService, IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMacroService macroService, IUserService userService, ILocalizationService localizationService, AppCaches appCaches) { _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); _treeSearcher = treeSearcher ?? throw new ArgumentNullException(nameof(treeSearcher)); _searchableTreeCollection = searchableTreeCollection ?? throw new ArgumentNullException(nameof(searchableTreeCollection)); _publishedContentQuery = publishedContentQuery ?? throw new ArgumentNullException(nameof(publishedContentQuery)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _publishedUrlProvider = publishedUrlProvider ?? throw new ArgumentNullException(nameof(publishedUrlProvider)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); _sqlContext = sqlContext ?? throw new ArgumentNullException(nameof(sqlContext)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); _mediaTypeService = mediaTypeService ?? throw new ArgumentNullException(nameof(mediaTypeService)); _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); } /// /// Returns an Umbraco alias given a string /// /// /// /// public dynamic GetSafeAlias(string value, bool camelCase = true) { var returnValue = string.IsNullOrWhiteSpace(value) ? string.Empty : value.ToSafeAlias(_shortStringHelper, camelCase); dynamic returnObj = new ExpandoObject(); returnObj.alias = returnValue; returnObj.original = value; returnObj.camelCase = camelCase; return returnObj; } /// /// Searches for results based on the entity type /// /// /// /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// /// If set used to look up whether user and group start node permissions will be ignored. /// [HttpGet] public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, Guid? dataTypeKey = null) { // NOTE: Theoretically you shouldn't be able to see member data if you don't have access to members right? ... but there is a member picker, so can't really do that if (string.IsNullOrEmpty(query)) { return Enumerable.Empty(); } //TODO: This uses the internal UmbracoTreeSearcher, this instead should delgate to the ISearchableTree implementation for the type var ignoreUserStartNodes = dataTypeKey.HasValue && _dataTypeService.IsDataTypeIgnoringUserStartNodes(dataTypeKey.Value); return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes); } /// /// Searches for all content that the user is allowed to see (based on their allowed sections) /// /// /// /// /// Even though a normal entity search will allow any user to search on a entity type that they may not have access to /// edit, we need /// to filter these results to the sections they are allowed to edit since this search function is explicitly for the /// global search /// so if we showed entities that they weren't allowed to edit they would get errors when clicking on the result. /// The reason a user is allowed to search individual entity types that they are not allowed to edit is because those /// search /// methods might be used in things like pickers in the content editor. /// [HttpGet] public IDictionary SearchAll(string query) { var result = new Dictionary(); if (string.IsNullOrEmpty(query)) { return result; } var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.AllowedSections.ToArray(); foreach (KeyValuePair searchableTree in _searchableTreeCollection .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) { if (allowedSections.Contains(searchableTree.Value.AppAlias)) { Tree tree = _treeService.GetByAlias(searchableTree.Key); if (tree == null) { continue; //shouldn't occur } result[Tree.GetRootNodeDisplayName(tree, _localizedTextService)] = new TreeSearchResult { Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out var total), TreeAlias = searchableTree.Key, AppAlias = searchableTree.Value.AppAlias, JsFormatterService = searchableTree.Value.FormatterService, JsFormatterMethod = searchableTree.Value.FormatterMethod }; } } return result; } /// /// Gets the path for a given node ID /// /// /// /// public IConvertToActionResult GetPath(int id, UmbracoEntityTypes type) { ActionResult foundContentResult = GetResultForId(id, type); EntityBasic foundContent = foundContentResult.Value; if (foundContent is null) { return foundContentResult; } return new ActionResult>(foundContent.Path .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select( s => int.Parse(s, CultureInfo.InvariantCulture))); } /// /// Gets the path for a given node ID /// /// /// /// public IConvertToActionResult GetPath(Guid id, UmbracoEntityTypes type) { ActionResult foundContentResult = GetResultForKey(id, type); EntityBasic foundContent = foundContentResult.Value; if (foundContent is null) { return foundContentResult; } return new ActionResult>(foundContent.Path .Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select( s => int.Parse(s, CultureInfo.InvariantCulture))); } /// /// Gets the path for a given node ID /// /// /// /// public IActionResult GetPath(Udi id, UmbracoEntityTypes type) { var guidUdi = id as GuidUdi; if (guidUdi != null) { return GetPath(guidUdi.Guid, type).Convert(); } return NotFound(); } /// /// Gets the URL of an entity /// /// UDI of the entity to fetch URL for /// The culture to fetch the URL for /// The URL or path to the item public IActionResult GetUrl(Udi id, string culture = "*") { Attempt intId = _entityService.GetId(id); if (!intId.Success) { return NotFound(); } UmbracoEntityTypes entityType; switch (id.EntityType) { case Constants.UdiEntityType.Document: entityType = UmbracoEntityTypes.Document; break; case Constants.UdiEntityType.Media: entityType = UmbracoEntityTypes.Media; break; case Constants.UdiEntityType.Member: entityType = UmbracoEntityTypes.Member; break; default: return NotFound(); } return GetUrl(intId.Result, entityType, culture); } /// /// Get entity URLs by UDIs /// /// /// A list of UDIs to lookup items by /// /// The culture to fetch the URL for /// Dictionary mapping Udi -> Url /// /// We allow for POST because there could be quite a lot of Ids. /// [HttpGet] [HttpPost] public IDictionary GetUrlsByUdis([FromJsonPath] Udi[] udis, string culture = null) { if (udis == null || udis.Length == 0) { return new Dictionary(); } // TODO: PMJ 2021-09-27 - Should GetUrl(Udi) exist as an extension method on UrlProvider/IUrlProvider (in v9) string MediaOrDocumentUrl(Udi udi) { if (udi is not GuidUdi guidUdi) { return null; } return guidUdi.EntityType switch { Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(guidUdi.Guid, culture: culture ?? ClientCulture()), // NOTE: If culture is passed here we get an empty string rather than a media item URL WAT Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(guidUdi.Guid, culture: null), _ => null }; } return udis .Select(udi => new { Udi = udi, Url = MediaOrDocumentUrl(udi) }).ToDictionary(x => x.Udi, x => x.Url); } /// /// Gets the URL of an entity /// /// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member /// The culture to fetch the URL for /// The URL or path to the item /// /// We are not restricting this with security because there is no sensitive data /// public IActionResult GetUrl(int id, UmbracoEntityTypes type, string culture = null) { culture = culture ?? ClientCulture(); var returnUrl = string.Empty; if (type == UmbracoEntityTypes.Document) { var foundUrl = _publishedUrlProvider.GetUrl(id, culture: culture); if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") { returnUrl = foundUrl; return Ok(returnUrl); } } IEnumerable ancestors = GetResultForAncestors(id, type); //if content, skip the first node for replicating NiceUrl defaults if (type == UmbracoEntityTypes.Document) { ancestors = ancestors.Skip(1); } returnUrl = "/" + string.Join("/", ancestors.Select(x => x.Name)); return Ok(returnUrl); } /// /// Gets an entity by a xpath query /// /// /// /// /// public ActionResult GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) { // TODO: Rename this!!! It's misleading, it should be GetByXPath if (type != UmbracoEntityTypes.Document) { throw new ArgumentException("Get by query is only compatible with entities of type Document"); } var q = ParseXPathQuery(query, nodeContextId); IPublishedContent node = _publishedContentQuery.ContentSingleAtXPath(q); if (node == null) { return null; } return GetById(node.Id, type); } // PP: Work in progress on the query parser private string ParseXPathQuery(string query, int id) => UmbracoXPathPathSyntaxParser.ParseXPathQuery( query, id, nodeid => { IEntitySlim ent = _entityService.Get(nodeid); return ent.Path.Split(Constants.CharArrays.Comma).Reverse(); }, i => _publishedContentQuery.Content(i) != null); [HttpGet] public ActionResult GetUrlAndAnchors(Udi id, string culture = "*") { Attempt intId = _entityService.GetId(id); if (!intId.Success) { return NotFound(); } return GetUrlAndAnchors(intId.Result, culture); } [HttpGet] public UrlAndAnchors GetUrlAndAnchors(int id, string culture = "*") { culture = culture ?? ClientCulture(); var url = _publishedUrlProvider.GetUrl(id, culture: culture); IEnumerable anchorValues = _contentService.GetAnchorValuesFromRTEs(id, culture); return new UrlAndAnchors(url, anchorValues); } [HttpGet] [HttpPost] public IEnumerable GetAnchors(AnchorsModel model) { IEnumerable anchorValues = _contentService.GetAnchorValuesFromRTEContent(model.RteContent); return anchorValues; } public IEnumerable GetChildren(int id, UmbracoEntityTypes type, Guid? dataTypeKey = null) { UmbracoObjectTypes? objectType = ConvertToObjectType(type); if (objectType.HasValue) { //TODO: Need to check for Object types that support hierarchy here, some might not. var startNodes = GetStartNodes(type); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) { IEntitySlim[] nodes = _entityService.GetAll(objectType.Value, startNodes).ToArray(); if (nodes.Length == 0) { return Enumerable.Empty(); } var pr = new List(nodes.Select(_umbracoMapper.Map)); return pr; } // else proceed as usual return _entityService.GetChildren(id, objectType.Value) .WhereNotNull() .Select(_umbracoMapper.Map); } //now we need to convert the unknown ones switch (type) { case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); } } /// /// Get paged child entities by id /// /// /// /// /// /// /// /// /// /// public ActionResult> GetPagedChildren( string id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "", Guid? dataTypeKey = null) { if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) { return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); } if (Guid.TryParse(id, out _)) { //Not supported currently return NotFound(); } if (UdiParser.TryParse(id, out _)) { //Not supported currently return NotFound(); } //so we don't have an INT, GUID or UDI, it's just a string, so now need to check if it's a special id or a member type if (id == Constants.Conventions.MemberTypes.AllMembersListId) { //the EntityService can search paged members from the root intId = -1; return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter, dataTypeKey); } //the EntityService cannot search members of a certain type, this is currently not supported and would require //quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search //TODO: We should really fix this in the EntityService but if we don't we should allow the ISearchableTree for the members controller // to be used for this search instead of the built in/internal searcher IEnumerable searchResult = _treeSearcher.ExamineSearch(filter ?? "", type, pageSize, pageNumber - 1, out var total, null, id); return new PagedResult(total, pageNumber, pageSize) { Items = searchResult }; } /// /// Get paged child entities by id /// /// /// /// /// /// /// /// /// public ActionResult> GetPagedChildren( int id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "", Guid? dataTypeKey = null) { if (pageNumber <= 0) { return NotFound(); } if (pageSize <= 0) { return NotFound(); } UmbracoObjectTypes? objectType = ConvertToObjectType(type); if (objectType.HasValue) { IEnumerable entities; long totalRecords; var startNodes = GetStartNodes(type); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); // root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) { if (pageNumber > 0) { return new PagedResult(0, 0, 0); } IEntitySlim[] nodes = _entityService.GetAll(objectType.Value, startNodes).ToArray(); if (nodes.Length == 0) { return new PagedResult(0, 0, 0); } if (pageSize < nodes.Length) { pageSize = nodes.Length; // bah } var pr = new PagedResult(nodes.Length, pageNumber, pageSize) { Items = nodes.Select(_umbracoMapper.Map) }; return pr; } // else proceed as usual entities = _entityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, filter.IsNullOrWhiteSpace() ? null : _sqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); if (totalRecords == 0) { return new PagedResult(0, 0, 0); } var culture = ClientCulture(); var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { Items = entities.Select(source => { EntityBasic target = _umbracoMapper.Map(source, context => { context.SetCulture(culture); context.SetCulture(culture); }); //TODO: Why is this here and not in the mapping? target.AdditionalData["hasChildren"] = source.HasChildren; return target; }) }; return pagedResult; } //now we need to convert the unknown ones switch (type) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); } } private int[] GetStartNodes(UmbracoEntityTypes type) { switch (type) { case UmbracoEntityTypes.Document: return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateContentStartNodeIds( _entityService, _appCaches); case UmbracoEntityTypes.Media: return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds( _entityService, _appCaches); default: return Array.Empty(); } } public ActionResult> GetPagedDescendants( int id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "", Guid? dataTypeKey = null) { if (pageNumber <= 0) { return NotFound(); } if (pageSize <= 0) { return NotFound(); } // re-normalize since NULL can be passed in filter = filter ?? string.Empty; UmbracoObjectTypes? objectType = ConvertToObjectType(type); if (objectType.HasValue) { IEnumerable entities; long totalRecords; if (id == Constants.System.Root) { // root is special: we reduce it to start nodes var aids = GetStartNodes(type); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey); entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? _entityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, _sqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection), false) : _entityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, _sqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); } else { entities = _entityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, _sqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); } if (totalRecords == 0) { return new PagedResult(0, 0, 0); } var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { Items = entities.Select(MapEntities()) }; return pagedResult; } //now we need to convert the unknown ones switch (type) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); } } private bool IsDataTypeIgnoringUserStartNodes(Guid? dataTypeKey) => dataTypeKey.HasValue && _dataTypeService .IsDataTypeIgnoringUserStartNodes( dataTypeKey.Value); public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) => GetResultForAncestors(id, type, queryStrings); public ActionResult> GetAncestors(Guid id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) { IEntitySlim entity = _entityService.Get(id); if (entity is null) { return NotFound(); } return Ok(GetResultForAncestors(entity.Id, type, queryStrings)); } /// /// Searches for results based on the entity type /// /// /// /// /// If set to true, user and group start node permissions will be ignored. /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { var culture = ClientCulture(); return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, culture, searchFrom, ignoreUserStartNodes); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { // TODO: Need to check for Object types that support hierarchic here, some might not. return _entityService.GetChildren(id, objectType.Value) .WhereNotNull() .Select(MapEntities()); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormCollection queryStrings = null) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { // TODO: Need to check for Object types that support hierarchic here, some might not. var ids = _entityService.Get(id).Path.Split(Constants.CharArrays.Comma) .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).Distinct().ToArray(); var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue("dataTypeId")); if (ignoreUserStartNodes == false) { int[] aids = null; switch (entityType) { case UmbracoEntityTypes.Document: aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser .CalculateContentStartNodeIds(_entityService, _appCaches); break; case UmbracoEntityTypes.Media: aids = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.CalculateMediaStartNodeIds( _entityService, _appCaches); break; } if (aids != null) { var lids = new List(); var ok = false; foreach (var i in ids) { if (ok) { lids.Add(i); continue; } if (aids.Contains(i)) { lids.Add(i); ok = true; } } ids = lids.ToArray(); } } var culture = queryStrings?.GetValue("culture"); return ids.Length == 0 ? Enumerable.Empty() : _entityService.GetAll(objectType.Value, ids) .WhereNotNull() .OrderBy(x => x.Level) .Select(MapEntities(culture)); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForKeys(Guid[] keys, UmbracoEntityTypes entityType) { if (keys.Length == 0) { return Enumerable.Empty(); } UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { IEnumerable entities = _entityService.GetAll(objectType.Value, keys) .WhereNotNull() .Select(MapEntities()); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Key); IEnumerable result = keys.Select(x => xref.ContainsKey(x) ? xref[x] : null) .Where(x => x != null); return result; } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForIds(int[] ids, UmbracoEntityTypes entityType) { if (ids.Length == 0) { return Enumerable.Empty(); } UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { IEnumerable entities = _entityService.GetAll(objectType.Value, ids) .WhereNotNull() .Select(MapEntities()); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Id); IEnumerable result = ids.Select(x => xref.ContainsKey(x) ? xref[x] : null) .Where(x => x != null); return result; } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private ActionResult GetResultForKey(Guid key, UmbracoEntityTypes entityType) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { IEntitySlim found = _entityService.Get(key, objectType.Value); if (found == null) { return NotFound(); } return _umbracoMapper.Map(found); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: case UmbracoEntityTypes.Template: var template = Services.FileService.GetTemplate(key); if (template is null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return Mapper.Map(template); default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private ActionResult GetResultForId(int id, UmbracoEntityTypes entityType) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { IEntitySlim found = _entityService.Get(id, objectType.Value); if (found == null) { return NotFound(); } return MapEntity(found); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: case UmbracoEntityTypes.Template: var template = Services.FileService.GetTemplate(id); if (template is null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return Mapper.Map(template); default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private static UmbracoObjectTypes? ConvertToObjectType(UmbracoEntityTypes entityType) { switch (entityType) { case UmbracoEntityTypes.Document: return UmbracoObjectTypes.Document; case UmbracoEntityTypes.Media: return UmbracoObjectTypes.Media; case UmbracoEntityTypes.MemberType: return UmbracoObjectTypes.MemberType; case UmbracoEntityTypes.MemberGroup: return UmbracoObjectTypes.MemberGroup; case UmbracoEntityTypes.MediaType: return UmbracoObjectTypes.MediaType; case UmbracoEntityTypes.DocumentType: return UmbracoObjectTypes.DocumentType; case UmbracoEntityTypes.Member: return UmbracoObjectTypes.Member; case UmbracoEntityTypes.DataType: return UmbracoObjectTypes.DataType; default: //There is no UmbracoEntity conversion (things like Macros, Users, etc...) return null; } } /// /// /// The type of entity. /// /// Optional filter - Format like: "BoolVariable==true&IntVariable>=6". Invalid filters are /// ignored. /// /// public IEnumerable GetAll(UmbracoEntityTypes type, string postFilter) => GetResultForAll(type, postFilter); /// /// Gets the result for the entity list based on the type /// /// /// A string where filter that will filter the results dynamically with linq - optional /// private IEnumerable GetResultForAll(UmbracoEntityTypes entityType, string postFilter = null) { UmbracoObjectTypes? objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { // TODO: Should we order this by something ? IEnumerable entities = _entityService.GetAll(objectType.Value).WhereNotNull().Select(MapEntities()); return ExecutePostFilter(entities, postFilter); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.Template: IEnumerable templates = _fileService.GetTemplates(); IEnumerable filteredTemplates = ExecutePostFilter(templates, postFilter); return filteredTemplates.Select(MapEntities()); case UmbracoEntityTypes.Macro: //Get all macros from the macro service IOrderedEnumerable macros = _macroService.GetAll().WhereNotNull().OrderBy(x => x.Name); IEnumerable filteredMacros = ExecutePostFilter(macros, postFilter); return filteredMacros.Select(MapEntities()); case UmbracoEntityTypes.PropertyType: //get all document types, then combine all property types into one list IEnumerable propertyTypes = _contentTypeService.GetAll() .Cast() .Concat(_mediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyTypes) .DistinctBy(composition => composition.Alias); IEnumerable filteredPropertyTypes = ExecutePostFilter(propertyTypes, postFilter); return _umbracoMapper.MapEnumerable(filteredPropertyTypes); case UmbracoEntityTypes.PropertyGroup: //get all document types, then combine all property types into one list IEnumerable propertyGroups = _contentTypeService.GetAll() .Cast() .Concat(_mediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyGroups) .DistinctBy(composition => composition.Name); IEnumerable filteredpropertyGroups = ExecutePostFilter(propertyGroups, postFilter); return _umbracoMapper.MapEnumerable(filteredpropertyGroups); case UmbracoEntityTypes.User: IEnumerable users = _userService.GetAll(0, int.MaxValue, out _); IEnumerable filteredUsers = ExecutePostFilter(users, postFilter); return _umbracoMapper.MapEnumerable(filteredUsers); case UmbracoEntityTypes.Stylesheet: if (!postFilter.IsNullOrWhiteSpace()) { throw new NotSupportedException("Filtering on stylesheets is not currently supported"); } return _fileService.GetStylesheets().Select(MapEntities()); case UmbracoEntityTypes.Script: if (!postFilter.IsNullOrWhiteSpace()) { throw new NotSupportedException("Filtering on scripts is not currently supported"); } return _fileService.GetScripts().Select(MapEntities()); case UmbracoEntityTypes.PartialView: if (!postFilter.IsNullOrWhiteSpace()) { throw new NotSupportedException("Filtering on partial views is not currently supported"); } return _fileService.GetPartialViews().Select(MapEntities()); case UmbracoEntityTypes.Language: if (!postFilter.IsNullOrWhiteSpace()) { throw new NotSupportedException("Filtering on languages is not currently supported"); } return _localizationService.GetAllLanguages().Select(MapEntities()); case UmbracoEntityTypes.DictionaryItem: if (!postFilter.IsNullOrWhiteSpace()) { throw new NotSupportedException("Filtering on dictionary items is not currently supported"); } return GetAllDictionaryItems(); default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable ExecutePostFilter(IEnumerable entities, string postFilter) { if (postFilter.IsNullOrWhiteSpace()) { return entities; } var postFilterConditions = postFilter.Split(Constants.CharArrays.Ampersand); foreach (var postFilterCondition in postFilterConditions) { QueryCondition queryCondition = BuildQueryCondition(postFilterCondition); if (queryCondition != null) { Expression> whereClauseExpression = queryCondition.BuildCondition("x"); entities = entities.Where(whereClauseExpression.Compile()); } } return entities; } private static QueryCondition BuildQueryCondition(string postFilter) { var postFilterParts = postFilter.Split(_postFilterSplitStrings, 2, StringSplitOptions.RemoveEmptyEntries); if (postFilterParts.Length != 2) { return null; } var propertyName = postFilterParts[0]; var constraintValue = postFilterParts[1]; var stringOperator = postFilter.Substring(propertyName.Length, postFilter.Length - propertyName.Length - constraintValue.Length); Operator binaryOperator; try { binaryOperator = OperatorFactory.FromString(stringOperator); } catch (ArgumentException) { // unsupported operators are ignored return null; } Type type = typeof(T); PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); if (property == null) { return null; } var queryCondition = new QueryCondition { Term = new OperatorTerm { Operator = binaryOperator }, ConstraintValue = constraintValue, Property = new PropertyModel { Alias = propertyName, Name = propertyName, Type = property.PropertyType.Name } }; return queryCondition; } private Func MapEntities(string culture = null) { culture = culture ?? ClientCulture(); return x => MapEntity(x, culture); } private EntityBasic MapEntity(object entity, string culture = null) { culture = culture ?? ClientCulture(); return _umbracoMapper.Map(entity, context => { context.SetCulture(culture); }); } private string ClientCulture() => Request.ClientCulture(); #region GetById /// /// Gets an entity by it's id /// /// /// /// public ActionResult GetById(int id, UmbracoEntityTypes type) => GetResultForId(id, type); /// /// Gets an entity by it's key /// /// /// /// public ActionResult GetById(Guid id, UmbracoEntityTypes type) => GetResultForKey(id, type); /// /// Gets an entity by it's UDI /// /// /// /// public ActionResult GetById(Udi id, UmbracoEntityTypes type) { var guidUdi = id as GuidUdi; if (guidUdi != null) { return GetResultForKey(guidUdi.Guid, type); } return NotFound(); } #endregion #region GetByIds /// /// Get entities by integer ids /// /// /// /// /// /// We allow for POST because there could be quite a lot of Ids /// [HttpGet] [HttpPost] public ActionResult> GetByIds([FromJsonPath] int[] ids, [FromQuery] UmbracoEntityTypes type) { if (ids == null) { return NotFound(); } return new ActionResult>(GetResultForIds(ids, type)); } /// /// Get entities by GUID ids /// /// /// /// /// /// We allow for POST because there could be quite a lot of Ids /// [HttpGet] [HttpPost] public ActionResult> GetByIds([FromJsonPath] Guid[] ids, [FromQuery] UmbracoEntityTypes type) { if (ids == null) { return NotFound(); } return new ActionResult>(GetResultForKeys(ids, type)); } /// /// Get entities by UDIs /// /// /// A list of UDIs to lookup items by, all UDIs must be of the same UDI type! /// /// /// /// /// We allow for POST because there could be quite a lot of Ids. /// [HttpGet] [HttpPost] public ActionResult> GetByIds([FromJsonPath] Udi[] ids, [FromQuery] UmbracoEntityTypes type) { if (ids == null) { return NotFound(); } if (ids.Length == 0) { return Enumerable.Empty().ToList(); } //all udi types will need to be the same in this list so we'll determine by the first //currently we only support GuidUdi for this method var guidUdi = ids[0] as GuidUdi; if (guidUdi != null) { return new ActionResult>( GetResultForKeys(ids.Select(x => ((GuidUdi)x).Guid).ToArray(), type)); } return NotFound(); } #endregion #region Methods to get all dictionary items private IEnumerable GetAllDictionaryItems() { var list = new List(); foreach (IDictionaryItem dictionaryItem in _localizationService.GetRootDictionaryItems() .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(dictionaryItem); list.Add(item); GetChildItemsForList(dictionaryItem, list); } return list; } private static Func DictionaryItemSort() => item => item.ItemKey; private void GetChildItemsForList(IDictionaryItem dictionaryItem, ICollection list) { foreach (IDictionaryItem childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key) .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(childItem); list.Add(item); GetChildItemsForList(childItem, list); } } #endregion } }