2013-07-22 15:18:10 +10:00
using System ;
using System.Collections.Generic ;
2013-08-09 18:04:44 +10:00
using System.Globalization ;
2013-05-28 18:27:11 -10:00
using System.Linq ;
using System.Net ;
2013-05-27 01:23:49 -10:00
using System.Net.Http ;
2013-11-14 18:50:31 +11:00
using System.Text ;
2013-05-27 01:23:49 -10:00
using System.Web.Http ;
2017-05-12 14:49:44 +02:00
using System.Web.Http.Controllers ;
2013-05-27 01:23:49 -10:00
using System.Web.Http.ModelBinding ;
2013-07-25 15:31:26 +10:00
using AutoMapper ;
2013-07-16 18:23:20 +10:00
using Umbraco.Core ;
2013-08-07 15:42:28 +10:00
using Umbraco.Core.Logging ;
2013-06-09 11:40:52 -02:00
using Umbraco.Core.Models ;
2013-06-17 01:06:31 +02:00
using Umbraco.Core.Models.Membership ;
2013-08-06 18:42:36 +10:00
using Umbraco.Core.Persistence.DatabaseModelDefinitions ;
2013-06-11 14:43:36 +02:00
using Umbraco.Core.Services ;
2013-05-27 01:23:49 -10:00
using Umbraco.Web.Models.ContentEditing ;
using Umbraco.Web.Models.Mapping ;
using Umbraco.Web.Mvc ;
2013-05-30 21:21:52 -10:00
using Umbraco.Web.WebApi ;
using Umbraco.Web.WebApi.Binders ;
2013-05-27 01:23:49 -10:00
using Umbraco.Web.WebApi.Filters ;
2016-05-02 15:38:45 +02:00
using Umbraco.Core.Persistence.Querying ;
2016-05-26 17:12:04 +02:00
using Umbraco.Web.PublishedCache ;
2017-09-12 16:22:16 +02:00
using Umbraco.Core.Events ;
2018-03-16 09:06:44 +01:00
using Umbraco.Core.Models.Validation ;
2018-04-04 01:59:51 +10:00
using Umbraco.Web.Models ;
2018-03-16 09:06:44 +01:00
using Umbraco.Web._Legacy.Actions ;
2013-08-07 15:42:28 +10:00
using Constants = Umbraco . Core . Constants ;
2018-04-04 01:59:51 +10:00
using ContentVariation = Umbraco . Core . Models . ContentVariation ;
2018-04-10 01:38:35 +10:00
using Language = Umbraco . Web . Models . ContentEditing . Language ;
2013-05-27 01:23:49 -10:00
2013-05-30 21:21:52 -10:00
namespace Umbraco.Web.Editors
2013-05-27 01:23:49 -10:00
{
2013-05-27 21:32:37 -10:00
/// <summary>
/// The API controller used for editing content
/// </summary>
2013-08-07 19:28:32 +10:00
/// <remarks>
/// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting
/// access to ALL of the methods on this controller will need access to the content application.
/// </remarks>
2013-06-10 10:36:59 -02:00
[PluginController("UmbracoApi")]
2016-05-26 17:12:04 +02:00
[UmbracoApplicationAuthorize(Constants.Applications.Content)]
2017-05-12 14:49:44 +02:00
[ContentControllerConfiguration]
2013-07-23 18:55:31 +10:00
public class ContentController : ContentControllerBase
2016-02-26 14:30:32 +00:00
{
2017-10-31 12:48:24 +01:00
private readonly IPublishedSnapshotService _publishedSnapshotService ;
2016-05-26 17:12:04 +02:00
2017-10-31 12:48:24 +01:00
public ContentController ( IPublishedSnapshotService publishedSnapshotService )
2016-02-26 14:30:32 +00:00
{
2017-10-31 12:48:24 +01:00
if ( publishedSnapshotService = = null ) throw new ArgumentNullException ( nameof ( publishedSnapshotService ) ) ;
_publishedSnapshotService = publishedSnapshotService ;
2013-05-27 01:23:49 -10:00
}
2017-05-12 14:49:44 +02:00
/// <summary>
/// Configures this controller with a custom action selector
/// </summary>
private class ContentControllerConfigurationAttribute : Attribute , IControllerConfiguration
{
public void Initialize ( HttpControllerSettings controllerSettings , HttpControllerDescriptor controllerDescriptor )
{
controllerSettings . Services . Replace ( typeof ( IHttpActionSelector ) , new ParameterSwapControllerActionSelector (
new ParameterSwapControllerActionSelector . ParameterSwapInfo ( "GetNiceUrl" , "id" , typeof ( int ) , typeof ( Guid ) , typeof ( Udi ) ) ) ) ;
}
}
2013-08-07 19:28:32 +10:00
/// <summary>
/// Return content for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
2013-09-03 13:59:25 +10:00
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemDisplay>))]
2013-06-11 14:43:36 +02:00
public IEnumerable < ContentItemDisplay > GetByIds ( [ FromUri ] int [ ] ids )
{
2018-05-02 17:50:58 +02:00
//fixme what about cultures?
2018-05-02 14:52:00 +10:00
2013-10-09 15:21:01 +11:00
var foundContent = Services . ContentService . GetByIds ( ids ) ;
2018-04-04 01:59:51 +10:00
return foundContent . Select ( x = > MapToDisplay ( x ) ) ;
2013-06-11 14:43:36 +02:00
}
2017-09-12 16:22:16 +02:00
/// <summary>
/// Updates the permissions for a content item for a particular user group
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
/// <remarks>
/// Permission check is done for letter 'R' which is for <see cref="ActionRights"/> which the user must have access to to update
/// </remarks>
[EnsureUserPermissionForContent("saveModel.ContentId", 'R')]
public IEnumerable < AssignedUserGroupPermissions > PostSaveUserGroupPermissions ( UserGroupPermissionsSave saveModel )
{
if ( saveModel . ContentId < = 0 ) throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ) ) ;
2017-09-19 15:51:47 +02:00
//TODO: Should non-admins be alowed to set granular permissions?
2017-09-12 16:22:16 +02:00
var content = Services . ContentService . GetById ( saveModel . ContentId ) ;
if ( content = = null ) throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ) ) ;
2017-09-23 10:08:18 +02:00
2017-09-12 16:22:16 +02:00
//current permissions explicitly assigned to this content item
2017-11-30 13:56:29 +01:00
var contentPermissions = Services . ContentService . GetPermissions ( content )
2017-09-12 16:22:16 +02:00
. ToDictionary ( x = > x . UserGroupId , x = > x ) ;
var allUserGroups = Services . UserService . GetAllUserGroups ( ) . ToArray ( ) ;
//loop through each user group
foreach ( var userGroup in allUserGroups )
{
//check if there's a permission set posted up for this user group
IEnumerable < string > groupPermissions ;
if ( saveModel . AssignedPermissions . TryGetValue ( userGroup . Id , out groupPermissions ) )
{
//create a string collection of the assigned letters
var groupPermissionCodes = groupPermissions . ToArray ( ) ;
//check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions
//for this group/node which will go back to the defaults
if ( groupPermissionCodes . Length = = 0 )
{
Services . UserService . RemoveUserGroupPermissions ( userGroup . Id , content . Id ) ;
}
//check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored
else if ( userGroup . Permissions . UnsortedSequenceEqual ( groupPermissionCodes ) )
{
//only remove them if they are actually currently assigned
if ( contentPermissions . ContainsKey ( userGroup . Id ) )
{
//remove these permissions from this node for this group since the ones being assigned are the same as the defaults
Services . UserService . RemoveUserGroupPermissions ( userGroup . Id , content . Id ) ;
}
}
//if they are different we need to update, otherwise there's nothing to update
else if ( contentPermissions . ContainsKey ( userGroup . Id ) = = false | | contentPermissions [ userGroup . Id ] . AssignedPermissions . UnsortedSequenceEqual ( groupPermissionCodes ) = = false )
{
2017-09-23 10:08:18 +02:00
2017-09-12 16:22:16 +02:00
Services . UserService . ReplaceUserGroupPermissions ( userGroup . Id , groupPermissionCodes . Select ( x = > x [ 0 ] ) , content . Id ) ;
2017-09-23 10:08:18 +02:00
}
}
2017-09-12 16:22:16 +02:00
}
return GetDetailedPermissions ( content , allUserGroups ) ;
}
/// <summary>
/// Returns the user group permissions for user groups assigned to this node
/// </summary>
/// <param name="contentId"></param>
/// <returns></returns>
/// <remarks>
/// Permission check is done for letter 'R' which is for <see cref="ActionRights"/> which the user must have access to to view
/// </remarks>
[EnsureUserPermissionForContent("contentId", 'R')]
public IEnumerable < AssignedUserGroupPermissions > GetDetailedPermissions ( int contentId )
{
if ( contentId < = 0 ) throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ) ) ;
var content = Services . ContentService . GetById ( contentId ) ;
2017-09-23 10:08:18 +02:00
if ( content = = null ) throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ) ) ;
2017-09-19 15:51:47 +02:00
//TODO: Should non-admins be able to see detailed permissions?
2017-09-12 16:22:16 +02:00
var allUserGroups = Services . UserService . GetAllUserGroups ( ) ;
return GetDetailedPermissions ( content , allUserGroups ) ;
}
private IEnumerable < AssignedUserGroupPermissions > GetDetailedPermissions ( IContent content , IEnumerable < IUserGroup > allUserGroups )
{
//get all user groups and map their default permissions to the AssignedUserGroupPermissions model.
//we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults.
var defaultPermissionsByGroup = Mapper . Map < IEnumerable < AssignedUserGroupPermissions > > ( allUserGroups ) . ToArray ( ) ;
var defaultPermissionsAsDictionary = defaultPermissionsByGroup
. ToDictionary ( x = > Convert . ToInt32 ( x . Id ) , x = > x ) ;
//get the actual assigned permissions
2017-11-30 13:56:29 +01:00
var assignedPermissionsByGroup = Services . ContentService . GetPermissions ( content ) . ToArray ( ) ;
2017-09-12 16:22:16 +02:00
//iterate over assigned and update the defaults with the real values
foreach ( var assignedGroupPermission in assignedPermissionsByGroup )
{
var defaultUserGroupPermissions = defaultPermissionsAsDictionary [ assignedGroupPermission . UserGroupId ] ;
//clone the default permissions model to the assigned ones
defaultUserGroupPermissions . AssignedPermissions = AssignedUserGroupPermissions . ClonePermissions ( defaultUserGroupPermissions . DefaultPermissions ) ;
//since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions
//and we'll re-check it if it's one of the explicitly assigned ones
foreach ( var permission in defaultUserGroupPermissions . AssignedPermissions . SelectMany ( x = > x . Value ) )
{
permission . Checked = false ;
permission . Checked = assignedGroupPermission . AssignedPermissions . Contains ( permission . PermissionCode , StringComparer . InvariantCulture ) ;
}
2017-09-23 10:08:18 +02:00
2017-09-12 16:22:16 +02:00
}
2017-09-23 10:08:18 +02:00
2017-09-12 16:22:16 +02:00
return defaultPermissionsByGroup ;
}
2016-02-11 14:04:14 +01:00
/// <summary>
/// Returns an item to be used to display the recycle bin for content
/// </summary>
/// <returns></returns>
public ContentItemDisplay GetRecycleBin ( )
{
var display = new ContentItemDisplay
{
Id = Constants . System . RecycleBinContent ,
Alias = "recycleBin" ,
ParentId = - 1 ,
Name = Services . TextService . Localize ( "general/recycleBin" ) ,
ContentTypeAlias = "recycleBin" ,
CreateDate = DateTime . Now ,
IsContainer = true ,
Path = "-1," + Constants . System . RecycleBinContent
} ;
TabsAndPropertiesResolver . AddListView ( display , "content" , Services . DataTypeService , Services . TextService ) ;
return display ;
}
2018-05-02 14:52:00 +10:00
//fixme what about cultures?
2017-09-12 16:22:16 +02:00
public ContentItemDisplay GetBlueprintById ( int id )
{
var foundContent = Services . ContentService . GetBlueprintById ( id ) ;
if ( foundContent = = null )
{
HandleContentNotFound ( id ) ;
}
2018-04-04 01:59:51 +10:00
var content = MapToDisplay ( foundContent ) ;
2017-09-12 16:22:16 +02:00
SetupBlueprint ( content , foundContent ) ;
return content ;
}
private static void SetupBlueprint ( ContentItemDisplay content , IContent persistedContent )
{
content . AllowPreview = false ;
//set a custom path since the tree that renders this has the content type id as the parent
content . Path = string . Format ( "-1,{0},{1}" , persistedContent . ContentTypeId , content . Id ) ;
2018-05-08 00:37:41 +10:00
content . AllowedActions = new [ ] { "A" } ;
2017-09-19 15:51:47 +02:00
content . IsBlueprint = true ;
2017-09-12 16:22:16 +02:00
2018-05-08 00:37:41 +10:00
var excludeProps = new [ ] { "_umb_urls" , "_umb_releasedate" , "_umb_expiredate" , "_umb_template" } ;
2017-09-12 16:22:16 +02:00
var propsTab = content . Tabs . Last ( ) ;
propsTab . Properties = propsTab . Properties
. Where ( p = > excludeProps . Contains ( p . Alias ) = = false ) ;
}
2013-05-27 01:23:49 -10:00
/// <summary>
/// Gets the content json for the content id
/// </summary>
/// <param name="id"></param>
2018-05-01 18:17:07 +10:00
/// <param name="culture"></param>
2013-05-27 01:23:49 -10:00
/// <returns></returns>
2016-01-27 13:20:13 +01:00
[OutgoingEditorModelEvent]
2013-08-07 19:28:32 +10:00
[EnsureUserPermissionForContent("id")]
2018-05-01 18:17:07 +10:00
public ContentItemDisplay GetById ( int id , string culture = null )
2013-05-27 01:23:49 -10:00
{
2016-02-26 14:30:32 +00:00
var foundContent = GetObjectFromRequest ( ( ) = > Services . ContentService . GetById ( id ) ) ;
2013-05-27 01:23:49 -10:00
if ( foundContent = = null )
{
2013-07-23 18:55:31 +10:00
HandleContentNotFound ( id ) ;
2018-04-04 01:59:51 +10:00
return null ; //irrelevant since the above throws
2013-05-27 01:23:49 -10:00
}
2013-12-12 13:27:33 +11:00
2018-05-01 18:17:07 +10:00
var content = MapToDisplay ( foundContent , culture ) ;
2013-09-19 10:38:37 +02:00
return content ;
2016-02-26 14:30:32 +00:00
}
2013-05-27 01:23:49 -10:00
2013-06-09 11:40:52 -02:00
/// <summary>
2016-05-26 17:12:04 +02:00
/// Gets an empty content item for the
2013-06-09 11:40:52 -02:00
/// </summary>
/// <param name="contentTypeAlias"></param>
/// <param name="parentId"></param>
2013-11-15 16:56:51 +11:00
/// <returns>
/// If this is a container type, we'll remove the umbContainerView tab for a new item since
/// it cannot actually list children if it doesn't exist yet.
/// </returns>
2016-01-27 13:20:13 +01:00
[OutgoingEditorModelEvent]
2013-06-11 14:30:37 +02:00
public ContentItemDisplay GetEmpty ( string contentTypeAlias , int parentId )
2013-06-09 11:40:52 -02:00
{
2016-05-18 10:55:19 +02:00
var contentType = Services . ContentTypeService . Get ( contentTypeAlias ) ;
2013-06-09 11:40:52 -02:00
if ( contentType = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
2013-06-10 10:36:59 -02:00
2018-03-02 15:48:21 +01:00
var emptyContent = Services . ContentService . Create ( "" , parentId , contentType . Alias , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-04-04 01:59:51 +10:00
var mapped = MapToDisplay ( emptyContent ) ;
2013-11-15 16:56:51 +11:00
//remove this tab if it exists: umbContainerView
2014-09-18 09:48:08 +10:00
var containerTab = mapped . Tabs . FirstOrDefault ( x = > x . Alias = = Constants . Conventions . PropertyGroups . ListViewGroupName ) ;
2016-02-26 14:30:32 +00:00
mapped . Tabs = mapped . Tabs . Except ( new [ ] { containerTab } ) ;
2018-05-08 01:16:32 +10:00
if ( contentType . Variations . Has ( ContentVariation . CultureNeutral ) )
{
//Remove all variants except for the default since currently the default must be saved before other variants can be edited
//TODO: Allow for editing all variants at once ... this will be a future task
mapped . Variants = new [ ] { mapped . Variants . FirstOrDefault ( x = > x . IsCurrent ) } ;
}
2018-04-09 15:01:55 +10:00
2013-11-15 16:56:51 +11:00
return mapped ;
2013-06-09 11:40:52 -02:00
}
2013-10-04 10:53:07 +02:00
2017-09-12 16:22:16 +02:00
[OutgoingEditorModelEvent]
2017-09-19 15:51:47 +02:00
public ContentItemDisplay GetEmpty ( int blueprintId , int parentId )
2017-09-12 16:22:16 +02:00
{
var blueprint = Services . ContentService . GetBlueprintById ( blueprintId ) ;
if ( blueprint = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
blueprint . Id = 0 ;
blueprint . Name = string . Empty ;
2017-09-19 15:51:47 +02:00
blueprint . ParentId = parentId ;
2017-09-12 16:22:16 +02:00
var mapped = Mapper . Map < ContentItemDisplay > ( blueprint ) ;
//remove this tab if it exists: umbContainerView
var containerTab = mapped . Tabs . FirstOrDefault ( x = > x . Alias = = Constants . Conventions . PropertyGroups . ListViewGroupName ) ;
mapped . Tabs = mapped . Tabs . Except ( new [ ] { containerTab } ) ;
return mapped ;
}
2013-10-04 10:53:07 +02:00
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2013-11-14 18:50:31 +11:00
public HttpResponseMessage GetNiceUrl ( int id )
2013-10-04 10:53:07 +02:00
{
2018-04-27 10:57:51 +10:00
var url = Umbraco . Url ( id ) ;
2013-12-04 17:05:05 +11:00
var response = Request . CreateResponse ( HttpStatusCode . OK ) ;
2016-02-26 14:30:32 +00:00
response . Content = new StringContent ( url , Encoding . UTF8 , "application/json" ) ;
2013-11-14 18:50:31 +11:00
return response ;
2013-10-04 10:53:07 +02:00
}
2017-05-12 14:49:44 +02:00
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public HttpResponseMessage GetNiceUrl ( Guid id )
{
var url = Umbraco . UrlProvider . GetUrl ( id ) ;
var response = Request . CreateResponse ( HttpStatusCode . OK ) ;
response . Content = new StringContent ( url , Encoding . UTF8 , "application/json" ) ;
return response ;
}
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public HttpResponseMessage GetNiceUrl ( Udi id )
{
var guidUdi = id as GuidUdi ;
if ( guidUdi ! = null )
{
return GetNiceUrl ( guidUdi . Guid ) ;
}
2017-07-20 11:21:28 +02:00
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2017-05-12 14:49:44 +02:00
}
2013-05-27 01:23:49 -10:00
/// <summary>
2013-08-06 18:42:36 +10:00
/// Gets the children for the content id passed in
/// </summary>
2016-05-26 17:12:04 +02:00
/// <returns></returns>
2013-09-03 13:59:25 +10:00
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic, IContent>>), "Items")]
2013-08-06 18:42:36 +10:00
public PagedResult < ContentItemBasic < ContentPropertyBasic , IContent > > GetChildren (
2016-02-26 14:30:32 +00:00
int id ,
int pageNumber = 0 , //TODO: This should be '1' as it's not the index
int pageSize = 0 ,
string orderBy = "SortOrder" ,
Direction orderDirection = Direction . Ascending ,
2016-04-06 09:17:05 +02:00
bool orderBySystemField = true ,
2016-02-26 14:30:32 +00:00
string filter = "" )
2018-04-17 08:36:36 +02:00
{
return GetChildren ( id , null , pageNumber , pageSize , orderBy , orderDirection , orderBySystemField , filter ) ;
}
/// <summary>
/// Gets the children for the content id passed in
/// </summary>
/// <returns></returns>
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic, IContent>>), "Items")]
public PagedResult < ContentItemBasic < ContentPropertyBasic , IContent > > GetChildren (
int id ,
string includeProperties ,
int pageNumber = 0 , //TODO: This should be '1' as it's not the index
int pageSize = 0 ,
string orderBy = "SortOrder" ,
Direction orderDirection = Direction . Ascending ,
bool orderBySystemField = true ,
string filter = "" )
2016-05-26 17:12:04 +02:00
{
2015-05-04 14:36:46 +10:00
long totalChildren ;
2014-08-24 23:37:10 +02:00
IContent [ ] children ;
if ( pageNumber > 0 & & pageSize > 0 )
{
2016-05-02 15:38:45 +02:00
IQuery < IContent > queryFilter = null ;
2016-04-29 00:50:26 +02:00
if ( filter . IsNullOrWhiteSpace ( ) = = false )
{
2016-05-26 17:12:04 +02:00
//add the default text filter
2017-09-22 18:28:21 +02:00
queryFilter = SqlContext . Query < IContent > ( )
2016-05-02 15:38:45 +02:00
. Where ( x = > x . Name . Contains ( filter ) ) ;
2016-04-29 00:50:26 +02:00
}
2016-05-26 17:12:04 +02:00
2016-02-26 14:30:32 +00:00
children = Services . ContentService
2016-04-29 00:50:26 +02:00
. GetPagedChildren (
2016-05-26 17:12:04 +02:00
id , ( pageNumber - 1 ) , pageSize ,
out totalChildren ,
2016-05-02 15:38:45 +02:00
orderBy , orderDirection , orderBySystemField ,
queryFilter ) . ToArray ( ) ;
2014-08-24 23:37:10 +02:00
}
else
{
children = Services . ContentService . GetChildren ( id ) . ToArray ( ) ;
totalChildren = children . Length ;
}
2013-09-26 16:26:05 +10:00
2013-10-21 14:27:46 +02:00
if ( totalChildren = = 0 )
2013-11-20 10:24:45 +01:00
{
2014-08-12 08:15:43 +01:00
return new PagedResult < ContentItemBasic < ContentPropertyBasic , IContent > > ( 0 , 0 , 0 ) ;
2013-11-20 10:24:45 +01:00
}
2014-08-13 20:02:51 +01:00
var pagedResult = new PagedResult < ContentItemBasic < ContentPropertyBasic , IContent > > ( totalChildren , pageNumber , pageSize ) ;
2018-04-09 15:43:17 +02:00
pagedResult . Items = children . Select ( content = >
Mapper . Map < IContent , ContentItemBasic < ContentPropertyBasic , IContent > > ( content ,
2018-04-17 08:36:36 +02:00
opts = >
{
// if there's a list of property aliases to map - we will make sure to store this in the mapping context.
if ( String . IsNullOrWhiteSpace ( includeProperties ) = = false )
{
opts . Items [ "IncludeProperties" ] = includeProperties . Split ( new [ ] { ", " , "," } , StringSplitOptions . RemoveEmptyEntries ) ;
}
} ) ) ;
2013-08-06 18:42:36 +10:00
return pagedResult ;
}
2013-10-30 22:12:13 +01:00
2015-06-18 09:44:16 +02:00
[Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")]
2013-10-30 22:12:13 +01:00
public bool GetHasPermission ( string permissionToCheck , int nodeId )
2015-06-18 09:44:16 +02:00
{
2016-02-26 14:30:32 +00:00
return HasPermission ( permissionToCheck , nodeId ) ;
2015-06-18 09:44:16 +02:00
}
2016-03-08 15:53:22 +01:00
/// <summary>
/// Returns permissions for all nodes passed in for the current user
2017-09-12 16:22:16 +02:00
/// TODO: This should be moved to the CurrentUserController?
2016-03-08 15:53:22 +01:00
/// </summary>
/// <param name="nodeIds"></param>
/// <returns></returns>
[HttpPost]
public Dictionary < int , string [ ] > GetPermissions ( int [ ] nodeIds )
{
2018-03-27 10:04:07 +02:00
var permissions = Services . UserService
. GetPermissions ( Security . CurrentUser , nodeIds ) ;
2018-04-18 11:14:08 +02:00
2018-03-27 10:04:07 +02:00
var permissionsDictionary = new Dictionary < int , string [ ] > ( ) ;
foreach ( var nodeId in nodeIds )
{
var aggregatePerms = permissions . GetAllPermissions ( nodeId ) . ToArray ( ) ;
permissionsDictionary . Add ( nodeId , aggregatePerms ) ;
}
return permissionsDictionary ;
2016-03-08 15:53:22 +01:00
}
2017-09-12 16:22:16 +02:00
/// <summary>
/// Checks a nodes permission for the current user
/// TODO: This should be moved to the CurrentUserController?
/// </summary>
/// <param name="permissionToCheck"></param>
/// <param name="nodeId"></param>
/// <returns></returns>
2015-06-18 09:44:16 +02:00
[HttpGet]
public bool HasPermission ( string permissionToCheck , int nodeId )
2013-10-30 22:12:13 +01:00
{
2017-09-12 16:22:16 +02:00
var p = Services . UserService . GetPermissions ( Security . CurrentUser , nodeId ) . GetAllPermissions ( ) ;
if ( p . Contains ( permissionToCheck . ToString ( CultureInfo . InvariantCulture ) ) )
2013-10-30 22:12:13 +01:00
{
return true ;
}
return false ;
}
2016-02-26 14:30:32 +00:00
2017-09-12 16:22:16 +02:00
/// <summary>
2017-09-23 10:08:18 +02:00
/// Creates a blueprint from a content item
2017-09-12 16:22:16 +02:00
/// </summary>
/// <param name="contentId">The content id to copy</param>
/// <param name="name">The name of the blueprint</param>
/// <returns></returns>
[HttpPost]
public SimpleNotificationModel CreateBlueprintFromContent ( [ FromUri ] int contentId , [ FromUri ] string name )
{
if ( string . IsNullOrWhiteSpace ( name ) ) throw new ArgumentException ( "Value cannot be null or whitespace." , "name" ) ;
var content = Services . ContentService . GetById ( contentId ) ;
if ( content = = null )
throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ) ) ;
EnsureUniqueName ( name , content , "name" ) ;
2018-03-02 15:48:21 +01:00
var blueprint = Services . ContentService . CreateContentFromBlueprint ( content , name , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2017-09-12 16:22:16 +02:00
2018-03-02 15:48:21 +01:00
Services . ContentService . SaveBlueprint ( blueprint , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2017-09-12 16:22:16 +02:00
var notificationModel = new SimpleNotificationModel ( ) ;
notificationModel . AddSuccessNotification (
Services . TextService . Localize ( "blueprints/createdBlueprintHeading" ) ,
2018-05-08 00:37:41 +10:00
Services . TextService . Localize ( "blueprints/createdBlueprintMessage" , new [ ] { content . Name } )
2017-09-12 16:22:16 +02:00
) ;
return notificationModel ;
}
private void EnsureUniqueName ( string name , IContent content , string modelName )
{
var existing = Services . ContentService . GetBlueprintsForContentTypes ( content . ContentTypeId ) ;
if ( existing . Any ( x = > x . Name = = name & & x . Id ! = content . Id ) )
{
ModelState . AddModelError ( modelName , Services . TextService . Localize ( "blueprints/duplicateBlueprintMessage" ) ) ;
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( ModelState ) ) ;
}
}
/// <summary>
/// Saves content
/// </summary>
/// <returns></returns>
[FileUploadCleanupFilter]
[ContentPostValidate]
2017-11-30 13:56:29 +01:00
public ContentItemDisplay PostSaveBlueprint ( [ ModelBinder ( typeof ( ContentItemBinder ) ) ] ContentItemSave contentItem )
2017-09-12 16:22:16 +02:00
{
var contentItemDisplay = PostSaveInternal ( contentItem ,
content = >
{
EnsureUniqueName ( content . Name , content , "Name" ) ;
Services . ContentService . SaveBlueprint ( contentItem . PersistedContent , Security . CurrentUser . Id ) ;
//we need to reuse the underlying logic so return the result that it wants
2017-11-30 13:56:29 +01:00
return OperationResult . Succeed ( new EventMessages ( ) ) ;
2017-09-12 16:22:16 +02:00
} ) ;
SetupBlueprint ( contentItemDisplay , contentItemDisplay . PersistedContent ) ;
return contentItemDisplay ;
}
2013-08-08 16:31:55 +10:00
/// <summary>
2013-05-27 01:23:49 -10:00
/// Saves content
/// </summary>
/// <returns></returns>
[FileUploadCleanupFilter]
2013-09-19 13:00:56 +02:00
[ContentPostValidate]
2018-03-27 10:04:07 +02:00
[OutgoingEditorModelEvent]
2017-11-30 13:56:29 +01:00
public ContentItemDisplay PostSave ( [ ModelBinder ( typeof ( ContentItemBinder ) ) ] ContentItemSave contentItem )
2017-09-12 16:22:16 +02:00
{
2018-04-09 16:59:45 +10:00
var contentItemDisplay = PostSaveInternal ( contentItem , content = > Services . ContentService . Save ( contentItem . PersistedContent , Security . CurrentUser . Id ) ) ;
2018-05-01 18:17:07 +10:00
//ensure the active culture is still selected
if ( ! contentItem . Culture . IsNullOrWhiteSpace ( ) )
2018-04-04 01:59:51 +10:00
{
foreach ( var contentVariation in contentItemDisplay . Variants )
{
2018-05-01 18:17:07 +10:00
contentVariation . IsCurrent = contentVariation . Language . IsoCode . InvariantEquals ( contentItem . Culture ) ;
2018-04-04 01:59:51 +10:00
}
}
return contentItemDisplay ;
2017-09-12 16:22:16 +02:00
}
2018-04-18 11:14:08 +02:00
2017-11-30 13:56:29 +01:00
private ContentItemDisplay PostSaveInternal ( ContentItemSave contentItem , Func < IContent , OperationResult > saveMethod )
2016-02-26 14:30:32 +00:00
{
2013-05-27 01:23:49 -10:00
//If we've reached here it means:
// * Our model has been bound
// * and validated
// * any file attachments have been saved to their temporary location for us to use
2013-05-27 21:32:37 -10:00
// * we have a reference to the DTO object and the persisted object
2013-08-21 14:13:22 +10:00
// * Permissions are valid
2013-07-23 18:55:31 +10:00
MapPropertyValues ( contentItem ) ;
2013-07-18 12:28:01 +10:00
//We need to manually check the validation results here because:
// * We still need to save the entity even if there are validation value errors
// * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null)
// then we cannot continue saving, we can only display errors
2016-05-26 17:12:04 +02:00
// * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display
2013-07-18 12:28:01 +10:00
// a message indicating this
2013-10-31 18:17:30 +11:00
if ( ModelState . IsValid = = false )
2013-07-18 12:28:01 +10:00
{
2018-03-16 09:06:44 +01:00
if ( ! RequiredForPersistenceAttribute . HasRequiredValuesForPersistence ( contentItem ) & & IsCreatingAction ( contentItem . Action ) )
2013-07-18 12:28:01 +10:00
{
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
2013-09-10 15:37:44 +10:00
// add the modelstate to the outgoing object and throw a validation message
2018-05-01 18:17:07 +10:00
var forDisplay = MapToDisplay ( contentItem . PersistedContent , contentItem . Culture ) ;
2013-07-18 12:28:01 +10:00
forDisplay . Errors = ModelState . ToErrorDictionary ( ) ;
2013-09-10 15:37:44 +10:00
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( forDisplay ) ) ;
2016-02-26 14:30:32 +00:00
2013-07-18 12:28:01 +10:00
}
//if the model state is not valid we cannot publish so change it to save
switch ( contentItem . Action )
{
case ContentSaveAction . Publish :
contentItem . Action = ContentSaveAction . Save ;
break ;
case ContentSaveAction . PublishNew :
contentItem . Action = ContentSaveAction . SaveNew ;
break ;
}
}
2013-07-24 14:59:49 +10:00
//initialize this to successful
2017-11-30 13:56:29 +01:00
var publishStatus = new PublishResult ( null , contentItem . PersistedContent ) ;
2016-02-26 14:30:32 +00:00
var wasCancelled = false ;
2013-07-24 14:59:49 +10:00
2013-06-11 15:13:45 +02:00
if ( contentItem . Action = = ContentSaveAction . Save | | contentItem . Action = = ContentSaveAction . SaveNew )
{
//save the item
2017-09-12 16:22:16 +02:00
var saveResult = saveMethod ( contentItem . PersistedContent ) ;
2015-07-29 15:12:12 +02:00
2017-11-30 13:56:29 +01:00
wasCancelled = saveResult . Success = = false & & saveResult . Result = = OperationResultType . FailedCancelledByEvent ;
2013-06-11 15:13:45 +02:00
}
2013-10-31 18:17:30 +11:00
else if ( contentItem . Action = = ContentSaveAction . SendPublish | | contentItem . Action = = ContentSaveAction . SendPublishNew )
{
2015-07-24 11:44:09 +02:00
var sendResult = Services . ContentService . SendToPublication ( contentItem . PersistedContent , Security . CurrentUser . Id ) ;
wasCancelled = sendResult = = false ;
2013-10-31 18:17:30 +11:00
}
2013-06-11 15:13:45 +02:00
else
2018-05-07 23:22:52 +10:00
{
2018-05-08 01:16:32 +10:00
PublishInternal ( contentItem , ref publishStatus , ref wasCancelled ) ;
2013-06-11 15:13:45 +02:00
}
2013-05-27 01:23:49 -10:00
2018-04-10 01:38:35 +10:00
//get the updated model
2018-05-01 18:17:07 +10:00
var display = MapToDisplay ( contentItem . PersistedContent , contentItem . Culture ) ;
2018-04-18 11:14:08 +02:00
2013-07-16 18:23:20 +10:00
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
2013-07-23 18:55:31 +10:00
HandleInvalidModelState ( display ) ;
2013-07-16 18:23:20 +10:00
2016-05-26 17:12:04 +02:00
//put the correct msgs in
2013-07-22 17:13:38 +10:00
switch ( contentItem . Action )
{
case ContentSaveAction . Save :
case ContentSaveAction . SaveNew :
2015-07-24 11:44:09 +02:00
if ( wasCancelled = = false )
{
display . AddSuccessNotification (
2017-09-12 16:22:16 +02:00
Services . TextService . Localize ( "speechBubbles/editContentSavedHeader" ) ,
Services . TextService . Localize ( "speechBubbles/editContentSavedText" ) ) ;
2015-07-24 11:44:09 +02:00
}
else
{
2015-07-27 18:04:20 +02:00
AddCancelMessage ( display ) ;
2015-07-24 11:44:09 +02:00
}
2013-07-22 17:13:38 +10:00
break ;
2013-10-31 18:17:30 +11:00
case ContentSaveAction . SendPublish :
case ContentSaveAction . SendPublishNew :
2015-07-24 11:44:09 +02:00
if ( wasCancelled = = false )
{
display . AddSuccessNotification (
2017-09-12 16:22:16 +02:00
Services . TextService . Localize ( "speechBubbles/editContentSendToPublish" ) ,
Services . TextService . Localize ( "speechBubbles/editContentSendToPublishText" ) ) ;
2015-07-24 11:44:09 +02:00
}
else
{
2015-07-27 18:04:20 +02:00
AddCancelMessage ( display ) ;
2015-07-24 11:44:09 +02:00
}
2013-10-31 18:17:30 +11:00
break ;
2013-07-22 17:13:38 +10:00
case ContentSaveAction . Publish :
case ContentSaveAction . PublishNew :
2017-11-30 13:56:29 +01:00
ShowMessageForPublishStatus ( publishStatus , display ) ;
2013-07-22 17:13:38 +10:00
break ;
}
2015-08-20 11:50:32 +02:00
//If the item is new and the operation was cancelled, we need to return a different
// status code so the UI can handle it since it won't be able to redirect since there
// is no Id to redirect to!
if ( wasCancelled & & IsCreatingAction ( contentItem . Action ) )
{
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( display ) ) ;
}
2017-09-12 16:22:16 +02:00
display . PersistedContent = contentItem . PersistedContent ;
2013-07-16 18:23:20 +10:00
return display ;
2013-05-27 01:23:49 -10:00
}
2018-05-08 01:16:32 +10:00
/// <summary>
/// Performs the publishing operation for a content item
/// </summary>
/// <param name="contentItem"></param>
/// <param name="publishStatus"></param>
/// <param name="wasCancelled"></param>
/// <remarks>
/// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal
/// </remarks>
private void PublishInternal ( ContentItemSave contentItem , ref PublishResult publishStatus , ref bool wasCancelled )
{
if ( ! contentItem . PersistedContent . ContentType . Variations . Has ( ContentVariation . CultureNeutral ) )
{
//its invariant, proceed normally
contentItem . PersistedContent . TryPublishValues ( ) ;
publishStatus = Services . ContentService . SaveAndPublish ( contentItem . PersistedContent , Security . CurrentUser . Id ) ;
wasCancelled = publishStatus . Result = = PublishResultType . FailedCancelledByEvent ;
}
else
{
var canPublish = true ;
//check if we are publishing other variants and validate them
var allLangs = Services . LocalizationService . GetAllLanguages ( ) . ToDictionary ( x = > x . IsoCode , x = > x , StringComparer . InvariantCultureIgnoreCase ) ;
var variantsToValidate = contentItem . PublishVariations . Where ( x = > ! x . Culture . InvariantEquals ( contentItem . Culture ) ) . ToList ( ) ;
//validate any mandatory variants that are not in the list
var mandatoryLangs = Mapper . Map < IEnumerable < ILanguage > , IEnumerable < Language > > ( allLangs . Values )
. Where ( x = > variantsToValidate . All ( v = > ! v . Culture . InvariantEquals ( x . IsoCode ) ) ) //don't include variants above
. Where ( x = > ! x . IsoCode . InvariantEquals ( contentItem . Culture ) ) //don't include the current variant
. Where ( x = > x . Mandatory ) ;
foreach ( var lang in mandatoryLangs )
{
//cannot continue publishing since a required language that is not currently being published isn't published
if ( ! contentItem . PersistedContent . IsCulturePublished ( lang . IsoCode ) )
{
var errMsg = Services . TextService . Localize ( "speechBubbles/contentReqCulturePublishError" , new [ ] { allLangs [ lang . IsoCode ] . CultureName } ) ;
ModelState . AddModelError ( "publish_variant_" + lang . Id + "_" , errMsg ) ;
canPublish = false ;
}
}
if ( canPublish )
{
//validate all variants to be published
foreach ( var publishVariation in variantsToValidate )
{
var invalid = contentItem . PersistedContent . Validate ( publishVariation . Culture ) . Any ( ) ;
if ( invalid )
{
var errMsg = Services . TextService . Localize ( "speechBubbles/contentCultureValidationError" , new [ ] { allLangs [ publishVariation . Culture ] . CultureName } ) ;
ModelState . AddModelError ( "publish_variant_" + publishVariation . Culture + "_" , errMsg ) ;
canPublish = false ;
}
}
}
if ( canPublish )
{
//set all publish values for the variant we are publishing and those variants flagged for publishing
//we are not checking for a return value here because we've already pre-validated the property values.
contentItem . PersistedContent . TryPublishValues ( contentItem . Culture ) ;
foreach ( var publishVariation in variantsToValidate )
{
contentItem . PersistedContent . TryPublishValues ( publishVariation . Culture ) ;
}
publishStatus = Services . ContentService . SaveAndPublish ( contentItem . PersistedContent , Security . CurrentUser . Id ) ;
wasCancelled = publishStatus . Result = = PublishResultType . FailedCancelledByEvent ;
}
else
{
//can only save
var saveResult = Services . ContentService . Save ( contentItem . PersistedContent , Security . CurrentUser . Id ) ;
publishStatus = new PublishResult ( PublishResultType . Failed , null , contentItem . PersistedContent ) ;
wasCancelled = saveResult . Result = = OperationResultType . FailedCancelledByEvent ;
}
}
}
2016-02-26 14:30:32 +00:00
2013-10-09 15:05:27 +02:00
/// <summary>
/// Publishes a document with a given ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>
/// The CanAccessContentAuthorize attribute will deny access to this method if the current user
/// does not have Publish access to this node.
/// </remarks>
2016-05-26 17:12:04 +02:00
///
2013-10-31 17:22:10 +11:00
[EnsureUserPermissionForContent("id", 'U')]
2013-10-09 15:05:27 +02:00
public HttpResponseMessage PostPublishById ( int id )
{
2013-10-22 11:24:56 +11:00
var foundContent = GetObjectFromRequest ( ( ) = > Services . ContentService . GetById ( id ) ) ;
2013-10-09 15:05:27 +02:00
if ( foundContent = = null )
{
return HandleContentNotFound ( id , false ) ;
}
2018-04-26 21:37:29 +10:00
foundContent . TryPublishValues ( ) ; // fixme variants?
2018-03-02 15:48:21 +01:00
var publishResult = Services . ContentService . SaveAndPublish ( foundContent , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2013-10-15 14:11:28 +11:00
if ( publishResult . Success = = false )
{
2015-07-29 15:12:12 +02:00
var notificationModel = new SimpleNotificationModel ( ) ;
2017-11-30 13:56:29 +01:00
ShowMessageForPublishStatus ( publishResult , notificationModel ) ;
2016-02-26 14:30:32 +00:00
return Request . CreateValidationErrorResponse ( notificationModel ) ;
2013-10-15 14:11:28 +11:00
}
//return ok
2013-10-09 15:05:27 +02:00
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
2017-09-12 16:22:16 +02:00
[HttpDelete]
[HttpPost]
public HttpResponseMessage DeleteBlueprint ( int id )
{
var found = Services . ContentService . GetBlueprintById ( id ) ;
if ( found = = null )
{
return HandleContentNotFound ( id , false ) ;
}
Services . ContentService . DeleteBlueprint ( found ) ;
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
2013-08-01 14:51:35 +10:00
/// <summary>
2013-08-05 16:13:27 +10:00
/// Moves an item to the recycle bin, if it is already there then it will permanently delete it
2013-08-01 14:51:35 +10:00
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2013-08-07 19:28:32 +10:00
/// <remarks>
/// The CanAccessContentAuthorize attribute will deny access to this method if the current user
/// does not have Delete access to this node.
/// </remarks>
[EnsureUserPermissionForContent("id", 'D')]
2013-10-09 15:05:27 +02:00
[HttpDelete]
2013-12-06 13:12:18 +01:00
[HttpPost]
2013-08-01 14:51:35 +10:00
public HttpResponseMessage DeleteById ( int id )
{
2013-10-22 11:24:56 +11:00
var foundContent = GetObjectFromRequest ( ( ) = > Services . ContentService . GetById ( id ) ) ;
2013-08-07 19:28:32 +10:00
2013-08-01 14:51:35 +10:00
if ( foundContent = = null )
{
2013-08-05 16:13:27 +10:00
return HandleContentNotFound ( id , false ) ;
}
//if the current item is in the recycle bin
2016-06-08 13:44:44 +02:00
if ( foundContent . Trashed = = false )
2013-08-05 16:13:27 +10:00
{
2018-03-02 15:48:21 +01:00
var moveResult = Services . ContentService . MoveToRecycleBin ( foundContent , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2017-11-30 13:56:29 +01:00
if ( moveResult . Success = = false )
2015-07-29 15:12:12 +02:00
{
2016-05-26 17:12:04 +02:00
//returning an object of INotificationModel will ensure that any pending
2015-07-29 15:12:12 +02:00
// notification messages are added to the response.
return Request . CreateValidationErrorResponse ( new SimpleNotificationModel ( ) ) ;
}
2013-08-01 14:51:35 +10:00
}
2013-08-05 16:13:27 +10:00
else
{
2018-03-02 15:48:21 +01:00
var deleteResult = Services . ContentService . Delete ( foundContent , Security . GetUserId ( ) . ResultOr ( 0 ) ) ;
2017-11-30 13:56:29 +01:00
if ( deleteResult . Success = = false )
2015-07-29 15:12:12 +02:00
{
2016-05-26 17:12:04 +02:00
//returning an object of INotificationModel will ensure that any pending
2015-07-29 15:12:12 +02:00
// notification messages are added to the response.
return Request . CreateValidationErrorResponse ( new SimpleNotificationModel ( ) ) ;
2016-02-26 14:30:32 +00:00
}
2013-08-05 16:13:27 +10:00
}
2013-08-01 14:51:35 +10:00
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
/// <summary>
/// Empties the recycle bin
/// </summary>
/// <returns></returns>
2013-08-07 19:28:32 +10:00
/// <remarks>
/// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin
/// </remarks>
2013-08-01 14:51:35 +10:00
[HttpDelete]
2013-12-06 13:12:18 +01:00
[HttpPost]
2013-10-09 17:32:13 +11:00
[EnsureUserPermissionForContent(Constants.System.RecycleBinContent)]
2013-08-01 14:51:35 +10:00
public HttpResponseMessage EmptyRecycleBin ( )
2016-02-26 14:30:32 +00:00
{
2013-08-01 14:51:35 +10:00
Services . ContentService . EmptyRecycleBin ( ) ;
2016-06-27 18:27:49 +02:00
return Request . CreateNotificationSuccessResponse ( Services . TextService . Localize ( "defaultdialogs/recycleBinIsEmpty" ) ) ;
2013-08-01 14:51:35 +10:00
}
2013-08-07 15:42:28 +10:00
/// <summary>
2016-09-02 14:50:54 +01:00
/// Change the sort order for content
2013-08-07 15:42:28 +10:00
/// </summary>
/// <param name="sorted"></param>
/// <returns></returns>
2013-08-09 18:13:05 +10:00
[EnsureUserPermissionForContent("sorted.ParentId", 'S')]
2013-08-07 15:42:28 +10:00
public HttpResponseMessage PostSort ( ContentSortOrder sorted )
{
if ( sorted = = null )
{
return Request . CreateResponse ( HttpStatusCode . NotFound ) ;
}
//if there's nothing to sort just return ok
if ( sorted . IdSortOrder . Length = = 0 )
{
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
try
{
2016-09-02 14:50:54 +01:00
var contentService = Services . ContentService ;
2016-09-05 12:20:05 +02:00
2013-08-07 15:42:28 +10:00
// Save content with new sort order and update content xml in db accordingly
2018-03-27 10:04:07 +02:00
if ( contentService . Sort ( sorted . IdSortOrder ) = = false )
2013-08-07 15:42:28 +10:00
{
2016-11-03 10:31:44 +01:00
Logger . Warn < ContentController > ( "Content sorting failed, this was probably caused by an event being cancelled" ) ;
2013-09-10 15:37:44 +10:00
return Request . CreateValidationErrorResponse ( "Content sorting failed, this was probably caused by an event being cancelled" ) ;
2013-08-07 15:42:28 +10:00
}
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
catch ( Exception ex )
{
2016-11-03 10:31:44 +01:00
Logger . Error < ContentController > ( "Could not update content sort order" , ex ) ;
2013-08-07 15:42:28 +10:00
throw ;
}
}
2013-09-30 23:34:07 +02:00
/// <summary>
/// Change the sort order for media
/// </summary>
2013-10-01 10:04:07 +10:00
/// <param name="move"></param>
2013-09-30 23:34:07 +02:00
/// <returns></returns>
[EnsureUserPermissionForContent("move.ParentId", 'M')]
2013-11-14 18:50:31 +11:00
public HttpResponseMessage PostMove ( MoveOrCopy move )
2013-09-30 23:34:07 +02:00
{
2013-10-01 10:04:07 +10:00
var toMove = ValidateMoveOrCopy ( move ) ;
2013-09-30 23:34:07 +02:00
2013-10-01 10:04:07 +10:00
Services . ContentService . Move ( toMove , move . ParentId ) ;
2013-11-14 18:50:31 +11:00
2013-12-04 17:05:05 +11:00
var response = Request . CreateResponse ( HttpStatusCode . OK ) ;
response . Content = new StringContent ( toMove . Path , Encoding . UTF8 , "application/json" ) ;
2016-02-26 14:30:32 +00:00
return response ;
2013-09-30 23:34:07 +02:00
}
/// <summary>
2013-10-03 22:18:44 +02:00
/// Copies a content item and places the copy as a child of a given parent Id
2013-09-30 23:34:07 +02:00
/// </summary>
2013-10-01 10:04:07 +10:00
/// <param name="copy"></param>
2013-09-30 23:34:07 +02:00
/// <returns></returns>
[EnsureUserPermissionForContent("copy.ParentId", 'C')]
2013-11-14 18:50:31 +11:00
public HttpResponseMessage PostCopy ( MoveOrCopy copy )
2013-09-30 23:34:07 +02:00
{
2013-10-01 10:04:07 +10:00
var toCopy = ValidateMoveOrCopy ( copy ) ;
2018-04-27 16:18:25 +01:00
var c = Services . ContentService . Copy ( toCopy , copy . ParentId , copy . RelateToOriginal , copy . Recursive , Security . CurrentUser . Id ) ;
2013-11-14 18:50:31 +11:00
2013-12-04 17:05:05 +11:00
var response = Request . CreateResponse ( HttpStatusCode . OK ) ;
response . Content = new StringContent ( c . Path , Encoding . UTF8 , "application/json" ) ;
2013-11-14 18:50:31 +11:00
return response ;
2013-10-01 10:04:07 +10:00
}
2013-10-03 22:18:44 +02:00
/// <summary>
/// Unpublishes a node with a given Id and returns the unpublished entity
/// </summary>
2018-05-07 23:22:52 +10:00
/// <param name="id">The content id to unpublish</param>
/// <param name="id">The culture variant for the content id to unpublish, if none specified will unpublish all variants of the content</param>
2013-10-03 22:18:44 +02:00
/// <returns></returns>
2013-10-31 16:51:08 +11:00
[EnsureUserPermissionForContent("id", 'U')]
2018-03-27 10:04:07 +02:00
[OutgoingEditorModelEvent]
2018-05-07 23:22:52 +10:00
public ContentItemDisplay PostUnPublish ( int id , string culture = null )
2013-10-03 22:18:44 +02:00
{
2013-10-22 11:24:56 +11:00
var foundContent = GetObjectFromRequest ( ( ) = > Services . ContentService . GetById ( id ) ) ;
2013-10-03 22:18:44 +02:00
if ( foundContent = = null )
HandleContentNotFound ( id ) ;
2018-05-08 00:37:41 +10:00
var unpublishResult = Services . ContentService . Unpublish ( foundContent , culture : culture , userId : Security . CurrentUser . Id ) ;
2013-10-31 16:51:08 +11:00
2018-05-07 23:22:52 +10:00
var content = MapToDisplay ( foundContent , culture ) ;
2013-10-31 16:51:08 +11:00
2018-05-08 00:37:41 +10:00
if ( ! unpublishResult . Success )
2015-07-29 15:12:12 +02:00
{
AddCancelMessage ( content ) ;
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( content ) ) ;
}
else
2018-05-08 00:37:41 +10:00
{
//fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status
content . AddSuccessNotification (
Services . TextService . Localize ( "content/unPublish" ) ,
unpublishResult . Result = = UnpublishResultType . SuccessVariant
? Services . TextService . Localize ( "speechBubbles/contentVariationUnpublished" , new [ ] { culture } )
: Services . TextService . Localize ( "speechBubbles/contentUnpublished" ) ) ;
2015-07-29 15:12:12 +02:00
return content ;
2018-03-27 16:18:51 +02:00
}
2018-05-08 00:37:41 +10:00
}
2015-07-27 18:04:20 +02:00
/// <summary>
/// Maps the dto property values to the persisted model
/// </summary>
/// <param name="contentItem"></param>
private void MapPropertyValues ( ContentItemSave contentItem )
{
2018-04-20 00:59:23 +10:00
//Don't update the name if it is empty
if ( contentItem . Name . IsNullOrWhiteSpace ( ) = = false )
2018-05-02 17:50:58 +02:00
{
2018-04-20 00:59:23 +10:00
//set the name according to the culture settings
2018-05-02 14:52:00 +10:00
if ( contentItem . PersistedContent . ContentType . Variations . HasFlag ( ContentVariation . CultureNeutral ) )
2018-04-20 00:59:23 +10:00
{
2018-05-02 14:52:00 +10:00
if ( contentItem . Culture . IsNullOrWhiteSpace ( ) ) throw new InvalidOperationException ( $"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified" ) ;
2018-05-02 17:50:58 +02:00
contentItem . PersistedContent . SetName ( contentItem . Name , contentItem . Culture ) ;
2018-04-20 00:59:23 +10:00
}
else
{
contentItem . PersistedContent . Name = contentItem . Name ;
}
}
2015-07-27 18:04:20 +02:00
//TODO: We need to support 'send to publish'
contentItem . PersistedContent . ExpireDate = contentItem . ExpireDate ;
contentItem . PersistedContent . ReleaseDate = contentItem . ReleaseDate ;
//only set the template if it didn't change
var templateChanged = ( contentItem . PersistedContent . Template = = null & & contentItem . TemplateAlias . IsNullOrWhiteSpace ( ) = = false )
2016-02-26 14:30:32 +00:00
| | ( contentItem . PersistedContent . Template ! = null & & contentItem . PersistedContent . Template . Alias ! = contentItem . TemplateAlias )
| | ( contentItem . PersistedContent . Template ! = null & & contentItem . TemplateAlias . IsNullOrWhiteSpace ( ) ) ;
2015-07-27 18:04:20 +02:00
if ( templateChanged )
{
var template = Services . FileService . GetTemplate ( contentItem . TemplateAlias ) ;
if ( template = = null & & contentItem . TemplateAlias . IsNullOrWhiteSpace ( ) = = false )
{
//ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias);
2016-09-11 19:57:33 +02:00
Logger . Warn < ContentController > ( "No template exists with the specified alias: " + contentItem . TemplateAlias ) ;
2015-07-27 18:04:20 +02:00
}
else
{
//NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template
contentItem . PersistedContent . Template = template ;
}
}
2018-05-08 17:09:26 +02:00
bool Varies ( Property property ) = > property . PropertyType . Variations . Has ( ContentVariation . CultureNeutral ) ;
MapPropertyValues < IContent , ContentItemSave > (
2018-04-19 23:41:35 +10:00
contentItem ,
2018-05-08 17:09:26 +02:00
( save , property ) = > Varies ( property ) ? property . GetValue ( save . Culture ) : property . GetValue ( ) , //get prop val
( save , property , v ) = > { if ( Varies ( property ) ) property . SetValue ( v , save . Culture ) ; else property . SetValue ( v ) ; } ) ; //set prop val
2015-07-27 18:04:20 +02:00
}
2013-10-01 10:04:07 +10:00
/// <summary>
/// Ensures the item can be moved/copied to the new location
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
private IContent ValidateMoveOrCopy ( MoveOrCopy model )
{
if ( model = = null )
2013-09-30 23:34:07 +02:00
{
2013-10-01 10:04:07 +10:00
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2013-09-30 23:34:07 +02:00
}
var contentService = Services . ContentService ;
2013-10-01 10:04:07 +10:00
var toMove = contentService . GetById ( model . Id ) ;
if ( toMove = = null )
2013-09-30 23:34:07 +02:00
{
2013-10-01 10:04:07 +10:00
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2013-09-30 23:34:07 +02:00
}
2013-10-01 10:04:07 +10:00
if ( model . ParentId < 0 )
2013-09-30 23:34:07 +02:00
{
2013-10-01 10:04:07 +10:00
//cannot move if the content item is not allowed at the root
if ( toMove . ContentType . AllowedAsRoot = = false )
{
2015-11-09 18:34:14 +01:00
throw new HttpResponseException (
2016-02-26 14:30:32 +00:00
Request . CreateNotificationValidationErrorResponse (
Services . TextService . Localize ( "moveOrCopy/notAllowedAtRoot" ) ) ) ;
2013-10-01 10:04:07 +10:00
}
2013-09-30 23:34:07 +02:00
}
2013-10-01 10:04:07 +10:00
else
{
var parent = contentService . GetById ( model . ParentId ) ;
if ( parent = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
//check if the item is allowed under this one
if ( parent . ContentType . AllowedContentTypes . Select ( x = > x . Id ) . ToArray ( )
2016-02-26 14:30:32 +00:00
. Any ( x = > x . Value = = toMove . ContentType . Id ) = = false )
2013-10-01 10:04:07 +10:00
{
2015-11-09 18:34:14 +01:00
throw new HttpResponseException (
2016-02-26 14:30:32 +00:00
Request . CreateNotificationValidationErrorResponse (
Services . TextService . Localize ( "moveOrCopy/notAllowedByContentType" ) ) ) ;
2013-10-01 10:04:07 +10:00
}
2013-09-30 23:34:07 +02:00
2013-10-01 10:04:07 +10:00
// Check on paths
if ( ( string . Format ( ",{0}," , parent . Path ) ) . IndexOf ( string . Format ( ",{0}," , toMove . Id ) , StringComparison . Ordinal ) > - 1 )
2016-02-26 14:30:32 +00:00
{
2015-11-09 18:34:14 +01:00
throw new HttpResponseException (
2016-02-26 14:30:32 +00:00
Request . CreateNotificationValidationErrorResponse (
Services . TextService . Localize ( "moveOrCopy/notAllowedByPath" ) ) ) ;
2013-10-01 10:04:07 +10:00
}
}
return toMove ;
}
2013-09-30 23:34:07 +02:00
2017-11-30 13:56:29 +01:00
private void ShowMessageForPublishStatus ( PublishResult status , INotificationModel display )
2013-07-24 14:59:49 +10:00
{
2017-11-30 13:56:29 +01:00
switch ( status . Result )
2013-07-24 14:59:49 +10:00
{
2017-11-30 13:56:29 +01:00
case PublishResultType . Success :
case PublishResultType . SuccessAlready :
2013-07-24 14:59:49 +10:00
display . AddSuccessNotification (
2016-02-26 14:30:32 +00:00
Services . TextService . Localize ( "speechBubbles/editContentPublishedHeader" ) ,
Services . TextService . Localize ( "speechBubbles/editContentPublishedText" ) ) ;
2013-07-24 14:59:49 +10:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedPathNotPublished :
2013-07-24 14:59:49 +10:00
display . AddWarningNotification (
2016-02-26 14:30:32 +00:00
Services . TextService . Localize ( "publish" ) ,
Services . TextService . Localize ( "publish/contentPublishedFailedByParent" ,
2017-11-30 13:56:29 +01:00
new [ ] { string . Format ( "{0} ({1})" , status . Content . Name , status . Content . Id ) } ) . Trim ( ) ) ;
2013-07-24 14:59:49 +10:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedCancelledByEvent :
2015-07-27 18:04:20 +02:00
AddCancelMessage ( display , "publish" , "speechBubbles/contentPublishedFailedByEvent" ) ;
2016-02-26 14:30:32 +00:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedAwaitingRelease :
2013-11-15 12:46:55 +11:00
display . AddWarningNotification (
2016-02-26 14:30:32 +00:00
Services . TextService . Localize ( "publish" ) ,
Services . TextService . Localize ( "publish/contentPublishedFailedAwaitingRelease" ,
2017-11-30 13:56:29 +01:00
new [ ] { string . Format ( "{0} ({1})" , status . Content . Name , status . Content . Id ) } ) . Trim ( ) ) ;
2013-07-24 14:59:49 +10:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedHasExpired :
2015-10-29 15:23:40 +00:00
display . AddWarningNotification (
2016-02-26 14:30:32 +00:00
Services . TextService . Localize ( "publish" ) ,
Services . TextService . Localize ( "publish/contentPublishedFailedExpired" ,
new [ ]
{
2017-11-30 13:56:29 +01:00
string . Format ( "{0} ({1})" , status . Content . Name , status . Content . Id ) ,
2016-02-26 14:30:32 +00:00
} ) . Trim ( ) ) ;
2015-10-29 15:23:40 +00:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedIsTrashed :
2013-11-15 12:46:55 +11:00
//TODO: We should add proper error messaging for this!
2015-10-29 15:23:40 +00:00
break ;
2017-11-30 13:56:29 +01:00
case PublishResultType . FailedContentInvalid :
2013-07-24 14:59:49 +10:00
display . AddWarningNotification (
2016-02-26 14:30:32 +00:00
Services . TextService . Localize ( "publish" ) ,
Services . TextService . Localize ( "publish/contentPublishedFailedInvalid" ,
new [ ]
{
2017-11-30 13:56:29 +01:00
string . Format ( "{0} ({1})" , status . Content . Name , status . Content . Id ) ,
2016-02-26 14:30:32 +00:00
string . Join ( "," , status . InvalidProperties . Select ( x = > x . Alias ) )
} ) . Trim ( ) ) ;
2013-07-24 14:59:49 +10:00
break ;
default :
throw new IndexOutOfRangeException ( ) ;
}
}
2013-08-09 18:04:44 +10:00
/// <summary>
2016-05-26 17:12:04 +02:00
/// Performs a permissions check for the user to check if it has access to the node based on
2013-08-09 18:04:44 +10:00
/// start node and/or permissions for the node
/// </summary>
/// <param name="storage">The storage to add the content item to so it can be reused</param>
/// <param name="user"></param>
/// <param name="userService"></param>
/// <param name="contentService"></param>
2017-09-12 16:22:16 +02:00
/// <param name="entityService"></param>
2013-08-09 18:04:44 +10:00
/// <param name="nodeId">The content to lookup, if the contentItem is not specified</param>
2013-10-31 16:51:08 +11:00
/// <param name="permissionsToCheck"></param>
2013-09-21 12:29:53 +10:00
/// <param name="contentItem">Specifies the already resolved content item to check against</param>
2013-08-09 18:04:44 +10:00
/// <returns></returns>
internal static bool CheckPermissions (
2016-02-26 14:30:32 +00:00
IDictionary < string , object > storage ,
IUser user ,
IUserService userService ,
IContentService contentService ,
2017-09-12 16:22:16 +02:00
IEntityService entityService ,
2016-02-26 14:30:32 +00:00
int nodeId ,
char [ ] permissionsToCheck = null ,
IContent contentItem = null )
2013-08-09 18:04:44 +10:00
{
2017-09-12 16:22:16 +02:00
if ( storage = = null ) throw new ArgumentNullException ( "storage" ) ;
if ( user = = null ) throw new ArgumentNullException ( "user" ) ;
if ( userService = = null ) throw new ArgumentNullException ( "userService" ) ;
if ( contentService = = null ) throw new ArgumentNullException ( "contentService" ) ;
if ( entityService = = null ) throw new ArgumentNullException ( "entityService" ) ;
2016-02-26 14:30:32 +00:00
2013-10-09 17:32:13 +11:00
if ( contentItem = = null & & nodeId ! = Constants . System . Root & & nodeId ! = Constants . System . RecycleBinContent )
2013-08-09 18:04:44 +10:00
{
contentItem = contentService . GetById ( nodeId ) ;
2016-05-26 17:12:04 +02:00
//put the content item into storage so it can be retreived
2013-09-20 17:27:26 +10:00
// in the controller (saves a lookup)
storage [ typeof ( IContent ) . ToString ( ) ] = contentItem ;
2013-08-09 18:04:44 +10:00
}
2013-10-09 17:32:13 +11:00
if ( contentItem = = null & & nodeId ! = Constants . System . Root & & nodeId ! = Constants . System . RecycleBinContent )
2013-08-09 18:04:44 +10:00
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
2017-09-23 10:08:18 +02:00
2013-09-20 17:27:26 +10:00
var hasPathAccess = ( nodeId = = Constants . System . Root )
2017-09-12 16:22:16 +02:00
? user . HasContentRootAccess ( entityService )
: ( nodeId = = Constants . System . RecycleBinContent )
? user . HasContentBinAccess ( entityService )
: user . HasPathAccess ( contentItem , entityService ) ;
2013-08-09 18:04:44 +10:00
if ( hasPathAccess = = false )
{
return false ;
}
2017-09-12 16:22:16 +02:00
if ( permissionsToCheck = = null | | permissionsToCheck . Length = = 0 )
2013-08-09 18:04:44 +10:00
{
return true ;
}
2013-10-09 17:32:13 +11:00
2017-09-12 16:22:16 +02:00
//get the implicit/inherited permissions for the user for this path,
//if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
var path = contentItem ! = null ? contentItem . Path : nodeId . ToString ( ) ;
var permission = userService . GetPermissionsForPath ( user , path ) ;
2013-10-31 16:51:08 +11:00
var allowed = true ;
foreach ( var p in permissionsToCheck )
2013-08-09 18:04:44 +10:00
{
2017-09-23 10:08:18 +02:00
if ( permission = = null
2017-09-12 16:22:16 +02:00
| | permission . GetAllPermissions ( ) . Contains ( p . ToString ( CultureInfo . InvariantCulture ) ) = = false )
2013-10-31 16:51:08 +11:00
{
allowed = false ;
}
2013-08-09 18:04:44 +10:00
}
2013-10-31 16:51:08 +11:00
return allowed ;
2013-08-09 18:04:44 +10:00
}
2018-04-04 01:59:51 +10:00
/// <summary>
/// Used to map an <see cref="IContent"/> instance to a <see cref="ContentItemDisplay"/> and ensuring a language is present if required
/// </summary>
/// <param name="content"></param>
2018-04-21 09:57:28 +02:00
/// <param name="culture"></param>
2018-04-04 01:59:51 +10:00
/// <returns></returns>
2018-04-21 09:57:28 +02:00
private ContentItemDisplay MapToDisplay ( IContent content , string culture = null )
2018-04-04 01:59:51 +10:00
{
2018-05-02 17:50:58 +02:00
//A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited,
2018-05-02 14:52:00 +10:00
// the Cuture property of ContentItemDisplay must exist (at least currently).
if ( culture = = null & & content . ContentType . Variations . Has ( ContentVariation . CultureNeutral ) )
2018-04-04 01:59:51 +10:00
{
2018-05-02 14:52:00 +10:00
//If a culture is not explicitly sent up, then it means that the user is editing the default variant language.
2018-04-26 16:03:08 +02:00
culture = Services . LocalizationService . GetDefaultLanguageIsoCode ( ) ;
2018-04-04 01:59:51 +10:00
}
2013-08-09 18:04:44 +10:00
2018-04-04 01:59:51 +10:00
var display = ContextMapper . Map < IContent , ContentItemDisplay > ( content , UmbracoContext ,
2018-04-21 09:57:28 +02:00
new Dictionary < string , object > { { ContextMapper . CultureKey , culture } } ) ;
2018-04-04 01:59:51 +10:00
return display ;
2018-05-08 00:37:41 +10:00
}
2013-05-27 01:23:49 -10:00
}
2017-07-20 11:21:28 +02:00
}