2021-01-12 16:24:50 +01:00
using System ;
2018-06-29 19:55:15 +02:00
using System.Collections.Generic ;
2021-09-15 13:40:08 +02:00
using System.Globalization ;
2018-06-29 19:55:15 +02:00
using System.Linq ;
2021-01-14 19:41:32 +01:00
using Microsoft.AspNetCore.Authorization ;
2020-06-09 07:49:26 +02:00
using Microsoft.AspNetCore.Http ;
2021-01-14 19:41:32 +01:00
using Microsoft.AspNetCore.Mvc ;
2020-09-21 09:52:58 +02:00
using Microsoft.Extensions.Logging ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Actions ;
2021-03-05 15:36:27 +01:00
using Umbraco.Cms.Core.Cache ;
2021-02-23 16:09:36 +01:00
using Umbraco.Cms.Core.Events ;
2021-02-25 15:08:56 +01:00
using Umbraco.Cms.Core.Mail ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.ContentEditing ;
using Umbraco.Cms.Core.Models.Entities ;
using Umbraco.Cms.Core.Models.Trees ;
using Umbraco.Cms.Core.Security ;
using Umbraco.Cms.Core.Services ;
using Umbraco.Cms.Core.Trees ;
2021-02-15 11:45:27 +01:00
using Umbraco.Cms.Infrastructure.Search ;
2021-02-10 11:42:04 +01:00
using Umbraco.Cms.Web.Common.Attributes ;
using Umbraco.Cms.Web.Common.Authorization ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2021-02-09 10:22:42 +01:00
using Constants = Umbraco . Cms . Core . Constants ;
2018-06-29 19:55:15 +02:00
2021-02-10 11:11:18 +01:00
namespace Umbraco.Cms.Web.BackOffice.Trees
2018-06-29 19:55:15 +02:00
{
2020-11-19 22:17:42 +11:00
[Authorize(Policy = AuthorizationPolicies.SectionAccessForContentTree)]
2019-11-05 13:45:42 +01:00
[Tree(Constants.Applications.Content, Constants.Trees.Content)]
2020-09-23 08:55:25 +02:00
[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)]
2018-06-29 19:55:15 +02:00
[CoreTree]
2019-05-31 20:29:00 +02:00
[SearchableTree("searchResultFormatter", "configureContentResult", 10)]
2018-06-29 19:55:15 +02:00
public class ContentTreeController : ContentTreeControllerBase , ISearchableTree
{
2018-12-03 22:10:56 +11:00
private readonly UmbracoTreeSearcher _treeSearcher ;
2018-12-11 15:42:32 +11:00
private readonly ActionCollection _actions ;
2020-02-17 14:58:34 +01:00
private readonly IMenuItemCollectionFactory _menuItemCollectionFactory ;
2020-10-21 16:51:00 +11:00
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor ;
2020-06-09 07:49:26 +02:00
private readonly IContentService _contentService ;
private readonly IEntityService _entityService ;
private readonly IPublicAccessService _publicAccessService ;
private readonly IUserService _userService ;
private readonly ILocalizationService _localizationService ;
2021-02-25 15:08:56 +01:00
private readonly IEmailSender _emailSender ;
2021-03-05 15:36:27 +01:00
private readonly AppCaches _appCaches ;
2018-12-03 22:10:56 +11:00
2020-02-14 13:04:49 +01:00
public ContentTreeController (
2020-06-09 07:49:26 +02:00
ILocalizedTextService localizedTextService ,
UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection ,
IMenuItemCollectionFactory menuItemCollectionFactory ,
IEntityService entityService ,
2020-10-21 16:51:00 +11:00
IBackOfficeSecurityAccessor backofficeSecurityAccessor ,
2020-09-21 09:52:58 +02:00
ILogger < ContentTreeController > logger ,
2020-06-09 07:49:26 +02:00
ActionCollection actionCollection ,
IUserService userService ,
IDataTypeService dataTypeService ,
2020-02-14 13:04:49 +01:00
UmbracoTreeSearcher treeSearcher ,
ActionCollection actions ,
2020-06-09 07:49:26 +02:00
IContentService contentService ,
IPublicAccessService publicAccessService ,
2021-02-25 15:08:56 +01:00
ILocalizationService localizationService ,
2021-03-01 13:26:34 +01:00
IEventAggregator eventAggregator ,
2021-03-05 15:36:27 +01:00
IEmailSender emailSender ,
AppCaches appCaches )
: base ( localizedTextService , umbracoApiControllerTypeCollection , menuItemCollectionFactory , entityService , backofficeSecurityAccessor , logger , actionCollection , userService , dataTypeService , eventAggregator , appCaches )
2019-10-15 00:07:44 +11:00
{
_treeSearcher = treeSearcher ;
_actions = actions ;
2020-02-17 14:58:34 +01:00
_menuItemCollectionFactory = menuItemCollectionFactory ;
2020-09-22 10:01:00 +02:00
_backofficeSecurityAccessor = backofficeSecurityAccessor ;
2020-06-09 07:49:26 +02:00
_contentService = contentService ;
_entityService = entityService ;
_publicAccessService = publicAccessService ;
_userService = userService ;
_localizationService = localizationService ;
2021-02-25 15:08:56 +01:00
_emailSender = emailSender ;
2021-03-05 15:36:27 +01:00
_appCaches = appCaches ;
2019-10-15 00:07:44 +11:00
}
2020-06-09 07:49:26 +02:00
protected override int RecycleBinId = > Constants . System . RecycleBinContent ;
protected override bool RecycleBinSmells = > _contentService . RecycleBinSmells ( ) ;
2022-03-31 12:52:26 +02:00
private int [ ] ? _userStartNodes ;
2020-06-09 07:49:26 +02:00
protected override int [ ] UserStartNodes
2022-03-31 12:52:26 +02:00
= > _userStartNodes ? ? ( _userStartNodes = _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser ? . CalculateContentStartNodeIds ( _entityService , _appCaches ) ? ? Array . Empty < int > ( ) ) ;
2020-06-09 07:49:26 +02:00
2018-06-29 19:55:15 +02:00
/// <inheritdoc />
2022-03-31 14:35:23 +02:00
protected override TreeNode ? GetSingleTreeNode ( IEntitySlim entity , string parentId , FormCollection ? queryStrings )
2018-06-29 19:55:15 +02:00
{
2022-03-31 14:35:23 +02:00
var culture = queryStrings ? [ "culture" ] . ToString ( ) ;
2018-07-09 18:26:15 +02:00
2018-06-29 19:55:15 +02:00
var allowedUserOptions = GetAllowedUserMenuItemsForNode ( entity ) ;
2018-09-21 15:49:37 +10:00
if ( CanUserAccessNode ( entity , allowedUserOptions , culture ) )
2018-06-29 19:55:15 +02:00
{
2019-01-26 10:52:19 -05:00
//Special check to see if it is a container, if so then we'll hide children.
2018-06-29 19:55:15 +02:00
var isContainer = entity . IsContainer ; // && (queryStrings.Get("isDialog") != "true");
var node = CreateTreeNode (
entity ,
2019-11-05 13:45:42 +01:00
Constants . ObjectTypes . Document ,
2018-06-29 19:55:15 +02:00
parentId ,
queryStrings ,
2018-12-19 18:01:36 +01:00
entity . HasChildren ) ;
2018-06-29 19:55:15 +02:00
2018-11-15 15:24:09 +11:00
// set container style if it is one
2018-06-29 19:55:15 +02:00
if ( isContainer )
{
node . AdditionalData . Add ( "isContainer" , true ) ;
node . SetContainerStyle ( ) ;
}
2018-11-15 15:24:09 +11:00
var documentEntity = ( IDocumentEntitySlim ) entity ;
if ( ! documentEntity . Variations . VariesByCulture ( ) )
{
if ( ! documentEntity . Published )
node . SetNotPublishedStyle ( ) ;
else if ( documentEntity . Edited )
node . SetHasPendingVersionStyle ( ) ;
}
2018-06-29 19:55:15 +02:00
else
{
2018-11-15 15:24:09 +11:00
if ( ! culture . IsNullOrWhiteSpace ( ) )
2018-09-21 15:49:37 +10:00
{
2018-11-15 15:24:09 +11:00
if ( ! documentEntity . Published | | ! documentEntity . PublishedCultures . Contains ( culture ) )
2018-09-21 15:49:37 +10:00
node . SetNotPublishedStyle ( ) ;
2018-11-15 15:24:09 +11:00
else if ( documentEntity . EditedCultures . Contains ( culture ) )
2018-09-27 19:30:31 +10:00
node . SetHasPendingVersionStyle ( ) ;
2018-09-21 15:49:37 +10:00
}
2018-06-29 19:55:15 +02:00
}
2018-11-15 15:24:09 +11:00
node . AdditionalData . Add ( "variesByCulture" , documentEntity . Variations . VariesByCulture ( ) ) ;
node . AdditionalData . Add ( "contentType" , documentEntity . ContentTypeAlias ) ;
2022-02-10 10:32:45 +01:00
if ( _publicAccessService . IsProtected ( entity . Path ) . Success )
2018-06-29 19:55:15 +02:00
node . SetProtectedStyle ( ) ;
return node ;
}
return null ;
}
2021-01-13 11:02:29 +01:00
protected override ActionResult < MenuItemCollection > PerformGetMenuForNode ( string id , FormCollection queryStrings )
2018-06-29 19:55:15 +02:00
{
2019-11-05 13:45:42 +01:00
if ( id = = Constants . System . RootString )
2018-06-29 19:55:15 +02:00
{
2020-02-17 14:58:34 +01:00
var menu = _menuItemCollectionFactory . Create ( ) ;
2018-06-29 19:55:15 +02:00
// if the user's start node is not the root then the only menu item to display is refresh
2019-11-05 13:45:42 +01:00
if ( UserStartNodes . Contains ( Constants . System . Root ) = = false )
2018-06-29 19:55:15 +02:00
{
2020-06-09 07:49:26 +02:00
menu . Items . Add ( new RefreshNode ( LocalizedTextService , true ) ) ;
2018-06-29 19:55:15 +02:00
return menu ;
}
//set the default to create
2018-10-29 17:27:33 +11:00
menu . DefaultMenuAlias = ActionNew . ActionAlias ;
2018-06-29 19:55:15 +02:00
// we need to get the default permissions as you can't set permissions on the very root node
2022-03-31 12:52:26 +02:00
var permission = _userService . GetPermissions ( _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser , Constants . System . Root ) . First ( ) ;
2018-12-11 15:42:32 +11:00
var nodeActions = _actions . FromEntityPermission ( permission )
2018-06-29 19:55:15 +02:00
. Select ( x = > new MenuItem ( x ) ) ;
//these two are the standard items
2020-06-09 07:49:26 +02:00
menu . Items . Add < ActionNew > ( LocalizedTextService , opensDialog : true ) ;
2021-03-05 15:36:27 +01:00
menu . Items . Add < ActionSort > ( LocalizedTextService , true , opensDialog : true ) ;
2018-06-29 19:55:15 +02:00
//filter the standard items
FilterUserAllowedMenuItems ( menu , nodeActions ) ;
if ( menu . Items . Any ( ) )
{
2019-01-14 09:32:32 -05:00
menu . Items . Last ( ) . SeparatorBefore = true ;
2018-06-29 19:55:15 +02:00
}
// add default actions for *all* users
2020-06-09 07:49:26 +02:00
menu . Items . Add ( new RefreshNode ( LocalizedTextService , true ) ) ;
2018-06-29 19:55:15 +02:00
return menu ;
}
//return a normal node menu:
int iid ;
2021-09-15 13:40:08 +02:00
if ( int . TryParse ( id , NumberStyles . Integer , CultureInfo . InvariantCulture , out iid ) = = false )
2018-06-29 19:55:15 +02:00
{
2021-01-13 11:02:29 +01:00
return NotFound ( ) ;
2018-06-29 19:55:15 +02:00
}
2020-06-09 07:49:26 +02:00
var item = _entityService . Get ( iid , UmbracoObjectTypes . Document ) ;
2018-06-29 19:55:15 +02:00
if ( item = = null )
{
2021-01-13 11:02:29 +01:00
return NotFound ( ) ;
2018-06-29 19:55:15 +02:00
}
//if the user has no path access for this node, all they can do is refresh
2022-03-31 12:52:26 +02:00
if ( ! _backofficeSecurityAccessor . BackOfficeSecurity ? . CurrentUser ? . HasContentPathAccess ( item , _entityService , _appCaches ) ? ? false )
2018-06-29 19:55:15 +02:00
{
2020-02-17 14:58:34 +01:00
var menu = _menuItemCollectionFactory . Create ( ) ;
2020-06-09 07:49:26 +02:00
menu . Items . Add ( new RefreshNode ( LocalizedTextService , true ) ) ;
2018-06-29 19:55:15 +02:00
return menu ;
}
2019-11-05 12:54:22 +01:00
var nodeMenu = GetAllNodeMenuItems ( item ) ;
2018-07-11 15:58:48 +10:00
//if the content node is in the recycle bin, don't have a default menu, just show the regular menu
2021-01-22 15:02:25 +13:00
if ( item . Path . Split ( Constants . CharArrays . Comma , StringSplitOptions . RemoveEmptyEntries ) . Contains ( RecycleBinId . ToInvariantString ( ) ) )
2018-06-29 19:55:15 +02:00
{
nodeMenu . DefaultMenuAlias = null ;
2018-07-11 15:58:48 +10:00
nodeMenu = GetNodeMenuItemsForDeletedContent ( item ) ;
2018-06-29 19:55:15 +02:00
}
else
{
//set the default to create
2018-10-29 17:27:33 +11:00
nodeMenu . DefaultMenuAlias = ActionNew . ActionAlias ;
2018-06-29 19:55:15 +02:00
}
2018-07-11 15:58:48 +10:00
var allowedMenuItems = GetAllowedUserMenuItemsForNode ( item ) ;
FilterUserAllowedMenuItems ( nodeMenu , allowedMenuItems ) ;
2018-06-29 19:55:15 +02:00
return nodeMenu ;
}
2018-08-17 00:50:05 +10:00
protected override UmbracoObjectTypes UmbracoObjectType = > UmbracoObjectTypes . Document ;
2018-06-29 19:55:15 +02:00
/// <summary>
/// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
2020-06-09 07:49:26 +02:00
protected override bool HasPathAccess ( string id , FormCollection queryStrings )
2018-06-29 19:55:15 +02:00
{
var entity = GetEntityFromId ( id ) ;
return HasPathAccess ( entity , queryStrings ) ;
2018-05-10 18:20:33 +10:00
}
2021-01-12 16:24:50 +01:00
protected override ActionResult < IEnumerable < IEntitySlim > > GetChildEntities ( string id , FormCollection queryStrings )
2018-05-10 18:20:33 +10:00
{
var result = base . GetChildEntities ( id , queryStrings ) ;
2021-01-14 19:41:32 +01:00
if ( ! ( result . Result is null ) )
{
return result . Result ;
}
2018-05-10 18:20:33 +10:00
var culture = queryStrings [ "culture" ] . TryConvertTo < string > ( ) ;
2018-06-29 19:55:15 +02:00
//if this is null we'll set it to the default.
2022-03-31 12:52:26 +02:00
var cultureVal = ( culture . Success ? culture . Result : null ) ? . IfNullOrWhiteSpace ( _localizationService . GetDefaultLanguageIsoCode ( ) ) ;
2018-06-29 19:55:15 +02:00
// set names according to variations
2022-03-31 12:52:26 +02:00
foreach ( var entity in result . Value ! )
2019-01-30 22:01:57 +11:00
{
2018-05-10 18:20:33 +10:00
EnsureName ( entity , cultureVal ) ;
2019-01-30 22:01:57 +11:00
}
2018-05-10 18:20:33 +10:00
2018-07-09 18:26:15 +02:00
return result ;
2018-06-29 19:55:15 +02:00
}
/// <summary>
/// Returns a collection of all menu items that can be on a content node
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected MenuItemCollection GetAllNodeMenuItems ( IUmbracoEntity item )
{
2020-02-17 14:58:34 +01:00
var menu = _menuItemCollectionFactory . Create ( ) ;
2018-10-30 00:34:43 +11:00
AddActionNode < ActionNew > ( item , menu , opensDialog : true ) ;
AddActionNode < ActionDelete > ( item , menu , opensDialog : true ) ;
AddActionNode < ActionCreateBlueprintFromContent > ( item , menu , opensDialog : true ) ;
AddActionNode < ActionMove > ( item , menu , true , opensDialog : true ) ;
AddActionNode < ActionCopy > ( item , menu , opensDialog : true ) ;
2020-10-31 23:19:59 +01:00
AddActionNode < ActionSort > ( item , menu , true , opensDialog : true ) ;
2018-10-30 00:34:43 +11:00
AddActionNode < ActionAssignDomain > ( item , menu , opensDialog : true ) ;
AddActionNode < ActionRights > ( item , menu , opensDialog : true ) ;
2019-01-30 22:01:57 +11:00
AddActionNode < ActionProtect > ( item , menu , true , opensDialog : true ) ;
2021-02-25 15:08:56 +01:00
if ( _emailSender . CanSendRequiredEmail ( ) )
2018-10-29 23:23:21 +11:00
{
2022-01-21 12:40:18 +01:00
menu . Items . Add ( new MenuItem ( "notify" , LocalizedTextService )
{
Icon = "megaphone" ,
SeparatorBefore = true ,
OpensDialog = true
} ) ;
2019-01-17 12:05:56 +01:00
}
2019-05-26 19:06:24 +02:00
if ( ( item is DocumentEntitySlim documentEntity & & documentEntity . IsContainer ) = = false )
{
2020-06-09 07:49:26 +02:00
menu . Items . Add ( new RefreshNode ( LocalizedTextService , true ) ) ;
2019-05-26 19:06:24 +02:00
}
2018-06-29 19:55:15 +02:00
return menu ;
}
2018-07-11 15:58:48 +10:00
/// <summary>
/// Returns a collection of all menu items that can be on a deleted (in recycle bin) content node
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected MenuItemCollection GetNodeMenuItemsForDeletedContent ( IUmbracoEntity item )
{
2020-02-17 14:58:34 +01:00
var menu = _menuItemCollectionFactory . Create ( ) ;
2020-06-09 07:49:26 +02:00
menu . Items . Add < ActionRestore > ( LocalizedTextService , opensDialog : true ) ;
menu . Items . Add < ActionMove > ( LocalizedTextService , opensDialog : true ) ;
menu . Items . Add < ActionDelete > ( LocalizedTextService , opensDialog : true ) ;
2018-07-11 15:58:48 +10:00
2020-06-09 07:49:26 +02:00
menu . Items . Add ( new RefreshNode ( LocalizedTextService , true ) ) ;
2018-07-11 15:58:48 +10:00
return menu ;
}
2019-11-05 12:54:22 +01:00
2018-06-29 19:55:15 +02:00
/// <summary>
/// set name according to variations
/// </summary>
/// <param name="entity"></param>
/// <param name="culture"></param>
2022-03-31 12:52:26 +02:00
private void EnsureName ( IEntitySlim entity , string? culture )
2018-06-29 19:55:15 +02:00
{
if ( culture = = null )
{
if ( string . IsNullOrWhiteSpace ( entity . Name ) )
2019-01-30 22:01:57 +11:00
{
2018-06-29 19:55:15 +02:00
entity . Name = "[[" + entity . Id + "]]" ;
2019-01-30 22:01:57 +11:00
}
2018-06-29 19:55:15 +02:00
return ;
}
2018-05-10 18:20:33 +10:00
2018-05-10 19:16:46 +10:00
if ( ! ( entity is IDocumentEntitySlim docEntity ) )
2019-01-30 22:01:57 +11:00
{
2018-05-10 19:16:46 +10:00
throw new InvalidOperationException ( $"Cannot render a tree node for a culture when the entity isn't {typeof(IDocumentEntitySlim)}, instead it is {entity.GetType()}" ) ;
2019-01-30 22:01:57 +11:00
}
2018-05-10 18:20:33 +10:00
2018-05-10 19:16:46 +10:00
// 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
2018-08-17 00:50:05 +10:00
// otherwise, invariant is fine (with brackets)
2018-05-10 18:20:33 +10:00
2018-08-17 00:50:05 +10:00
if ( docEntity . Variations . VariesByCulture ( ) )
2018-05-10 19:16:46 +10:00
{
2018-08-17 00:50:05 +10:00
if ( docEntity . CultureNames . TryGetValue ( culture , out var name ) & &
! string . IsNullOrWhiteSpace ( name ) )
{
entity . Name = name ;
}
else
{
entity . Name = "(" + entity . Name + ")" ;
}
2018-05-10 18:20:33 +10:00
}
2018-05-10 19:16:46 +10:00
if ( string . IsNullOrWhiteSpace ( entity . Name ) )
2019-01-30 22:01:57 +11:00
{
2018-06-29 19:55:15 +02:00
entity . Name = "[[" + entity . Id + "]]" ;
2019-01-30 22:01:57 +11:00
}
2018-06-29 19:55:15 +02:00
}
2019-01-30 22:01:57 +11:00
private void AddActionNode < TAction > ( IUmbracoEntity item , MenuItemCollection menu , bool hasSeparator = false , bool opensDialog = false )
2018-06-29 19:55:15 +02:00
where TAction : IAction
{
2020-06-09 07:49:26 +02:00
var menuItem = menu . Items . Add < TAction > ( LocalizedTextService , hasSeparator , opensDialog ) ;
2018-06-29 19:55:15 +02:00
}
2022-03-31 12:52:26 +02:00
public IEnumerable < SearchResultEntity > Search ( string query , int pageSize , long pageIndex , out long totalFound , string? searchFrom = null )
2018-06-29 19:55:15 +02:00
{
2019-06-13 23:47:48 +10:00
return _treeSearcher . ExamineSearch ( query , UmbracoEntityTypes . Document , pageSize , pageIndex , out totalFound , searchFrom ) ;
2018-05-10 18:20:33 +10:00
}
2018-06-29 19:55:15 +02:00
}
}