using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http.Formatting; using System.Web.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Composing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. [UmbracoApplicationAuthorize( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)] [Tree(Constants.Applications.Content, Constants.Trees.Content)] [PluginController("UmbracoTrees")] [CoreTree] [SearchableTree("searchResultFormatter", "configureContentResult")] public class ContentTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); protected override int RecycleBinId => Constants.System.RecycleBinContent; protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells(); private int[] _userStartNodes; protected override int[] UserStartNodes => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); /// protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { var culture = queryStrings?["culture"]; var allowedUserOptions = GetAllowedUserMenuItemsForNode(entity); if (CanUserAccessNode(entity, allowedUserOptions, culture)) { //Special check to see if it ia a container, if so then we'll hide children. var isContainer = entity.IsContainer; // && (queryStrings.Get("isDialog") != "true"); var hasChildren = ShouldRenderChildrenOfContainer(entity); var node = CreateTreeNode( entity, Constants.ObjectTypes.Document, parentId, queryStrings, hasChildren); // set container style if it is one if (isContainer) { node.AdditionalData.Add("isContainer", true); node.SetContainerStyle(); } var documentEntity = (IDocumentEntitySlim)entity; if (!documentEntity.Variations.VariesByCulture()) { if (!documentEntity.Published) node.SetNotPublishedStyle(); else if (documentEntity.Edited) node.SetHasPendingVersionStyle(); } else { if (!culture.IsNullOrWhiteSpace()) { if (!documentEntity.Published || !documentEntity.PublishedCultures.Contains(culture)) node.SetNotPublishedStyle(); else if (documentEntity.EditedCultures.Contains(culture)) node.SetHasPendingVersionStyle(); } } node.AdditionalData.Add("variesByCulture", documentEntity.Variations.VariesByCulture()); node.AdditionalData.Add("contentType", documentEntity.ContentTypeAlias); if (Services.PublicAccessService.IsProtected(entity.Path)) node.SetProtectedStyle(); return node; } return null; } protected override MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings) { if (id == Constants.System.Root.ToInvariantString()) { var menu = new MenuItemCollection(); // if the user's start node is not the root then the only menu item to display is refresh if (UserStartNodes.Contains(Constants.System.Root) == false) { menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; } //set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; // we need to get the default permissions as you can't set permissions on the very root node var permission = Services.UserService.GetPermissions(Security.CurrentUser, Constants.System.Root).First(); var nodeActions = Current.Actions.FromEntityPermission(permission) .Select(x => new MenuItem(x)); //these two are the standard items menu.Items.Add(Services.TextService, opensDialog: true); menu.Items.Add(Services.TextService, true); //filter the standard items FilterUserAllowedMenuItems(menu, nodeActions); if (menu.Items.Any()) { menu.Items.Last().SeperatorBefore = true; } // add default actions for *all* users // fixme - temp disable RePublish as the page itself (republish.aspx) has been temp disabled //menu.Items.Add(Services.TextService.Localize("actions", ActionRePublish.Instance.Alias)).ConvertLegacyMenuItem(null, "content", "content"); menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; } //return a normal node menu: int iid; if (int.TryParse(id, out iid) == false) { throw new HttpResponseException(HttpStatusCode.NotFound); } var item = Services.EntityService.Get(iid, UmbracoObjectTypes.Document); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } //if the user has no path access for this node, all they can do is refresh if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService)) { var menu = new MenuItemCollection(); menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; } var nodeMenu = GetAllNodeMenuItems(item); //if the content node is in the recycle bin, don't have a default menu, just show the regular menu if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { nodeMenu.DefaultMenuAlias = null; nodeMenu = GetNodeMenuItemsForDeletedContent(item); } else { //set the default to create nodeMenu.DefaultMenuAlias = ActionNew.ActionAlias; } var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); return nodeMenu; } protected override UmbracoObjectTypes UmbracoObjectType => UmbracoObjectTypes.Document; /// /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access /// /// /// /// protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); return HasPathAccess(entity, queryStrings); } internal override IEnumerable GetChildrenFromEntityService(int entityId) => Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToList(); protected override IEnumerable GetChildEntities(string id, FormDataCollection queryStrings) { var result = base.GetChildEntities(id, queryStrings); var culture = queryStrings["culture"].TryConvertTo(); //if this is null we'll set it to the default. var cultureVal = (culture.Success ? culture.Result : null) ?? Services.LocalizationService.GetDefaultLanguageIsoCode(); // set names according to variations foreach (var entity in result) EnsureName(entity, cultureVal); return result; } /// /// Returns a collection of all menu items that can be on a content node /// /// /// protected MenuItemCollection GetAllNodeMenuItems(IUmbracoEntity item) { var menu = new MenuItemCollection(); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, true, opensDialog: true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, true); AddActionNode(item, menu, opensDialog: true); AddActionNode(item, menu, opensDialog: true); //fixme - conver this editor to angular AddActionNode(item, menu, true, convert: true, opensDialog: true); menu.Items.Add(new MenuItem("notify", Services.TextService) { Icon = "megaphone", SeperatorBefore = true, OpensDialog = true }); menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; } /// /// Returns a collection of all menu items that can be on a deleted (in recycle bin) content node /// /// /// protected MenuItemCollection GetNodeMenuItemsForDeletedContent(IUmbracoEntity item) { var menu = new MenuItemCollection(); menu.Items.Add(Services.TextService, opensDialog: true); menu.Items.Add(Services.TextService, opensDialog: true); menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; } /// /// set name according to variations /// /// /// private void EnsureName(IEntitySlim entity, string culture) { if (culture == null) { if (string.IsNullOrWhiteSpace(entity.Name)) entity.Name = "[[" + entity.Id + "]]"; return; } if (!(entity is IDocumentEntitySlim docEntity)) throw new InvalidOperationException($"Cannot render a tree node for a culture when the entity isn't {typeof(IDocumentEntitySlim)}, instead it is {entity.GetType()}"); // we are getting the tree for a given culture, // for those items that DO support cultures, we need to get the proper name, IF it exists // otherwise, invariant is fine (with brackets) if (docEntity.Variations.VariesByCulture()) { if (docEntity.CultureNames.TryGetValue(culture, out var name) && !string.IsNullOrWhiteSpace(name)) { entity.Name = name; } else { entity.Name = "(" + entity.Name + ")"; } } if (string.IsNullOrWhiteSpace(entity.Name)) entity.Name = "[[" + entity.Id + "]]"; } //fixme: Remove the need for converting to legacy private void AddActionNode(IUmbracoEntity item, MenuItemCollection menu, bool hasSeparator = false, bool convert = false, bool opensDialog = false) where TAction : IAction { //fixme: Inject var menuItem = menu.Items.Add(Services.TextService.Localize("actions", Current.Actions.GetAction().Alias), hasSeparator); if (convert) menuItem.ConvertLegacyMenuItem(item, "content", "content"); menuItem.OpensDialog = opensDialog; } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) { return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Document, pageSize, pageIndex, out totalFound, searchFrom); } } }