2018-06-29 19:52:40 +02:00
using System ;
using System.Collections.Generic ;
2018-10-01 14:32:46 +02:00
using System.IO ;
2018-06-29 19:52:40 +02:00
using System.Linq ;
using System.Net ;
2020-06-12 22:13:43 +02:00
using System.Net.Mime ;
2018-06-29 19:52:40 +02:00
using System.Text ;
2020-06-09 13:48:50 +02:00
using Microsoft.AspNetCore.Http.Extensions ;
using Microsoft.AspNetCore.Mvc ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core ;
2019-12-20 17:36:44 +01:00
using Umbraco.Core.Dictionary ;
using Umbraco.Core.Events ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core.Logging ;
2020-06-09 13:48:50 +02:00
using Umbraco.Core.Mapping ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core.Models ;
2018-09-20 17:22:39 +02:00
using Umbraco.Core.Models.ContentEditing ;
2019-12-20 17:36:44 +01:00
using Umbraco.Core.Models.Entities ;
using Umbraco.Core.Models.Membership ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core.Models.Validation ;
2019-12-20 17:36:44 +01:00
using Umbraco.Core.Persistence ;
using Umbraco.Core.Persistence.Querying ;
2018-07-17 14:23:07 +10:00
using Umbraco.Core.PropertyEditors ;
2019-12-20 17:36:44 +01:00
using Umbraco.Core.Security ;
using Umbraco.Core.Services ;
using Umbraco.Core.Strings ;
2018-10-29 17:27:33 +11:00
using Umbraco.Web.Actions ;
2018-09-20 17:22:39 +02:00
using Umbraco.Web.ContentApps ;
2019-12-20 17:36:44 +01:00
using Umbraco.Web.Models.ContentEditing ;
2019-02-24 09:37:22 +01:00
using Umbraco.Web.Routing ;
2019-11-05 13:45:42 +01:00
using Constants = Umbraco . Core . Constants ;
2020-06-09 13:48:50 +02:00
using Umbraco.Extensions ;
using Umbraco.Web.BackOffice.Controllers ;
using Umbraco.Web.BackOffice.Filters ;
2020-06-12 22:13:43 +02:00
using Umbraco.Web.BackOffice.ModelBinders ;
2020-06-12 11:14:06 +02:00
using Umbraco.Web.Common.ActionResults ;
2020-06-09 13:48:50 +02:00
using Umbraco.Web.Common.Attributes ;
using Umbraco.Web.Common.Exceptions ;
using Umbraco.Web.Models.Mapping ;
using Umbraco.Web.Security ;
using Umbraco.Web.WebApi.Filters ;
2018-08-07 15:48:21 +01:00
2018-06-29 19:52:40 +02:00
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for editing content
/// </summary>
/// <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>
[PluginController("UmbracoApi")]
2019-11-05 13:45:42 +01:00
[UmbracoApplicationAuthorize(Constants.Applications.Content)]
2018-06-29 19:52:40 +02:00
public class ContentController : ContentControllerBase
{
2018-07-17 14:23:07 +10:00
private readonly PropertyEditorCollection _propertyEditors ;
2020-06-09 13:48:50 +02:00
private readonly IContentService _contentService ;
private readonly ILocalizedTextService _localizedTextService ;
private readonly IUserService _userService ;
private readonly IWebSecurity _webSecurity ;
private readonly IEntityService _entityService ;
private readonly IContentTypeService _contentTypeService ;
private readonly UmbracoMapper _umbracoMapper ;
private readonly IPublishedUrlProvider _publishedUrlProvider ;
private readonly IPublicAccessService _publicAccessService ;
private readonly IDomainService _domainService ;
private readonly IDataTypeService _dataTypeService ;
private readonly ILocalizationService _localizationService ;
private readonly IMemberService _memberService ;
private readonly IFileService _fileService ;
private readonly INotificationService _notificationService ;
private readonly ActionCollection _actionCollection ;
private readonly IMemberGroupService _memberGroupService ;
private readonly ISqlContext _sqlContext ;
2018-09-04 01:13:26 +10:00
private readonly Lazy < IDictionary < string , ILanguage > > _allLangs ;
2018-06-29 19:52:40 +02:00
2018-09-30 15:02:09 +01:00
public object Domains { get ; private set ; }
2019-12-18 18:55:00 +01:00
public ContentController (
ICultureDictionary cultureDictionary ,
2020-06-09 13:48:50 +02:00
ILogger logger ,
2020-02-14 13:04:49 +01:00
IShortStringHelper shortStringHelper ,
2020-06-12 11:14:06 +02:00
IEventMessagesFactory eventMessages ,
2020-06-09 13:48:50 +02:00
ILocalizedTextService localizedTextService ,
PropertyEditorCollection propertyEditors ,
IContentService contentService ,
IUserService userService ,
IWebSecurity webSecurity ,
IEntityService entityService ,
IContentTypeService contentTypeService ,
2020-02-14 13:04:49 +01:00
UmbracoMapper umbracoMapper ,
2020-06-09 13:48:50 +02:00
IPublishedUrlProvider publishedUrlProvider ,
IPublicAccessService publicAccessService ,
IDomainService domainService ,
IDataTypeService dataTypeService ,
ILocalizationService localizationService ,
IMemberService memberService ,
IFileService fileService ,
INotificationService notificationService ,
ActionCollection actionCollection ,
IMemberGroupService memberGroupService ,
ISqlContext sqlContext )
: base ( cultureDictionary , logger , shortStringHelper , eventMessages , localizedTextService )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
_propertyEditors = propertyEditors ;
_contentService = contentService ;
_localizedTextService = localizedTextService ;
_userService = userService ;
_webSecurity = webSecurity ;
_entityService = entityService ;
_contentTypeService = contentTypeService ;
_umbracoMapper = umbracoMapper ;
_publishedUrlProvider = publishedUrlProvider ;
_publicAccessService = publicAccessService ;
_domainService = domainService ;
_dataTypeService = dataTypeService ;
_localizationService = localizationService ;
_memberService = memberService ;
_fileService = fileService ;
_notificationService = notificationService ;
_actionCollection = actionCollection ;
_memberGroupService = memberGroupService ;
_sqlContext = sqlContext ;
2020-06-12 11:14:06 +02:00
_allLangs = new Lazy < IDictionary < string , ILanguage > > ( ( ) = > _localizationService . GetAllLanguages ( ) . ToDictionary ( x = > x . IsoCode , x = > x , StringComparer . InvariantCultureIgnoreCase ) ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Returns true if any content types have culture variation enabled
/// </summary>
2018-05-10 19:16:46 +10:00
/// <returns></returns>
[HttpGet]
2020-06-09 13:48:50 +02:00
[UmbracoAuthorize]
2018-06-29 19:52:40 +02:00
public bool AllowsCultureVariation ( )
{
2020-06-09 13:48:50 +02:00
var contentTypes = _contentTypeService . GetAll ( ) ;
2018-06-20 14:18:57 +02:00
return contentTypes . Any ( contentType = > contentType . VariesByCulture ( ) ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Return content for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemDisplay>))]
2020-06-09 13:48:50 +02:00
public IEnumerable < ContentItemDisplay > GetByIds ( [ FromQuery ] int [ ] ids )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = _contentService . GetByIds ( ids ) ;
2019-01-21 15:57:48 +01:00
return foundContent . Select ( MapToDisplay ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Updates the permissions for a content item for a particular user group
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
/// <remarks>
2019-01-26 10:52:19 -05:00
/// Permission check is done for letter 'R' which is for <see cref="ActionRights"/> which the user must have access to update
2018-06-29 19:52:40 +02:00
/// </remarks>
[EnsureUserPermissionForContent("saveModel.ContentId", 'R')]
2020-06-09 13:48:50 +02:00
public ActionResult < IEnumerable < AssignedUserGroupPermissions > > PostSaveUserGroupPermissions ( UserGroupPermissionsSave saveModel )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
if ( saveModel . ContentId < = 0 ) return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
2019-01-26 10:52:19 -05:00
// TODO: Should non-admins be allowed to set granular permissions?
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( saveModel . ContentId ) ;
if ( content = = null ) return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
//current permissions explicitly assigned to this content item
2020-06-09 13:48:50 +02:00
var contentPermissions = _contentService . GetPermissions ( content )
2018-06-29 19:52:40 +02:00
. ToDictionary ( x = > x . UserGroupId , x = > x ) ;
2020-06-09 13:48:50 +02:00
var allUserGroups = _userService . GetAllUserGroups ( ) . ToArray ( ) ;
2018-06-29 19:52:40 +02:00
//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 )
{
2020-06-09 13:48:50 +02:00
_userService . RemoveUserGroupPermissions ( userGroup . Id , content . Id ) ;
2018-06-29 19:52:40 +02:00
}
//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
2020-06-09 13:48:50 +02:00
_userService . RemoveUserGroupPermissions ( userGroup . Id , content . Id ) ;
2018-06-29 19:52:40 +02:00
}
}
//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 )
{
2020-06-09 13:48:50 +02:00
_userService . ReplaceUserGroupPermissions ( userGroup . Id , groupPermissionCodes . Select ( x = > x [ 0 ] ) , content . Id ) ;
2018-06-29 19:52:40 +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>
2019-01-26 10:52:19 -05:00
/// Permission check is done for letter 'R' which is for <see cref="ActionRights"/> which the user must have access to view
2018-06-29 19:52:40 +02:00
/// </remarks>
[EnsureUserPermissionForContent("contentId", 'R')]
2020-06-09 13:48:50 +02:00
public ActionResult < IEnumerable < AssignedUserGroupPermissions > > GetDetailedPermissions ( int contentId )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
if ( contentId < = 0 ) return NotFound ( ) ;
var content = _contentService . GetById ( contentId ) ;
if ( content = = null ) return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
2019-01-27 01:17:32 -05:00
// TODO: Should non-admins be able to see detailed permissions?
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
var allUserGroups = _userService . GetAllUserGroups ( ) ;
2018-06-29 19:52:40 +02:00
return GetDetailedPermissions ( content , allUserGroups ) ;
2018-05-10 19:16:46 +10:00
}
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
private ActionResult < IEnumerable < AssignedUserGroupPermissions > > GetDetailedPermissions ( IContent content , IEnumerable < IUserGroup > allUserGroups )
2018-06-29 19:52:40 +02:00
{
//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.
2020-06-09 13:48:50 +02:00
var defaultPermissionsByGroup = _umbracoMapper . MapEnumerable < IUserGroup , AssignedUserGroupPermissions > ( allUserGroups ) ;
2018-06-29 19:52:40 +02:00
var defaultPermissionsAsDictionary = defaultPermissionsByGroup
. ToDictionary ( x = > Convert . ToInt32 ( x . Id ) , x = > x ) ;
//get the actual assigned permissions
2020-06-09 13:48:50 +02:00
var assignedPermissionsByGroup = _contentService . GetPermissions ( content ) . ToArray ( ) ;
2018-06-29 19:52:40 +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 ) ;
}
}
return defaultPermissionsByGroup ;
}
/// <summary>
/// Returns an item to be used to display the recycle bin for content
/// </summary>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
public ActionResult < ContentItemDisplay > GetRecycleBin ( )
2018-06-29 19:52:40 +02:00
{
2018-07-17 14:23:07 +10:00
var apps = new List < ContentApp > ( ) ;
2020-06-09 13:48:50 +02:00
apps . Add ( ListViewContentAppFactory . CreateContentApp ( _dataTypeService , _propertyEditors , "recycleBin" , "content" , Core . Constants . DataTypes . DefaultMembersListView ) ) ;
2018-07-17 14:23:07 +10:00
apps [ 0 ] . Active = true ;
2018-06-29 19:52:40 +02:00
var display = new ContentItemDisplay
{
2019-11-05 13:45:42 +01:00
Id = Constants . System . RecycleBinContent ,
2018-06-29 19:52:40 +02:00
ParentId = - 1 ,
ContentTypeAlias = "recycleBin" ,
IsContainer = true ,
2019-11-05 13:45:42 +01:00
Path = "-1," + Constants . System . RecycleBinContent ,
2018-07-19 19:32:07 +10:00
Variants = new List < ContentVariantDisplay >
2018-07-13 12:45:04 +10:00
{
new ContentVariantDisplay
{
CreateDate = DateTime . Now ,
2020-06-09 13:48:50 +02:00
Name = _localizedTextService . Localize ( "general/recycleBin" )
2018-07-13 12:45:04 +10:00
}
2018-07-17 14:23:07 +10:00
} ,
ContentApps = apps
2018-06-29 19:52:40 +02:00
} ;
return display ;
}
2020-06-09 13:48:50 +02:00
public ActionResult < ContentItemDisplay > GetBlueprintById ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = _contentService . GetBlueprintById ( id ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
{
2020-06-09 13:48:50 +02:00
return HandleContentNotFound ( id ) ;
2018-06-29 19:52:40 +02:00
}
var content = MapToDisplay ( foundContent ) ;
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 ) ;
content . AllowedActions = new [ ] { "A" } ;
content . IsBlueprint = true ;
2019-01-27 01:17:32 -05:00
// TODO: exclude the content apps here
2018-07-13 12:45:04 +10:00
//var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" };
//var propsTab = content.Tabs.Last();
//propsTab.Properties = propsTab.Properties
// .Where(p => excludeProps.Contains(p.Alias) == false);
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Gets the content json for the content id
/// </summary>
/// <param name="id"></param>
/// <param name="culture"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2018-06-29 19:52:40 +02:00
[EnsureUserPermissionForContent("id")]
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
2018-07-13 12:45:04 +10:00
public ContentItemDisplay GetById ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = GetObjectFromRequest ( ( ) = > _contentService . GetById ( id ) ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
{
HandleContentNotFound ( id ) ;
return null ; //irrelevant since the above throws
2018-07-18 13:19:48 +10:00
}
2018-07-19 19:32:07 +10:00
var content = MapToDisplay ( foundContent ) ;
2018-07-18 13:19:48 +10:00
return content ;
}
2018-07-18 13:19:14 +10:00
/// <summary>
2020-01-08 19:02:16 +01:00
/// Gets the content json for the content guid
2018-07-18 13:19:14 +10:00
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2018-07-18 13:19:14 +10:00
[EnsureUserPermissionForContent("id")]
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
2018-07-19 19:32:07 +10:00
public ContentItemDisplay GetById ( Guid id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = GetObjectFromRequest ( ( ) = > _contentService . GetById ( id ) ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
{
HandleContentNotFound ( id ) ;
return null ; //irrelevant since the above throws
}
2018-07-13 12:45:04 +10:00
var content = MapToDisplay ( foundContent ) ;
2018-06-29 19:52:40 +02:00
return content ;
}
2018-07-18 13:19:14 +10:00
/// <summary>
2020-01-08 19:02:16 +01:00
/// Gets the content json for the content udi
2018-07-18 13:19:14 +10:00
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2018-07-18 13:19:14 +10:00
[EnsureUserPermissionForContent("id")]
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
2018-07-19 19:32:07 +10:00
public ContentItemDisplay GetById ( Udi id )
2018-07-18 13:19:14 +10:00
{
var guidUdi = id as GuidUdi ;
if ( guidUdi ! = null )
{
2018-07-19 19:32:07 +10:00
return GetById ( guidUdi . Guid ) ;
2018-07-18 13:19:14 +10:00
}
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2018-11-12 17:29:38 +11:00
}
2018-06-29 19:52:40 +02:00
/// <summary>
2020-01-08 19:02:16 +01:00
/// Gets an empty content item for the document type.
2018-06-29 19:52:40 +02:00
/// </summary>
/// <param name="contentTypeAlias"></param>
2018-12-07 13:44:41 +01:00
/// <param name="parentId"></param>
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2020-06-12 22:13:43 +02:00
[DetermineAmbiguousActionByPassingParameters]
2018-06-29 19:52:40 +02:00
public ContentItemDisplay GetEmpty ( string contentTypeAlias , int parentId )
{
2020-06-09 13:48:50 +02:00
var contentType = _contentTypeService . Get ( contentTypeAlias ) ;
2018-06-29 19:52:40 +02:00
if ( contentType = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
2020-06-09 13:48:50 +02:00
var emptyContent = _contentService . Create ( "" , parentId , contentType . Alias , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
var mapped = MapToDisplay ( emptyContent ) ;
2018-12-20 16:58:01 +11:00
// translate the content type name if applicable
2020-06-09 13:48:50 +02:00
mapped . ContentTypeName = _localizedTextService . UmbracoDictionaryTranslate ( CultureDictionary , mapped . ContentTypeName ) ;
2019-01-11 10:23:27 +01:00
// if your user type doesn't have access to the Settings section it would not get this property mapped
2019-03-12 16:10:35 +11:00
if ( mapped . DocumentType ! = null )
2020-06-09 13:48:50 +02:00
mapped . DocumentType . Name = _localizedTextService . UmbracoDictionaryTranslate ( CultureDictionary , mapped . DocumentType . Name ) ;
2018-06-29 19:52:40 +02:00
2018-07-13 12:45:04 +10:00
//remove the listview app if it exists
2018-09-14 10:53:59 +01:00
mapped . ContentApps = mapped . ContentApps . Where ( x = > x . Alias ! = "umbListView" ) . ToList ( ) ;
2018-11-12 17:29:38 +11:00
2018-06-29 19:52:40 +02:00
return mapped ;
}
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2020-06-12 22:13:43 +02:00
[DetermineAmbiguousActionByPassingParameters]
2018-06-29 19:52:40 +02:00
public ContentItemDisplay GetEmpty ( int blueprintId , int parentId )
{
2020-06-09 13:48:50 +02:00
var blueprint = _contentService . GetBlueprintById ( blueprintId ) ;
2018-06-29 19:52:40 +02:00
if ( blueprint = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
blueprint . Id = 0 ;
blueprint . Name = string . Empty ;
blueprint . ParentId = parentId ;
2020-06-09 13:48:50 +02:00
var mapped = _umbracoMapper . Map < ContentItemDisplay > ( blueprint ) ;
2018-06-29 19:52:40 +02:00
2018-07-13 12:45:04 +10:00
//remove the listview app if it exists
2018-09-14 10:53:59 +01:00
mapped . ContentApps = mapped . ContentApps . Where ( x = > x . Alias ! = "umbListView" ) . ToList ( ) ;
2018-07-13 12:45:04 +10:00
2018-06-29 19:52:40 +02:00
return mapped ;
}
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
public IActionResult GetNiceUrl ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var url = _publishedUrlProvider . GetUrl ( id ) ;
2020-06-12 22:13:43 +02:00
return Content ( url , MediaTypeNames . Text . Plain , Encoding . UTF8 ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
public IActionResult GetNiceUrl ( Guid id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var url = _publishedUrlProvider . GetUrl ( id ) ;
2020-06-12 22:13:43 +02:00
return Content ( url , MediaTypeNames . Text . Plain , Encoding . UTF8 ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Gets the Url for a given node ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2020-06-09 13:48:50 +02:00
[DetermineAmbiguousActionByPassingParameters]
public IActionResult GetNiceUrl ( Udi id )
2018-06-29 19:52:40 +02:00
{
var guidUdi = id as GuidUdi ;
if ( guidUdi ! = null )
{
return GetNiceUrl ( guidUdi . Guid ) ;
2020-06-09 13:48:50 +02:00
2018-06-29 19:52:40 +02:00
}
2020-06-09 13:48:50 +02:00
return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Gets the children for the content id passed in
/// </summary>
/// <returns></returns>
2018-08-01 15:40:54 +10:00
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>), "Items")]
public PagedResult < ContentItemBasic < ContentPropertyBasic > > GetChildren (
2018-06-29 19:52:40 +02:00
int id ,
2019-01-27 01:17:32 -05:00
int pageNumber = 0 , // TODO: This should be '1' as it's not the index
2018-06-29 19:52:40 +02:00
int pageSize = 0 ,
string orderBy = "SortOrder" ,
Direction orderDirection = Direction . Ascending ,
bool orderBySystemField = true ,
string filter = "" )
{
return GetChildren ( id , null , pageNumber , pageSize , orderBy , orderDirection , orderBySystemField , filter ) ;
}
/// <summary>
/// Gets the children for the content id passed in
/// </summary>
/// <returns></returns>
2018-08-01 15:40:54 +10:00
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>), "Items")]
public PagedResult < ContentItemBasic < ContentPropertyBasic > > GetChildren (
2018-06-29 19:52:40 +02:00
int id ,
string includeProperties ,
2018-11-12 17:29:38 +11:00
int pageNumber = 0 ,
2018-06-29 19:52:40 +02:00
int pageSize = 0 ,
string orderBy = "SortOrder" ,
Direction orderDirection = Direction . Ascending ,
bool orderBySystemField = true ,
2018-09-07 15:02:44 +01:00
string filter = "" ,
2019-01-26 10:52:19 -05:00
string cultureName = "" ) // TODO: it's not a NAME it's the ISO CODE
2018-06-29 19:52:40 +02:00
{
long totalChildren ;
2018-09-18 11:53:33 +02:00
List < IContent > children ;
2019-02-12 11:58:38 +01:00
// Sets the culture to the only existing culture if we only have one culture.
if ( string . IsNullOrWhiteSpace ( cultureName ) )
{
if ( _allLangs . Value . Count = = 1 )
{
cultureName = _allLangs . Value . First ( ) . Key ;
}
}
2018-06-29 19:52:40 +02:00
if ( pageNumber > 0 & & pageSize > 0 )
{
IQuery < IContent > queryFilter = null ;
if ( filter . IsNullOrWhiteSpace ( ) = = false )
{
//add the default text filter
2020-06-09 13:48:50 +02:00
queryFilter = _sqlContext . Query < IContent > ( )
2018-06-29 19:52:40 +02:00
. Where ( x = > x . Name . Contains ( filter ) ) ;
}
2020-06-09 13:48:50 +02:00
children = _contentService
2018-09-18 11:53:33 +02:00
. GetPagedChildren ( id , pageNumber - 1 , pageSize , out totalChildren ,
queryFilter ,
Ordering . By ( orderBy , orderDirection , cultureName , ! orderBySystemField ) ) . ToList ( ) ;
2018-06-29 19:52:40 +02:00
}
else
{
2018-10-31 18:01:39 +11:00
//better to not use this without paging where possible, currently only the sort dialog does
2020-06-09 13:48:50 +02:00
children = _contentService . GetPagedChildren ( id , 0 , int . MaxValue , out var total ) . ToList ( ) ;
2018-09-18 11:53:33 +02:00
totalChildren = children . Count ;
2018-06-29 19:52:40 +02:00
}
if ( totalChildren = = 0 )
{
2018-08-01 15:40:54 +10:00
return new PagedResult < ContentItemBasic < ContentPropertyBasic > > ( 0 , 0 , 0 ) ;
2018-06-29 19:52:40 +02:00
}
2018-08-01 15:40:54 +10:00
var pagedResult = new PagedResult < ContentItemBasic < ContentPropertyBasic > > ( totalChildren , pageNumber , pageSize ) ;
2018-06-29 19:52:40 +02:00
pagedResult . Items = children . Select ( content = >
2020-06-09 13:48:50 +02:00
_umbracoMapper . Map < IContent , ContentItemBasic < ContentPropertyBasic > > ( content ,
2019-03-26 10:39:50 +01:00
context = >
2018-06-29 19:52:40 +02:00
{
2018-09-07 15:02:44 +01:00
2019-03-26 10:39:50 +01:00
context . SetCulture ( cultureName ) ;
2018-06-29 19:52:40 +02:00
2018-09-17 13:19:24 +02:00
// if there's a list of property aliases to map - we will make sure to store this in the mapping context.
if ( ! includeProperties . IsNullOrWhiteSpace ( ) )
2019-03-26 10:39:50 +01:00
context . SetIncludedProperties ( includeProperties . Split ( new [ ] { ", " , "," } , StringSplitOptions . RemoveEmptyEntries ) ) ;
2018-09-17 13:19:24 +02:00
} ) )
. ToList ( ) ; // evaluate now
2018-09-07 15:02:44 +01:00
2018-06-29 19:52:40 +02:00
return pagedResult ;
}
/// <summary>
/// Creates a blueprint from a content item
/// </summary>
/// <param name="contentId">The content id to copy</param>
/// <param name="name">The name of the blueprint</param>
/// <returns></returns>
[HttpPost]
2020-06-09 13:48:50 +02:00
public ActionResult < SimpleNotificationModel > CreateBlueprintFromContent ( [ FromQuery ] int contentId , [ FromQuery ] string name )
2018-06-29 19:52:40 +02:00
{
2019-01-08 22:30:12 +01:00
if ( string . IsNullOrWhiteSpace ( name ) )
2019-01-10 07:03:08 +01:00
throw new ArgumentException ( "Value cannot be null or whitespace." , nameof ( name ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( contentId ) ;
2018-06-29 19:52:40 +02:00
if ( content = = null )
2020-06-09 13:48:50 +02:00
return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
2019-01-10 07:03:08 +01:00
EnsureUniqueName ( name , content , nameof ( name ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
var blueprint = _contentService . CreateContentFromBlueprint ( content , name , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
_contentService . SaveBlueprint ( blueprint , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
var notificationModel = new SimpleNotificationModel ( ) ;
notificationModel . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "blueprints/createdBlueprintHeading" ) ,
_localizedTextService . Localize ( "blueprints/createdBlueprintMessage" , new [ ] { content . Name } )
2018-06-29 19:52:40 +02:00
) ;
return notificationModel ;
}
private void EnsureUniqueName ( string name , IContent content , string modelName )
{
2020-06-09 13:48:50 +02:00
var existing = _contentService . GetBlueprintsForContentTypes ( content . ContentTypeId ) ;
2018-06-29 19:52:40 +02:00
if ( existing . Any ( x = > x . Name = = name & & x . Id ! = content . Id ) )
{
2020-06-09 13:48:50 +02:00
ModelState . AddModelError ( modelName , _localizedTextService . Localize ( "blueprints/duplicateBlueprintMessage" ) ) ;
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
2018-06-29 19:52:40 +02:00
}
}
2020-06-12 11:14:06 +02:00
/// <summary>
/// Saves content
/// </summary>
/// <returns></returns>
[FileUploadCleanupFilter]
[ContentSaveValidation]
public ContentItemDisplay PostSaveBlueprint ( [ ModelBinder ( typeof ( BlueprintItemBinder ) ) ] ContentItemSave contentItem )
{
var contentItemDisplay = PostSaveInternal ( contentItem ,
content = >
{
EnsureUniqueName ( content . Name , content , "Name" ) ;
_contentService . SaveBlueprint ( contentItem . PersistedContent , _webSecurity . CurrentUser . Id ) ;
//we need to reuse the underlying logic so return the result that it wants
return OperationResult . Succeed ( new EventMessages ( ) ) ;
} ,
content = >
{
var display = MapToDisplay ( content ) ;
SetupBlueprint ( display , content ) ;
return display ;
} ) ;
return contentItemDisplay ;
}
2018-06-29 19:52:40 +02:00
/// <summary>
/// Saves content
/// </summary>
/// <returns></returns>
[FileUploadCleanupFilter]
2018-08-01 16:46:13 +10:00
[ContentSaveValidation]
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2018-06-29 19:52:40 +02:00
public ContentItemDisplay PostSave ( [ ModelBinder ( typeof ( ContentItemBinder ) ) ] ContentItemSave contentItem )
{
2019-06-12 12:22:14 +10:00
var contentItemDisplay = PostSaveInternal (
contentItem ,
2020-06-09 13:48:50 +02:00
content = > _contentService . Save ( contentItem . PersistedContent , _webSecurity . CurrentUser . Id ) ,
2019-06-12 12:22:14 +10:00
MapToDisplay ) ;
2018-08-02 15:12:26 +10:00
return contentItemDisplay ;
2018-06-29 19:52:40 +02:00
}
2019-06-12 12:22:14 +10:00
private ContentItemDisplay PostSaveInternal ( ContentItemSave contentItem , Func < IContent , OperationResult > saveMethod , Func < IContent , ContentItemDisplay > mapToDisplay )
2018-06-29 19:52:40 +02:00
{
2019-01-26 10:52:19 -05:00
//Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
2018-10-01 14:32:46 +02:00
//To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
//uploaded files to being *only* the actual file name (as it should be).
if ( contentItem . UploadedFiles ! = null & & contentItem . UploadedFiles . Any ( ) )
{
foreach ( var file in contentItem . UploadedFiles )
{
file . FileName = Path . GetFileName ( file . FileName ) ;
}
}
2018-07-19 19:32:07 +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
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
2018-08-14 20:21:33 +10:00
MapValuesForPersistence ( contentItem ) ;
2019-03-12 16:10:35 +11:00
var passesCriticalValidationRules = ValidateCriticalData ( contentItem , out var variantCount ) ;
2018-07-19 19:32:07 +10:00
2019-03-13 18:12:48 +11:00
//we will continue to save if model state is invalid, however we cannot save if critical data is missing.
if ( ! ModelState . IsValid )
2018-07-19 19:32:07 +10:00
{
2019-03-12 16:10:35 +11:00
//check for critical data validation issues, we can't continue saving if this data is invalid
if ( ! passesCriticalValidationRules )
2018-09-06 20:33:32 +10:00
{
2019-03-12 16:10:35 +11:00
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// add the model state to the outgoing object and throw a validation message
2019-06-12 12:22:14 +10:00
var forDisplay = mapToDisplay ( contentItem . PersistedContent ) ;
2019-03-12 16:10:35 +11:00
forDisplay . Errors = ModelState . ToErrorDictionary ( ) ;
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( forDisplay ) ;
2018-07-19 19:32:07 +10:00
}
2018-07-13 12:45:04 +10:00
2018-09-03 22:46:24 +10:00
//if there's only one variant and the model state is not valid we cannot publish so change it to save
if ( variantCount = = 1 )
2018-07-19 19:32:07 +10:00
{
2018-09-03 22:46:24 +10:00
switch ( contentItem . Action )
{
case ContentSaveAction . Publish :
2018-11-15 00:09:57 +11:00
case ContentSaveAction . PublishWithDescendants :
case ContentSaveAction . PublishWithDescendantsForce :
2018-11-09 16:12:08 +11:00
case ContentSaveAction . SendPublish :
case ContentSaveAction . Schedule :
2018-09-03 22:46:24 +10:00
contentItem . Action = ContentSaveAction . Save ;
break ;
case ContentSaveAction . PublishNew :
2018-11-15 00:09:57 +11:00
case ContentSaveAction . PublishWithDescendantsNew :
case ContentSaveAction . PublishWithDescendantsForceNew :
2018-11-09 16:12:08 +11:00
case ContentSaveAction . SendPublishNew :
case ContentSaveAction . ScheduleNew :
2018-09-03 22:46:24 +10:00
contentItem . Action = ContentSaveAction . SaveNew ;
break ;
}
2018-07-19 19:32:07 +10:00
}
}
2018-07-13 12:45:04 +10:00
2018-09-04 01:13:26 +10:00
bool wasCancelled ;
2018-07-13 12:45:04 +10:00
2018-09-04 01:13:26 +10:00
//used to track successful notifications
2018-09-04 16:16:11 +10:00
var globalNotifications = new SimpleNotificationModel ( ) ;
var notifications = new Dictionary < string , SimpleNotificationModel >
{
//global (non variant specific) notifications
[string.Empty] = globalNotifications
} ;
2018-07-13 12:45:04 +10:00
2019-04-04 21:57:49 +11:00
//The default validation language will be either: The default languauge, else if the content is brand new and the default culture is
// not marked to be saved, it will be the first culture in the list marked for saving.
2019-04-05 00:00:22 +11:00
var defaultCulture = _allLangs . Value . Values . FirstOrDefault ( x = > x . IsDefault ) ? . CultureName ;
2019-04-04 21:57:49 +11:00
var cultureForInvariantErrors = CultureImpact . GetCultureForInvariantErrors (
contentItem . PersistedContent ,
contentItem . Variants . Where ( x = > x . Save ) . Select ( x = > x . Culture ) . ToArray ( ) ,
defaultCulture ) ;
2018-07-19 19:32:07 +10:00
switch ( contentItem . Action )
{
case ContentSaveAction . Save :
case ContentSaveAction . SaveNew :
2019-04-04 21:57:49 +11:00
SaveAndNotify ( contentItem , saveMethod , variantCount , notifications , globalNotifications , "editContentSavedText" , "editVariantSavedText" , cultureForInvariantErrors , out wasCancelled ) ;
2018-07-19 19:32:07 +10:00
break ;
2018-11-09 16:12:08 +11:00
case ContentSaveAction . Schedule :
case ContentSaveAction . ScheduleNew :
2018-11-13 14:31:37 +11:00
if ( ! SaveSchedule ( contentItem , globalNotifications ) )
2018-11-09 16:12:08 +11:00
{
wasCancelled = false ;
break ;
}
2019-04-04 21:57:49 +11:00
SaveAndNotify ( contentItem , saveMethod , variantCount , notifications , globalNotifications , "editContentScheduledSavedText" , "editVariantSavedText" , cultureForInvariantErrors , out wasCancelled ) ;
2018-11-13 14:31:37 +11:00
break ;
2018-11-12 17:29:38 +11:00
2018-07-19 19:32:07 +10:00
case ContentSaveAction . SendPublish :
case ContentSaveAction . SendPublishNew :
2020-06-09 13:48:50 +02:00
var sendResult = _contentService . SendToPublication ( contentItem . PersistedContent , _webSecurity . CurrentUser . Id ) ;
2018-09-04 01:13:26 +10:00
wasCancelled = sendResult = = false ;
if ( sendResult )
2018-07-19 19:32:07 +10:00
{
2018-09-04 01:13:26 +10:00
if ( variantCount > 1 )
{
2020-05-14 17:03:33 +02:00
var variantErrors = ModelState . GetVariantsWithErrors ( cultureForInvariantErrors ) ;
var validVariants = contentItem . Variants
. Where ( x = > x . Save & & ! variantErrors . Contains ( ( x . Culture , x . Segment ) ) )
. Select ( x = > ( culture : x . Culture , segment : x . Segment ) ) ;
foreach ( var ( culture , segment ) in validVariants )
2018-09-04 01:13:26 +10:00
{
2020-05-14 17:03:33 +02:00
var variantName = GetVariantName ( culture , segment ) ;
AddSuccessNotification ( notifications , culture , segment ,
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentSendToPublish" ) ,
_localizedTextService . Localize ( "speechBubbles/editVariantSendToPublishText" , new [ ] { variantName } ) ) ;
2018-09-04 01:13:26 +10:00
}
}
else if ( ModelState . IsValid )
{
2018-09-04 16:16:11 +10:00
globalNotifications . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentSendToPublish" ) ,
_localizedTextService . Localize ( "speechBubbles/editContentSendToPublishText" ) ) ;
2018-09-04 01:13:26 +10:00
}
2018-07-19 19:32:07 +10:00
}
break ;
case ContentSaveAction . Publish :
case ContentSaveAction . PublishNew :
2018-09-04 16:16:11 +10:00
{
2019-04-04 21:57:49 +11:00
var publishStatus = PublishInternal ( contentItem , defaultCulture , cultureForInvariantErrors , out wasCancelled , out var successfulCultures ) ;
2019-06-12 10:11:27 +10:00
AddPublishStatusNotifications ( new [ ] { publishStatus } , globalNotifications , notifications , successfulCultures ) ;
2018-11-15 00:09:57 +11:00
}
break ;
case ContentSaveAction . PublishWithDescendants :
case ContentSaveAction . PublishWithDescendantsNew :
{
2018-11-15 15:24:09 +11:00
if ( ! ValidatePublishBranchPermissions ( contentItem , out var noAccess ) )
{
globalNotifications . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/invalidPublishBranchPermissions" ) ) ;
2018-11-15 15:24:09 +11:00
wasCancelled = false ;
break ;
}
2019-04-04 21:57:49 +11:00
var publishStatus = PublishBranchInternal ( contentItem , false , cultureForInvariantErrors , out wasCancelled , out var successfulCultures ) . ToList ( ) ;
2019-06-12 10:11:27 +10:00
AddPublishStatusNotifications ( publishStatus , globalNotifications , notifications , successfulCultures ) ;
2018-11-15 00:09:57 +11:00
}
break ;
case ContentSaveAction . PublishWithDescendantsForce :
case ContentSaveAction . PublishWithDescendantsForceNew :
{
2018-11-15 15:24:09 +11:00
if ( ! ValidatePublishBranchPermissions ( contentItem , out var noAccess ) )
{
globalNotifications . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/invalidPublishBranchPermissions" ) ) ;
2018-11-15 15:24:09 +11:00
wasCancelled = false ;
break ;
}
2019-04-04 21:57:49 +11:00
var publishStatus = PublishBranchInternal ( contentItem , true , cultureForInvariantErrors , out wasCancelled , out var successfulCultures ) . ToList ( ) ;
2019-06-12 10:11:27 +10:00
AddPublishStatusNotifications ( publishStatus , globalNotifications , notifications , successfulCultures ) ;
2018-09-04 16:16:11 +10:00
}
2018-07-19 19:32:07 +10:00
break ;
2018-09-04 01:13:26 +10:00
default :
throw new ArgumentOutOfRangeException ( ) ;
2018-07-19 19:32:07 +10:00
}
2018-07-13 12:45:04 +10:00
2018-09-04 01:13:26 +10:00
//get the updated model
2019-06-12 12:22:14 +10:00
var display = mapToDisplay ( contentItem . PersistedContent ) ;
2018-09-04 01:13:26 +10:00
//merge the tracked success messages with the outgoing model
2018-09-04 16:16:11 +10:00
display . Notifications . AddRange ( globalNotifications . Notifications ) ;
2018-09-06 15:21:02 +10:00
foreach ( var v in display . Variants . Where ( x = > x . Language ! = null ) )
2018-09-04 16:16:11 +10:00
{
if ( notifications . TryGetValue ( v . Language . IsoCode , out var n ) )
v . Notifications . AddRange ( n . Notifications ) ;
}
2018-09-04 01:13:26 +10:00
2019-03-14 00:59:51 +11:00
//lastly, if it is not valid, add the model state to the outgoing object and throw a 400
2019-04-04 21:57:49 +11:00
HandleInvalidModelState ( display , cultureForInvariantErrors ) ;
2018-09-04 01:13:26 +10:00
if ( wasCancelled )
2018-07-19 19:32:07 +10:00
{
2018-09-04 01:13:26 +10:00
AddCancelMessage ( display ) ;
if ( IsCreatingAction ( contentItem . Action ) )
{
//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!
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( display ) ;
2018-09-04 01:13:26 +10:00
}
2018-07-19 19:32:07 +10:00
}
2018-11-12 17:29:38 +11:00
2018-07-19 19:32:07 +10:00
display . PersistedContent = contentItem . PersistedContent ;
2018-07-13 12:45:04 +10:00
2018-07-19 19:32:07 +10:00
return display ;
2018-06-29 19:52:40 +02:00
}
2019-06-12 10:11:27 +10:00
private void AddPublishStatusNotifications ( IReadOnlyCollection < PublishResult > publishStatus , SimpleNotificationModel globalNotifications , Dictionary < string , SimpleNotificationModel > variantNotifications , string [ ] successfulCultures )
{
//global notifications
AddMessageForPublishStatus ( publishStatus , globalNotifications , successfulCultures ) ;
//variant specific notifications
foreach ( var c in successfulCultures ? ? Array . Empty < string > ( ) )
AddMessageForPublishStatus ( publishStatus , variantNotifications . GetOrCreate ( c ) , successfulCultures ) ;
}
2019-03-13 18:12:48 +11:00
/// <summary>
/// Validates critical data for persistence and updates the ModelState and result accordingly
/// </summary>
/// <param name="contentItem"></param>
/// <param name="variantCount">Returns the total number of variants (will be one if it's an invariant content item)</param>
/// <returns></returns>
/// <remarks>
/// For invariant, the variants collection count will be 1 and this will check if that invariant item has the critical values for persistence (i.e. Name)
2019-11-05 12:54:22 +01:00
///
2019-03-13 18:12:48 +11:00
/// For variant, each variant will be checked for critical data for persistence and if it's not there then it's flags will be reset and it will not
/// be persisted. However, we also need to deal with the case where all variants don't pass this check and then there is nothing to save. This also deals
/// with removing the Name validation keys based on data annotations validation for items that haven't been marked to be saved.
/// </remarks>
/// <returns>
/// returns false if persistence cannot take place, returns true if persistence can take place even if there are validation errors
/// </returns>
private bool ValidateCriticalData ( ContentItemSave contentItem , out int variantCount )
{
var variants = contentItem . Variants . ToList ( ) ;
variantCount = variants . Count ;
var savedCount = 0 ;
var variantCriticalValidationErrors = new List < string > ( ) ;
for ( var i = 0 ; i < variants . Count ; i + + )
{
var variant = variants [ i ] ;
if ( variant . Save )
{
//ensure the variant has all critical required data to be persisted
if ( ! RequiredForPersistenceAttribute . HasRequiredValuesForPersistence ( variant ) )
{
variantCriticalValidationErrors . Add ( variant . Culture ) ;
//if there's no Name, it cannot be persisted at all reset the flags, this cannot be saved or published
variant . Save = variant . Publish = false ;
//if there's more than 1 variant, then we need to add the culture specific error
//messages based on the variants in error so that the messages show in the publish/save dialog
if ( variants . Count > 1 )
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( variant . Culture , variant . Segment , "publish/contentPublishedFailedByMissingName" ) ;
2019-03-13 18:12:48 +11:00
else
return false ; //It's invariant and is missing critical data, it cannot be saved
}
savedCount + + ;
}
else
{
var msKey = $"Variants[{i}].Name" ;
if ( ModelState . ContainsKey ( msKey ) )
{
//if it's not being saved, remove the validation key
if ( ! variant . Save ) ModelState . Remove ( msKey ) ;
}
}
}
if ( savedCount = = variantCriticalValidationErrors . Count )
{
//in this case there can be nothing saved since all variants marked to be saved haven't passed critical validation rules
return false ;
}
return true ;
}
2019-11-05 12:54:22 +01:00
2019-04-04 21:57:49 +11:00
2018-11-13 14:31:37 +11:00
/// <summary>
/// Helper method to perform the saving of the content and add the notifications to the result
/// </summary>
/// <param name="contentItem"></param>
/// <param name="saveMethod"></param>
/// <param name="variantCount"></param>
/// <param name="notifications"></param>
/// <param name="globalNotifications"></param>
/// <param name="invariantSavedLocalizationKey"></param>
/// <param name="variantSavedLocalizationKey"></param>
/// <param name="wasCancelled"></param>
/// <remarks>
/// Method is used for normal Saving and Scheduled Publishing
/// </remarks>
private void SaveAndNotify ( ContentItemSave contentItem , Func < IContent , OperationResult > saveMethod , int variantCount ,
Dictionary < string , SimpleNotificationModel > notifications , SimpleNotificationModel globalNotifications ,
2019-04-04 21:57:49 +11:00
string invariantSavedLocalizationKey , string variantSavedLocalizationKey , string cultureForInvariantErrors ,
2018-11-13 14:31:37 +11:00
out bool wasCancelled )
{
var saveResult = saveMethod ( contentItem . PersistedContent ) ;
wasCancelled = saveResult . Success = = false & & saveResult . Result = = OperationResultType . FailedCancelledByEvent ;
if ( saveResult . Success )
{
if ( variantCount > 1 )
{
2020-05-14 17:03:33 +02:00
var variantErrors = ModelState . GetVariantsWithErrors ( cultureForInvariantErrors ) ;
2020-02-18 11:46:31 +01:00
var savedWithoutErrors = contentItem . Variants
2020-05-14 17:03:33 +02:00
. Where ( x = > x . Save & & ! variantErrors . Contains ( ( x . Culture , x . Segment ) ) )
2020-05-18 10:37:28 +02:00
. Select ( x = > ( culture : x . Culture , segment : x . Segment ) ) ;
2020-02-18 11:46:31 +01:00
2020-05-14 17:03:33 +02:00
foreach ( var ( culture , segment ) in savedWithoutErrors )
2018-11-13 14:31:37 +11:00
{
2020-05-14 17:03:33 +02:00
var variantName = GetVariantName ( culture , segment ) ;
AddSuccessNotification ( notifications , culture , segment ,
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentSavedHeader" ) ,
_localizedTextService . Localize ( variantSavedLocalizationKey , new [ ] { variantName } ) ) ;
2018-11-13 14:31:37 +11:00
}
}
else if ( ModelState . IsValid )
{
globalNotifications . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentSavedHeader" ) ,
_localizedTextService . Localize ( invariantSavedLocalizationKey ) ) ;
2018-11-13 14:31:37 +11:00
}
}
}
2018-11-09 16:12:08 +11:00
/// <summary>
/// Validates the incoming schedule and update the model
/// </summary>
/// <param name="contentItem"></param>
/// <param name="globalNotifications"></param>
private bool SaveSchedule ( ContentItemSave contentItem , SimpleNotificationModel globalNotifications )
{
if ( ! contentItem . PersistedContent . ContentType . VariesByCulture ( ) )
return SaveScheduleInvariant ( contentItem , globalNotifications ) ;
else
return SaveScheduleVariant ( contentItem ) ;
}
private bool SaveScheduleInvariant ( ContentItemSave contentItem , SimpleNotificationModel globalNotifications )
{
2018-11-12 17:29:38 +11:00
var variant = contentItem . Variants . First ( ) ;
2018-11-09 16:12:08 +11:00
2018-11-14 22:31:51 +11:00
var currRelease = contentItem . PersistedContent . ContentSchedule . GetSchedule ( ContentScheduleAction . Release ) . ToList ( ) ;
var currExpire = contentItem . PersistedContent . ContentSchedule . GetSchedule ( ContentScheduleAction . Expire ) . ToList ( ) ;
2018-11-09 16:12:08 +11:00
2018-11-12 17:29:38 +11:00
//Do all validation of data first
2018-11-09 16:12:08 +11:00
2018-11-12 17:29:38 +11:00
//1) release date cannot be less than now
if ( variant . ReleaseDate . HasValue & & variant . ReleaseDate < DateTime . Now )
2018-11-09 16:12:08 +11:00
{
2018-11-12 17:29:38 +11:00
globalNotifications . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles" , "validationFailedHeader" ) ,
_localizedTextService . Localize ( "speechBubbles" , "scheduleErrReleaseDate1" ) ) ;
2018-11-12 17:29:38 +11:00
return false ;
2018-11-09 16:12:08 +11:00
}
2018-11-12 17:29:38 +11:00
//2) expire date cannot be less than now
if ( variant . ExpireDate . HasValue & & variant . ExpireDate < DateTime . Now )
2018-11-09 16:12:08 +11:00
{
2018-11-12 17:29:38 +11:00
globalNotifications . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles" , "validationFailedHeader" ) ,
_localizedTextService . Localize ( "speechBubbles" , "scheduleErrExpireDate1" ) ) ;
2018-11-12 17:29:38 +11:00
return false ;
2018-11-09 16:12:08 +11:00
}
2018-11-12 17:29:38 +11:00
//3) expire date cannot be less than release date
if ( variant . ExpireDate . HasValue & & variant . ReleaseDate . HasValue & & variant . ExpireDate < = variant . ReleaseDate )
2018-11-09 16:12:08 +11:00
{
2018-11-12 17:29:38 +11:00
globalNotifications . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles" , "validationFailedHeader" ) ,
_localizedTextService . Localize ( "speechBubbles" , "scheduleErrExpireDate2" ) ) ;
2018-11-12 17:29:38 +11:00
return false ;
2018-11-09 16:12:08 +11:00
}
2018-11-12 17:29:38 +11:00
//Now we can do the data updates
//remove any existing release dates so we can replace it
//if there is a release date in the request or if there was previously a release and the request value is null then we are clearing the schedule
if ( variant . ReleaseDate . HasValue | | currRelease . Count > 0 )
2018-11-14 22:31:51 +11:00
contentItem . PersistedContent . ContentSchedule . Clear ( ContentScheduleAction . Release ) ;
2018-11-12 17:29:38 +11:00
//remove any existing expire dates so we can replace it
//if there is an expiry date in the request or if there was a previous expiry and the request value is null then we are clearing the schedule
if ( variant . ExpireDate . HasValue | | currExpire . Count > 0 )
2018-11-14 22:31:51 +11:00
contentItem . PersistedContent . ContentSchedule . Clear ( ContentScheduleAction . Expire ) ;
2018-11-12 17:29:38 +11:00
2018-11-09 16:12:08 +11:00
//add the new schedule
2018-11-12 17:29:38 +11:00
contentItem . PersistedContent . ContentSchedule . Add ( variant . ReleaseDate , variant . ExpireDate ) ;
2018-11-09 16:12:08 +11:00
return true ;
}
private bool SaveScheduleVariant ( ContentItemSave contentItem )
{
2018-11-13 00:46:13 +11:00
//All variants in this collection should have a culture if we get here but we'll double check and filter here)
var cultureVariants = contentItem . Variants . Where ( x = > ! x . Culture . IsNullOrWhiteSpace ( ) ) . ToList ( ) ;
var mandatoryCultures = _allLangs . Value . Values . Where ( x = > x . IsMandatory ) . Select ( x = > x . IsoCode ) . ToList ( ) ;
2018-11-12 17:29:38 +11:00
2018-11-13 14:31:37 +11:00
//Make a copy of the current schedule and apply updates to it
var schedCopy = ( ContentScheduleCollection ) contentItem . PersistedContent . ContentSchedule . DeepClone ( ) ;
foreach ( var variant in cultureVariants . Where ( x = > x . Save ) )
{
2018-11-14 22:31:51 +11:00
var currRelease = schedCopy . GetSchedule ( variant . Culture , ContentScheduleAction . Release ) . ToList ( ) ;
var currExpire = schedCopy . GetSchedule ( variant . Culture , ContentScheduleAction . Expire ) . ToList ( ) ;
2018-11-13 14:31:37 +11:00
//remove any existing release dates so we can replace it
//if there is a release date in the request or if there was previously a release and the request value is null then we are clearing the schedule
if ( variant . ReleaseDate . HasValue | | currRelease . Count > 0 )
2018-11-14 22:31:51 +11:00
schedCopy . Clear ( variant . Culture , ContentScheduleAction . Release ) ;
2018-11-13 14:31:37 +11:00
//remove any existing expire dates so we can replace it
//if there is an expiry date in the request or if there was a previous expiry and the request value is null then we are clearing the schedule
if ( variant . ExpireDate . HasValue | | currExpire . Count > 0 )
2018-11-14 22:31:51 +11:00
schedCopy . Clear ( variant . Culture , ContentScheduleAction . Expire ) ;
2018-11-13 14:31:37 +11:00
//add the new schedule
schedCopy . Add ( variant . Culture , variant . ReleaseDate , variant . ExpireDate ) ;
}
//now validate the new schedule to make sure it passes all of the rules
2018-11-09 16:12:08 +11:00
var isValid = true ;
2018-11-13 00:46:13 +11:00
2018-11-13 14:31:37 +11:00
//create lists of mandatory/non-mandatory states
var mandatoryVariants = new List < ( string culture , bool isPublished , List < DateTime > releaseDates ) > ( ) ;
var nonMandatoryVariants = new List < ( string culture , bool isPublished , List < DateTime > releaseDates ) > ( ) ;
foreach ( var groupedSched in schedCopy . FullSchedule . GroupBy ( x = > x . Culture ) )
{
var isPublished = contentItem . PersistedContent . Published & & contentItem . PersistedContent . IsCulturePublished ( groupedSched . Key ) ;
2018-11-14 22:31:51 +11:00
var releaseDates = groupedSched . Where ( x = > x . Action = = ContentScheduleAction . Release ) . Select ( x = > x . Date ) . ToList ( ) ;
2018-11-13 14:31:37 +11:00
if ( mandatoryCultures . Contains ( groupedSched . Key , StringComparer . InvariantCultureIgnoreCase ) )
mandatoryVariants . Add ( ( groupedSched . Key , isPublished , releaseDates ) ) ;
else
nonMandatoryVariants . Add ( ( groupedSched . Key , isPublished , releaseDates ) ) ;
}
var nonMandatoryVariantReleaseDates = nonMandatoryVariants . SelectMany ( x = > x . releaseDates ) . ToList ( ) ;
2018-11-13 00:46:13 +11:00
2018-11-13 14:31:37 +11:00
//validate that the mandatory languages have the right data
foreach ( var ( culture , isPublished , releaseDates ) in mandatoryVariants )
2018-11-13 00:46:13 +11:00
{
2018-11-13 14:31:37 +11:00
if ( ! isPublished & & releaseDates . Count = = 0 )
2018-11-13 00:46:13 +11:00
{
2018-11-13 14:31:37 +11:00
//can't continue, a mandatory variant is not published and not scheduled for publishing
2020-05-14 17:03:33 +02:00
// TODO: Add segment
AddVariantValidationError ( culture , null , "speechBubbles/scheduleErrReleaseDate2" ) ;
2018-11-13 00:46:13 +11:00
isValid = false ;
2018-11-13 14:31:37 +11:00
continue ;
}
if ( ! isPublished & & releaseDates . Any ( x = > nonMandatoryVariantReleaseDates . Any ( r = > x . Date > r . Date ) ) )
{
//can't continue, a mandatory variant is not published and it's scheduled for publishing after a non-mandatory
2020-05-14 17:03:33 +02:00
// TODO: Add segment
AddVariantValidationError ( culture , null , "speechBubbles/scheduleErrReleaseDate3" ) ;
2018-11-13 14:31:37 +11:00
isValid = false ;
continue ;
2018-11-13 00:46:13 +11:00
}
}
if ( ! isValid ) return false ;
2018-11-13 14:31:37 +11:00
//now we can validate the more basic rules for individual variants
2018-11-13 00:46:13 +11:00
foreach ( var variant in cultureVariants . Where ( x = > x . ReleaseDate . HasValue | | x . ExpireDate . HasValue ) )
2018-11-09 16:12:08 +11:00
{
2018-11-12 17:29:38 +11:00
//1) release date cannot be less than now
if ( variant . ReleaseDate . HasValue & & variant . ReleaseDate < DateTime . Now )
2018-11-09 16:12:08 +11:00
{
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( variant . Culture , variant . Segment , "speechBubbles/scheduleErrReleaseDate1" ) ;
2018-11-09 16:12:08 +11:00
isValid = false ;
continue ;
}
2018-11-12 17:29:38 +11:00
//2) expire date cannot be less than now
if ( variant . ExpireDate . HasValue & & variant . ExpireDate < DateTime . Now )
2018-11-09 16:12:08 +11:00
{
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( variant . Culture , variant . Segment , "speechBubbles/scheduleErrExpireDate1" ) ;
2018-11-12 17:29:38 +11:00
isValid = false ;
continue ;
2018-11-09 16:12:08 +11:00
}
2018-11-12 17:29:38 +11:00
//3) expire date cannot be less than release date
if ( variant . ExpireDate . HasValue & & variant . ReleaseDate . HasValue & & variant . ExpireDate < = variant . ReleaseDate )
2018-11-09 16:12:08 +11:00
{
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( variant . Culture , variant . Segment , "speechBubbles/scheduleErrExpireDate2" ) ;
2018-11-12 17:29:38 +11:00
isValid = false ;
continue ;
2018-11-09 16:12:08 +11:00
}
2018-11-12 17:29:38 +11:00
}
2018-11-09 16:12:08 +11:00
2018-11-12 17:29:38 +11:00
if ( ! isValid ) return false ;
2018-11-13 14:31:37 +11:00
//now that we are validated, we can assign the copied schedule back to the model
contentItem . PersistedContent . ContentSchedule = schedCopy ;
return true ;
2018-11-09 16:12:08 +11:00
}
2018-09-04 16:16:11 +10:00
/// <summary>
/// Used to add success notifications globally and for the culture
/// </summary>
/// <param name="notifications"></param>
/// <param name="culture"></param>
/// <param name="header"></param>
/// <param name="msg"></param>
/// <remarks>
/// global notifications will be shown if all variant processing is successful and the save/publish dialog is closed, otherwise
2019-01-26 10:52:19 -05:00
/// variant specific notifications are used to show success messages in the save/publish dialog.
2018-09-04 16:16:11 +10:00
/// </remarks>
2020-05-14 17:03:33 +02:00
private static void AddSuccessNotification ( IDictionary < string , SimpleNotificationModel > notifications , string culture , string segment , string header , string msg )
2018-09-04 16:16:11 +10:00
{
//add the global notification (which will display globally if all variants are successfully processed)
notifications [ string . Empty ] . AddSuccessNotification ( header , msg ) ;
//add the variant specific notification (which will display in the dialog if all variants are not successfully processed)
2020-05-14 17:03:33 +02:00
var key = culture + "_" + segment ;
notifications . GetOrCreate ( key ) . AddSuccessNotification ( header , msg ) ;
2018-09-04 16:16:11 +10:00
}
2018-11-15 15:24:09 +11:00
/// <summary>
/// The user must have publish access to all descendant nodes of the content item in order to continue
/// </summary>
/// <param name="contentItem"></param>
/// <returns></returns>
private bool ValidatePublishBranchPermissions ( ContentItemSave contentItem , out IReadOnlyList < IUmbracoEntity > noAccess )
{
var denied = new List < IUmbracoEntity > ( ) ;
var page = 0 ;
const int pageSize = 500 ;
var total = long . MaxValue ;
while ( page * pageSize < total )
{
2020-06-09 13:48:50 +02:00
var descendants = _entityService . GetPagedDescendants ( contentItem . Id , UmbracoObjectTypes . Document , page + + , pageSize , out total ,
2018-11-15 15:24:09 +11:00
//order by shallowest to deepest, this allows us to check permissions from top to bottom so we can exit
//early if a permission higher up fails
2018-12-21 13:15:46 +11:00
ordering : Ordering . By ( "path" , Direction . Ascending ) ) ;
2018-11-15 15:24:09 +11:00
foreach ( var c in descendants )
{
//if this item's path has already been denied or if the user doesn't have access to it, add to the deny list
if ( denied . Any ( x = > c . Path . StartsWith ( $"{x.Path}," ) )
| | ( ContentPermissionsHelper . CheckPermissions ( c ,
2020-06-09 13:48:50 +02:00
_webSecurity . CurrentUser , _userService , _entityService ,
2018-11-15 15:24:09 +11:00
ActionPublish . ActionLetter ) = = ContentPermissionsHelper . ContentAccess . Denied ) )
{
denied . Add ( c ) ;
2018-12-07 13:44:41 +01:00
}
2018-11-15 15:24:09 +11:00
}
}
noAccess = denied ;
return denied . Count = = 0 ;
}
2019-04-04 21:57:49 +11:00
private IEnumerable < PublishResult > PublishBranchInternal ( ContentItemSave contentItem , bool force , string cultureForInvariantErrors ,
2018-11-15 00:09:57 +11:00
out bool wasCancelled , out string [ ] successfulCultures )
{
if ( ! contentItem . PersistedContent . ContentType . VariesByCulture ( ) )
{
//its invariant, proceed normally
2020-06-09 13:48:50 +02:00
var publishStatus = _contentService . SaveAndPublishBranch ( contentItem . PersistedContent , force , userId : _webSecurity . CurrentUser . Id ) ;
2019-01-27 01:17:32 -05:00
// TODO: Deal with multiple cancellations
2018-11-15 00:09:57 +11:00
wasCancelled = publishStatus . Any ( x = > x . Result = = PublishResultType . FailedPublishCancelledByEvent ) ;
2019-06-07 13:13:52 +10:00
successfulCultures = null ; //must be null! this implies invariant
2019-01-04 08:16:20 +01:00
return publishStatus ;
2018-11-15 00:09:57 +11:00
}
var mandatoryCultures = _allLangs . Value . Values . Where ( x = > x . IsMandatory ) . Select ( x = > x . IsoCode ) . ToList ( ) ;
2020-05-14 17:03:33 +02:00
var variantErrors = ModelState . GetVariantsWithErrors ( cultureForInvariantErrors ) ;
var variants = contentItem . Variants . ToList ( ) ;
2019-03-14 14:18:42 +11:00
2018-11-15 00:09:57 +11:00
//validate if we can publish based on the mandatory language requirements
var canPublish = ValidatePublishingMandatoryLanguages (
2020-05-14 17:03:33 +02:00
variantErrors ,
contentItem , variants , mandatoryCultures ,
2019-03-14 14:18:42 +11:00
mandatoryVariant = > mandatoryVariant . Publish ) ;
2018-11-15 00:09:57 +11:00
//Now check if there are validation errors on each variant.
//If validation errors are detected on a variant and it's state is set to 'publish', then we
//need to change it to 'save'.
//It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages.
2019-11-05 12:54:22 +01:00
2018-11-15 00:09:57 +11:00
foreach ( var variant in contentItem . Variants )
{
2020-05-14 17:03:33 +02:00
if ( variantErrors . Contains ( ( variant . Culture , variant . Segment ) ) )
2018-11-15 00:09:57 +11:00
variant . Publish = false ;
}
2020-05-14 17:03:33 +02:00
var culturesToPublish = variants . Where ( x = > x . Publish ) . Select ( x = > x . Culture ) . ToArray ( ) ;
2018-11-15 00:09:57 +11:00
if ( canPublish )
{
//proceed to publish if all validation still succeeds
2020-06-09 13:48:50 +02:00
var publishStatus = _contentService . SaveAndPublishBranch ( contentItem . PersistedContent , force , culturesToPublish , _webSecurity . CurrentUser . Id ) ;
2019-01-27 01:17:32 -05:00
// TODO: Deal with multiple cancellations
2018-11-15 00:09:57 +11:00
wasCancelled = publishStatus . Any ( x = > x . Result = = PublishResultType . FailedPublishCancelledByEvent ) ;
successfulCultures = contentItem . Variants . Where ( x = > x . Publish ) . Select ( x = > x . Culture ) . ToArray ( ) ;
return publishStatus ;
}
else
{
//can only save
2020-06-09 13:48:50 +02:00
var saveResult = _contentService . Save ( contentItem . PersistedContent , _webSecurity . CurrentUser . Id ) ;
2018-11-15 00:09:57 +11:00
var publishStatus = new [ ]
{
new PublishResult ( PublishResultType . FailedPublishMandatoryCultureMissing , null , contentItem . PersistedContent )
} ;
wasCancelled = saveResult . Result = = OperationResultType . FailedCancelledByEvent ;
successfulCultures = Array . Empty < string > ( ) ;
return publishStatus ;
}
}
2018-06-29 19:52:40 +02:00
/// <summary>
/// Performs the publishing operation for a content item
/// </summary>
/// <param name="contentItem"></param>
2018-05-08 01:16:32 +10:00
/// <param name="wasCancelled"></param>
2018-09-04 01:13:26 +10:00
/// <param name="successfulCultures">
/// if the content is variant this will return an array of cultures that will be published (passed validation rules)
/// </param>
2018-05-08 01:16:32 +10:00
/// <remarks>
/// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal
/// </remarks>
2019-04-04 21:57:49 +11:00
private PublishResult PublishInternal ( ContentItemSave contentItem , string defaultCulture , string cultureForInvariantErrors , out bool wasCancelled , out string [ ] successfulCultures )
2018-05-08 01:16:32 +10:00
{
2018-06-20 14:18:57 +02:00
if ( ! contentItem . PersistedContent . ContentType . VariesByCulture ( ) )
2018-05-08 01:16:32 +10:00
{
//its invariant, proceed normally
2020-06-09 13:48:50 +02:00
var publishStatus = _contentService . SaveAndPublish ( contentItem . PersistedContent , userId : _webSecurity . CurrentUser . Id ) ;
2018-11-07 19:42:49 +11:00
wasCancelled = publishStatus . Result = = PublishResultType . FailedPublishCancelledByEvent ;
2019-06-12 10:11:27 +10:00
successfulCultures = null ; //must be null! this implies invariant
2018-11-15 00:09:57 +11:00
return publishStatus ;
2018-05-08 01:16:32 +10:00
}
2018-11-15 00:09:57 +11:00
var mandatoryCultures = _allLangs . Value . Values . Where ( x = > x . IsMandatory ) . Select ( x = > x . IsoCode ) . ToList ( ) ;
2020-05-14 17:03:33 +02:00
var variantErrors = ModelState . GetVariantsWithErrors ( cultureForInvariantErrors ) ;
var variants = contentItem . Variants . ToList ( ) ;
2019-03-14 14:18:42 +11:00
//validate if we can publish based on the mandatory languages selected
2018-11-15 00:09:57 +11:00
var canPublish = ValidatePublishingMandatoryLanguages (
2020-05-14 17:03:33 +02:00
variantErrors ,
contentItem , variants , mandatoryCultures ,
2019-03-14 14:18:42 +11:00
mandatoryVariant = > mandatoryVariant . Publish ) ;
//if none are published and there are validation errors for mandatory cultures, then we can't publish anything
2018-11-15 00:09:57 +11:00
//Now check if there are validation errors on each variant.
//If validation errors are detected on a variant and it's state is set to 'publish', then we
//need to change it to 'save'.
2019-11-05 12:54:22 +01:00
//It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages.
2018-11-15 00:09:57 +11:00
foreach ( var variant in contentItem . Variants )
2018-05-08 01:16:32 +10:00
{
2020-05-14 17:03:33 +02:00
if ( variantErrors . Contains ( ( variant . Culture , variant . Segment ) ) )
2018-11-15 00:09:57 +11:00
variant . Publish = false ;
}
2018-09-03 22:46:24 +10:00
2019-02-19 02:51:05 +11:00
//At this stage all variants might have failed validation which means there are no cultures flagged for publishing!
2020-05-14 17:03:33 +02:00
var culturesToPublish = variants . Where ( x = > x . Publish ) . Select ( x = > x . Culture ) . ToArray ( ) ;
2019-02-19 02:51:05 +11:00
canPublish = canPublish & & culturesToPublish . Length > 0 ;
2018-11-15 00:09:57 +11:00
if ( canPublish )
{
//try to publish all the values on the model - this will generally only fail if someone is tampering with the request
//since there's no reason variant rules would be violated in normal cases.
2020-05-14 17:03:33 +02:00
canPublish = PublishCulture ( contentItem . PersistedContent , variants , defaultCulture ) ;
2018-11-15 00:09:57 +11:00
}
2018-05-08 01:16:32 +10:00
2018-11-15 00:09:57 +11:00
if ( canPublish )
{
//proceed to publish if all validation still succeeds
2020-06-09 13:48:50 +02:00
var publishStatus = _contentService . SaveAndPublish ( contentItem . PersistedContent , culturesToPublish , _webSecurity . CurrentUser . Id ) ;
2018-11-15 00:09:57 +11:00
wasCancelled = publishStatus . Result = = PublishResultType . FailedPublishCancelledByEvent ;
2019-02-04 16:55:35 +11:00
successfulCultures = culturesToPublish ;
2018-11-15 00:09:57 +11:00
return publishStatus ;
}
else
{
//can only save
2020-06-09 13:48:50 +02:00
var saveResult = _contentService . Save ( contentItem . PersistedContent , _webSecurity . CurrentUser . Id ) ;
2018-11-15 00:09:57 +11:00
var publishStatus = new PublishResult ( PublishResultType . FailedPublishMandatoryCultureMissing , null , contentItem . PersistedContent ) ;
wasCancelled = saveResult . Result = = OperationResultType . FailedCancelledByEvent ;
successfulCultures = Array . Empty < string > ( ) ;
return publishStatus ;
2018-05-08 01:16:32 +10:00
}
}
2018-05-10 18:01:41 +10:00
2018-09-03 22:46:24 +10:00
/// <summary>
/// Validate if publishing is possible based on the mandatory language requirements
/// </summary>
2020-05-14 17:03:33 +02:00
/// <param name="variantsWithValidationErrors"></param>
2018-09-03 22:46:24 +10:00
/// <param name="contentItem"></param>
2020-05-14 17:03:33 +02:00
/// <param name="variants"></param>
2019-02-04 16:55:35 +11:00
/// <param name="mandatoryCultures"></param>
/// <param name="publishingCheck"></param>
2018-09-03 22:46:24 +10:00
/// <returns></returns>
2018-11-09 16:12:08 +11:00
private bool ValidatePublishingMandatoryLanguages (
2020-05-14 17:03:33 +02:00
IReadOnlyCollection < ( string culture , string segment ) > variantsWithValidationErrors ,
2018-11-09 16:12:08 +11:00
ContentItemSave contentItem ,
2020-05-14 17:03:33 +02:00
IReadOnlyCollection < ContentVariantSave > variants ,
2018-11-13 00:46:13 +11:00
IReadOnlyList < string > mandatoryCultures ,
2019-03-14 14:18:42 +11:00
Func < ContentVariantSave , bool > publishingCheck )
2018-09-03 22:46:24 +10:00
{
var canPublish = true ;
2019-03-14 14:18:42 +11:00
var result = new List < ( ContentVariantSave model , bool publishing , bool isValid ) > ( ) ;
2018-09-03 22:46:24 +10:00
2018-11-13 00:46:13 +11:00
foreach ( var culture in mandatoryCultures )
2018-09-03 22:46:24 +10:00
{
//Check if a mandatory language is missing from being published
2020-05-14 17:03:33 +02:00
var mandatoryVariant = variants . First ( x = > x . Culture . InvariantEquals ( culture ) ) ;
2018-12-07 13:44:41 +01:00
2018-11-13 00:46:13 +11:00
var isPublished = contentItem . PersistedContent . Published & & contentItem . PersistedContent . IsCulturePublished ( culture ) ;
2019-02-04 16:55:35 +11:00
var isPublishing = isPublished | | publishingCheck ( mandatoryVariant ) ;
2020-05-14 17:03:33 +02:00
var isValid = ! variantsWithValidationErrors . Select ( v = > v . culture ) . InvariantContains ( culture ) ;
2018-09-03 22:46:24 +10:00
2019-03-14 14:18:42 +11:00
result . Add ( ( mandatoryVariant , isPublished | | isPublishing , isValid ) ) ;
}
2018-09-03 22:46:24 +10:00
2019-03-14 14:18:42 +11:00
//iterate over the results by invalid first
string firstInvalidMandatoryCulture = null ;
foreach ( var r in result . OrderBy ( x = > x . isValid ) )
{
if ( ! r . isValid )
firstInvalidMandatoryCulture = r . model . Culture ;
2018-09-03 22:46:24 +10:00
2019-03-14 14:18:42 +11:00
if ( r . publishing & & ! r . isValid )
{
//flagged for publishing but the mandatory culture is invalid
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( r . model . Culture , r . model . Segment , "publish/contentPublishedFailedReqCultureValidationError" ) ;
2019-03-14 14:18:42 +11:00
canPublish = false ;
}
else if ( r . publishing & & r . isValid & & firstInvalidMandatoryCulture ! = null )
{
//in this case this culture also cannot be published because another mandatory culture is invalid
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( r . model . Culture , r . model . Segment , "publish/contentPublishedFailedReqCultureValidationError" , firstInvalidMandatoryCulture ) ;
2019-03-14 14:18:42 +11:00
canPublish = false ;
}
else if ( ! r . publishing )
{
//cannot continue publishing since a required culture that is not currently being published isn't published
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( r . model . Culture , r . model . Segment , "speechBubbles/contentReqCulturePublishError" ) ;
2019-03-14 14:18:42 +11:00
canPublish = false ;
}
2018-09-03 22:46:24 +10:00
}
return canPublish ;
}
2018-06-29 19:52:40 +02:00
/// <summary>
2019-02-04 16:55:35 +11:00
/// Call PublishCulture on the content item for each culture to get a validation result for each culture
2018-06-29 19:52:40 +02:00
/// </summary>
2018-07-31 17:50:24 +10:00
/// <param name="persistentContent"></param>
/// <param name="cultureVariants"></param>
2018-05-10 18:01:41 +10:00
/// <returns></returns>
2018-11-06 21:33:24 +11:00
/// <remarks>
/// This would generally never fail unless someone is tampering with the request
/// </remarks>
2019-04-04 21:57:49 +11:00
private bool PublishCulture ( IContent persistentContent , IEnumerable < ContentVariantSave > cultureVariants , string defaultCulture )
2018-05-10 18:01:41 +10:00
{
2018-11-12 17:29:38 +11:00
foreach ( var variant in cultureVariants . Where ( x = > x . Publish ) )
2018-05-10 18:01:41 +10:00
{
2018-06-22 21:03:47 +02:00
// publishing any culture, implies the invariant culture
2019-04-04 21:57:49 +11:00
var valid = persistentContent . PublishCulture ( CultureImpact . Explicit ( variant . Culture , defaultCulture . InvariantEquals ( variant . Culture ) ) ) ;
2018-05-10 18:01:41 +10:00
if ( ! valid )
{
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( variant . Culture , variant . Segment , "speechBubbles/contentCultureValidationError" ) ;
2018-05-10 18:01:41 +10:00
return false ;
}
}
return true ;
}
2018-06-29 19:52:40 +02:00
2018-08-06 16:50:40 +10:00
/// <summary>
2018-11-09 16:12:08 +11:00
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs
2018-08-06 16:50:40 +10:00
/// </summary>
2019-03-14 14:18:42 +11:00
/// <param name="culture">Culture to assign the error to</param>
2020-05-14 17:03:33 +02:00
/// <param name="segment">Segment to assign the error to</param>
2018-08-14 16:29:14 +10:00
/// <param name="localizationKey"></param>
2019-03-14 14:18:42 +11:00
/// <param name="cultureToken">
2019-11-05 12:54:22 +01:00
/// The culture used in the localization message, null by default which means <see cref="culture"/> will be used.
2019-03-14 14:18:42 +11:00
/// </param>
2020-05-14 17:03:33 +02:00
private void AddVariantValidationError ( string culture , string segment , string localizationKey , string cultureToken = null )
2018-08-06 16:50:40 +10:00
{
2020-05-14 17:03:33 +02:00
var cultureToUse = cultureToken ? ? culture ;
var variantName = GetVariantName ( cultureToUse , segment ) ;
2020-06-09 13:48:50 +02:00
var errMsg = _localizedTextService . Localize ( localizationKey , new [ ] { variantName } ) ;
2020-05-14 17:03:33 +02:00
ModelState . AddVariantValidationError ( culture , segment , errMsg ) ;
}
/// <summary>
2020-05-18 10:37:28 +02:00
/// Creates the human readable variant name based on culture and segment
2020-05-14 17:03:33 +02:00
/// </summary>
/// <param name="culture">Culture</param>
/// <param name="segment">Segment</param>
/// <returns></returns>
private string GetVariantName ( string culture , string segment )
{
if ( culture . IsNullOrWhiteSpace ( ) & & segment . IsNullOrWhiteSpace ( ) )
{
// TODO: Get name for default variant from somewhere?
return "Default" ;
}
var cultureName = culture = = null ? null : _allLangs . Value [ culture ] . CultureName ;
var variantName = string . Join ( " — " , new [ ] { segment , cultureName } . Where ( x = > ! x . IsNullOrWhiteSpace ( ) ) ) ;
// Format: <segment> [—] <culture name>
return variantName ;
2018-08-06 16:50:40 +10:00
}
2018-10-29 17:27:33 +11:00
2018-06-29 19:52:40 +02:00
/// <summary>
/// Publishes a document with a given ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>
2018-10-29 17:27:33 +11:00
/// The EnsureUserPermissionForContent attribute will deny access to this method if the current user
2018-06-29 19:52:40 +02:00
/// does not have Publish access to this node.
/// </remarks>
///
[EnsureUserPermissionForContent("id", 'U')]
2020-06-09 13:48:50 +02:00
public IActionResult PostPublishById ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = GetObjectFromRequest ( ( ) = > _contentService . GetById ( id ) ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
{
2018-10-30 00:36:11 +11:00
return HandleContentNotFound ( id , false ) ;
2018-06-29 19:52:40 +02:00
}
2020-06-09 13:48:50 +02:00
var publishResult = _contentService . SaveAndPublish ( foundContent , userId : _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
if ( publishResult . Success = = false )
{
var notificationModel = new SimpleNotificationModel ( ) ;
2019-03-12 16:10:35 +11:00
AddMessageForPublishStatus ( new [ ] { publishResult } , notificationModel ) ;
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( notificationModel ) ;
2018-06-29 19:52:40 +02:00
}
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-06-29 19:52:40 +02:00
}
[HttpDelete]
[HttpPost]
2020-06-09 13:48:50 +02:00
public IActionResult DeleteBlueprint ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var found = _contentService . GetBlueprintById ( id ) ;
2018-06-29 19:52:40 +02:00
if ( found = = null )
{
return HandleContentNotFound ( id , false ) ;
}
2020-06-09 13:48:50 +02:00
_contentService . DeleteBlueprint ( found ) ;
2018-06-29 19:52:40 +02:00
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Moves an item to the recycle bin, if it is already there then it will permanently delete it
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>
/// The CanAccessContentAuthorize attribute will deny access to this method if the current user
/// does not have Delete access to this node.
/// </remarks>
2019-01-22 22:46:15 +01:00
[EnsureUserPermissionForContent("id", ActionDelete.ActionLetter)]
2018-06-29 19:52:40 +02:00
[HttpDelete]
[HttpPost]
2020-06-09 13:48:50 +02:00
public IActionResult DeleteById ( int id )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = GetObjectFromRequest ( ( ) = > _contentService . GetById ( id ) ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
{
return HandleContentNotFound ( id , false ) ;
}
//if the current item is in the recycle bin
if ( foundContent . Trashed = = false )
{
2020-06-09 13:48:50 +02:00
var moveResult = _contentService . MoveToRecycleBin ( foundContent , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
if ( moveResult . Success = = false )
{
//returning an object of INotificationModel will ensure that any pending
// notification messages are added to the response.
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( new SimpleNotificationModel ( ) ) ;
2018-06-29 19:52:40 +02:00
}
}
else
{
2020-06-09 13:48:50 +02:00
var deleteResult = _contentService . Delete ( foundContent , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
if ( deleteResult . Success = = false )
{
//returning an object of INotificationModel will ensure that any pending
// notification messages are added to the response.
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( new SimpleNotificationModel ( ) ) ;
2018-06-29 19:52:40 +02:00
}
}
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Empties the recycle bin
/// </summary>
/// <returns></returns>
/// <remarks>
/// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin
/// </remarks>
[HttpDelete]
[HttpPost]
2019-11-05 13:45:42 +01:00
[EnsureUserPermissionForContent(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)]
2020-06-12 11:14:06 +02:00
public IActionResult EmptyRecycleBin ( )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
_contentService . EmptyRecycleBin ( _webSecurity . GetUserId ( ) . ResultOr ( Constants . Security . SuperUserId ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-12 11:14:06 +02:00
return new UmbracoNotificationSuccessResponse ( _localizedTextService . Localize ( "defaultdialogs/recycleBinIsEmpty" ) ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Change the sort order for content
/// </summary>
/// <param name="sorted"></param>
/// <returns></returns>
[EnsureUserPermissionForContent("sorted.ParentId", 'S')]
2020-06-09 13:48:50 +02:00
public IActionResult PostSort ( ContentSortOrder sorted )
2018-06-29 19:52:40 +02:00
{
if ( sorted = = null )
{
2020-06-09 13:48:50 +02:00
return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
}
//if there's nothing to sort just return ok
if ( sorted . IdSortOrder . Length = = 0 )
{
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-06-29 19:52:40 +02:00
}
try
{
2020-06-09 13:48:50 +02:00
var contentService = _contentService ;
2018-06-29 19:52:40 +02:00
// Save content with new sort order and update content xml in db accordingly
2020-06-09 13:48:50 +02:00
var sortResult = contentService . Sort ( sorted . IdSortOrder , _webSecurity . CurrentUser . Id ) ;
2018-10-24 23:55:55 +11:00
if ( ! sortResult . Success )
2018-06-29 19:52:40 +02:00
{
Logger . Warn < ContentController > ( "Content sorting failed, this was probably caused by an event being cancelled" ) ;
2019-01-27 01:17:32 -05:00
// TODO: Now you can cancel sorting, does the event messages bubble up automatically?
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( "Content sorting failed, this was probably caused by an event being cancelled" ) ;
2018-06-29 19:52:40 +02:00
}
2018-08-03 08:35:01 +01:00
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-06-29 19:52:40 +02:00
}
catch ( Exception ex )
{
2018-08-17 15:41:58 +01:00
Logger . Error < ContentController > ( ex , "Could not update content sort order" ) ;
2018-06-29 19:52:40 +02:00
throw ;
}
}
/// <summary>
/// Change the sort order for media
/// </summary>
/// <param name="move"></param>
/// <returns></returns>
[EnsureUserPermissionForContent("move.ParentId", 'M')]
2020-06-09 13:48:50 +02:00
public IActionResult PostMove ( MoveOrCopy move )
2018-06-29 19:52:40 +02:00
{
var toMove = ValidateMoveOrCopy ( move ) ;
2020-06-09 13:48:50 +02:00
_contentService . Move ( toMove , move . ParentId , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-12 22:13:43 +02:00
return Content ( toMove . Path , MediaTypeNames . Text . Plain , Encoding . UTF8 ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Copies a content item and places the copy as a child of a given parent Id
/// </summary>
/// <param name="copy"></param>
/// <returns></returns>
[EnsureUserPermissionForContent("copy.ParentId", 'C')]
2020-06-09 13:48:50 +02:00
public IActionResult PostCopy ( MoveOrCopy copy )
2018-06-29 19:52:40 +02:00
{
var toCopy = ValidateMoveOrCopy ( copy ) ;
2020-06-09 13:48:50 +02:00
var c = _contentService . Copy ( toCopy , copy . ParentId , copy . RelateToOriginal , copy . Recursive , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
2020-06-12 22:13:43 +02:00
return Content ( c . Path , MediaTypeNames . Text . Plain , Encoding . UTF8 ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// Unpublishes a node with a given Id and returns the unpublished entity
/// </summary>
2018-10-03 14:27:48 +02:00
/// <param name="model">The content and variants to unpublish</param>
2018-06-29 19:52:40 +02:00
/// <returns></returns>
2018-10-26 16:40:28 +11:00
[EnsureUserPermissionForContent("model.Id", 'Z')]
2020-06-09 13:48:50 +02:00
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
2018-10-03 14:27:48 +02:00
public ContentItemDisplay PostUnpublish ( UnpublishContent model )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var foundContent = GetObjectFromRequest ( ( ) = > _contentService . GetById ( model . Id ) ) ;
2018-06-29 19:52:40 +02:00
if ( foundContent = = null )
2018-10-03 14:27:48 +02:00
HandleContentNotFound ( model . Id ) ;
2018-07-05 17:14:11 +02:00
2018-10-03 14:27:48 +02:00
var languageCount = _allLangs . Value . Count ( ) ;
if ( model . Cultures . Length = = 0 | | model . Cultures . Length = = languageCount )
{
//this means that the entire content item will be unpublished
2020-06-09 13:48:50 +02:00
var unpublishResult = _contentService . Unpublish ( foundContent , userId : _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-06-29 19:52:40 +02:00
2018-10-03 14:27:48 +02:00
var content = MapToDisplay ( foundContent ) ;
2018-06-29 19:52:40 +02:00
2018-11-12 17:29:38 +11:00
if ( ! unpublishResult . Success )
2018-10-03 14:27:48 +02:00
{
AddCancelMessage ( content ) ;
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( content ) ;
2018-10-03 14:27:48 +02:00
}
else
{
content . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "content/unpublish" ) ,
_localizedTextService . Localize ( "speechBubbles/contentUnpublished" ) ) ;
2018-10-03 14:27:48 +02:00
return content ;
}
2018-06-29 19:52:40 +02:00
}
else
2018-05-08 00:37:41 +10:00
{
2018-10-03 14:27:48 +02:00
//we only want to unpublish some of the variants
2018-11-07 19:42:49 +11:00
var results = new Dictionary < string , PublishResult > ( ) ;
2018-11-12 17:29:38 +11:00
foreach ( var c in model . Cultures )
2018-10-03 14:27:48 +02:00
{
2020-06-09 13:48:50 +02:00
var result = _contentService . Unpublish ( foundContent , culture : c , userId : _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-10-03 14:27:48 +02:00
results [ c ] = result ;
2018-11-07 19:42:49 +11:00
if ( result . Result = = PublishResultType . SuccessUnpublishMandatoryCulture )
2018-10-03 14:27:48 +02:00
{
//if this happens, it means they are all unpublished, we don't need to continue
break ;
}
}
2018-06-29 19:52:40 +02:00
2018-10-03 14:27:48 +02:00
var content = MapToDisplay ( foundContent ) ;
2018-06-29 19:52:40 +02:00
2018-10-03 14:27:48 +02:00
//check for this status and return the correct message
2018-11-07 19:42:49 +11:00
if ( results . Any ( x = > x . Value . Result = = PublishResultType . SuccessUnpublishMandatoryCulture ) )
2018-10-03 14:27:48 +02:00
{
content . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "content/unpublish" ) ,
_localizedTextService . Localize ( "speechBubbles/contentMandatoryCultureUnpublished" ) ) ;
2018-10-03 14:27:48 +02:00
return content ;
}
//otherwise add a message for each one unpublished
foreach ( var r in results )
{
content . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "content/unpublish" ) ,
_localizedTextService . Localize ( "speechBubbles/contentCultureUnpublished" , new [ ] { _allLangs . Value [ r . Key ] . CultureName } ) ) ;
2018-10-03 14:27:48 +02:00
}
2018-06-29 19:52:40 +02:00
return content ;
2018-10-03 14:27:48 +02:00
2018-06-29 19:52:40 +02:00
}
2018-10-03 14:27:48 +02:00
2018-05-08 00:37:41 +10:00
}
2018-09-30 15:02:09 +01:00
public ContentDomainsAndCulture GetCultureAndDomains ( int id )
{
2020-06-09 13:48:50 +02:00
var nodeDomains = _domainService . GetAssignedDomains ( id , true ) . ToArray ( ) ;
2018-09-30 15:02:09 +01:00
var wildcard = nodeDomains . FirstOrDefault ( d = > d . IsWildcard ) ;
var domains = nodeDomains . Where ( d = > ! d . IsWildcard ) . Select ( d = > new DomainDisplay ( d . DomainName , d . LanguageId . GetValueOrDefault ( 0 ) ) ) ;
return new ContentDomainsAndCulture
{
Domains = domains ,
Language = wildcard = = null | | ! wildcard . LanguageId . HasValue ? "undefined" : wildcard . LanguageId . ToString ( )
} ;
}
2018-07-31 15:56:04 +10:00
[HttpPost]
2020-06-09 13:48:50 +02:00
public ActionResult < DomainSave > PostSaveLanguageAndDomains ( DomainSave model )
2018-07-31 15:56:04 +10:00
{
2020-02-12 13:47:48 +11:00
foreach ( var domain in model . Domains )
2019-02-24 09:37:22 +01:00
{
try
{
2020-06-09 13:48:50 +02:00
var uri = DomainUtilities . ParseUriFromDomainName ( domain . Name , new Uri ( Request . GetEncodedUrl ( ) , UriKind . RelativeOrAbsolute ) ) ;
2019-02-24 09:37:22 +01:00
}
catch ( UriFormatException )
2019-11-05 12:54:22 +01:00
{
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( _localizedTextService . Localize ( "assignDomain/invalidDomain" ) ) ;
2019-02-24 09:37:22 +01:00
}
}
2020-06-09 13:48:50 +02:00
var node = _contentService . GetById ( model . NodeId ) ;
2018-07-31 15:56:04 +10:00
if ( node = = null )
{
2020-06-09 13:48:50 +02:00
HttpContext . SetReasonPhrase ( "Node Not Found." ) ;
return NotFound ( "There is no content node with id {model.NodeId}." ) ;
2018-07-31 15:56:04 +10:00
}
2020-06-09 13:48:50 +02:00
var permission = _userService . GetPermissions ( _webSecurity . CurrentUser , node . Path ) ;
2018-07-31 15:56:04 +10:00
2020-01-06 14:59:17 +01:00
2018-10-29 17:27:33 +11:00
if ( permission . AssignedPermissions . Contains ( ActionAssignDomain . ActionLetter . ToString ( ) , StringComparer . Ordinal ) = = false )
2018-07-31 15:56:04 +10:00
{
2020-06-09 13:48:50 +02:00
HttpContext . SetReasonPhrase ( "Permission Denied." ) ;
return BadRequest ( "You do not have permission to assign domains on that node." ) ;
2018-07-31 15:56:04 +10:00
}
model . Valid = true ;
2020-06-09 13:48:50 +02:00
var domains = _domainService . GetAssignedDomains ( model . NodeId , true ) . ToArray ( ) ;
var languages = _localizationService . GetAllLanguages ( ) . ToArray ( ) ;
2018-07-31 15:56:04 +10:00
var language = model . Language > 0 ? languages . FirstOrDefault ( l = > l . Id = = model . Language ) : null ;
// process wildcard
if ( language ! = null )
{
// yet there is a race condition here...
var wildcard = domains . FirstOrDefault ( d = > d . IsWildcard ) ;
if ( wildcard ! = null )
{
wildcard . LanguageId = language . Id ;
}
else
{
wildcard = new UmbracoDomain ( "*" + model . NodeId )
{
LanguageId = model . Language ,
RootContentId = model . NodeId
} ;
}
2020-06-09 13:48:50 +02:00
var saveAttempt = _domainService . Save ( wildcard ) ;
2018-07-31 15:56:04 +10:00
if ( saveAttempt = = false )
{
2020-06-09 13:48:50 +02:00
HttpContext . SetReasonPhrase ( saveAttempt . Result . Result . ToString ( ) ) ;
return BadRequest ( "Saving domain failed" ) ;
2018-07-31 15:56:04 +10:00
}
}
else
{
var wildcard = domains . FirstOrDefault ( d = > d . IsWildcard ) ;
if ( wildcard ! = null )
{
2020-06-09 13:48:50 +02:00
_domainService . Delete ( wildcard ) ;
2018-07-31 15:56:04 +10:00
}
}
// process domains
// delete every (non-wildcard) domain, that exists in the DB yet is not in the model
foreach ( var domain in domains . Where ( d = > d . IsWildcard = = false & & model . Domains . All ( m = > m . Name . InvariantEquals ( d . DomainName ) = = false ) ) )
{
2020-06-09 13:48:50 +02:00
_domainService . Delete ( domain ) ;
2018-07-31 15:56:04 +10:00
}
2018-11-12 17:29:38 +11:00
2018-07-31 15:56:04 +10:00
var names = new List < string > ( ) ;
// create or update domains in the model
foreach ( var domainModel in model . Domains . Where ( m = > string . IsNullOrWhiteSpace ( m . Name ) = = false ) )
{
language = languages . FirstOrDefault ( l = > l . Id = = domainModel . Lang ) ;
if ( language = = null )
{
continue ;
}
var name = domainModel . Name . ToLowerInvariant ( ) ;
if ( names . Contains ( name ) )
{
domainModel . Duplicate = true ;
continue ;
}
names . Add ( name ) ;
var domain = domains . FirstOrDefault ( d = > d . DomainName . InvariantEquals ( domainModel . Name ) ) ;
if ( domain ! = null )
{
domain . LanguageId = language . Id ;
2020-06-09 13:48:50 +02:00
_domainService . Save ( domain ) ;
2018-07-31 15:56:04 +10:00
}
2020-06-09 13:48:50 +02:00
else if ( _domainService . Exists ( domainModel . Name ) )
2018-07-31 15:56:04 +10:00
{
domainModel . Duplicate = true ;
2020-06-09 13:48:50 +02:00
var xdomain = _domainService . GetByName ( domainModel . Name ) ;
2018-07-31 15:56:04 +10:00
var xrcid = xdomain . RootContentId ;
if ( xrcid . HasValue )
{
2020-06-09 13:48:50 +02:00
var xcontent = _contentService . GetById ( xrcid . Value ) ;
2018-07-31 15:56:04 +10:00
var xnames = new List < string > ( ) ;
while ( xcontent ! = null )
{
xnames . Add ( xcontent . Name ) ;
if ( xcontent . ParentId < - 1 )
xnames . Add ( "Recycle Bin" ) ;
2020-06-09 13:48:50 +02:00
xcontent = _contentService . GetParent ( xcontent ) ;
2018-07-31 15:56:04 +10:00
}
xnames . Reverse ( ) ;
domainModel . Other = "/" + string . Join ( "/" , xnames ) ;
}
}
else
{
// yet there is a race condition here...
var newDomain = new UmbracoDomain ( name )
{
LanguageId = domainModel . Lang ,
RootContentId = model . NodeId
} ;
2020-06-09 13:48:50 +02:00
var saveAttempt = _domainService . Save ( newDomain ) ;
2018-07-31 15:56:04 +10:00
if ( saveAttempt = = false )
{
2020-06-09 13:48:50 +02:00
HttpContext . SetReasonPhrase ( saveAttempt . Result . Result . ToString ( ) ) ;
return BadRequest ( "Saving new domain failed" ) ;
2018-07-31 15:56:04 +10:00
}
}
}
model . Valid = model . Domains . All ( m = > m . Duplicate = = false ) ;
return model ;
}
2018-08-06 16:50:40 +10:00
/// <summary>
2019-03-14 00:59:51 +11:00
/// Ensure there is culture specific errors in the result if any errors are for culture properties
/// and we're dealing with variant content, then call the base class HandleInvalidModelState
2018-08-06 16:50:40 +10:00
/// </summary>
/// <param name="display"></param>
/// <remarks>
/// This is required to wire up the validation in the save/publish dialog
/// </remarks>
2019-04-04 21:57:49 +11:00
private void HandleInvalidModelState ( ContentItemDisplay display , string cultureForInvariantErrors )
2018-08-06 16:50:40 +10:00
{
2019-03-14 00:59:51 +11:00
if ( ! ModelState . IsValid & & display . Variants . Count ( ) > 1 )
2018-08-06 16:50:40 +10:00
{
//Add any culture specific errors here
2020-05-14 17:03:33 +02:00
var variantErrors = ModelState . GetVariantsWithErrors ( cultureForInvariantErrors ) ;
2018-08-06 16:50:40 +10:00
2020-05-14 17:03:33 +02:00
foreach ( var ( culture , segment ) in variantErrors )
2018-08-06 16:50:40 +10:00
{
2020-05-14 17:03:33 +02:00
AddVariantValidationError ( culture , segment , "speechBubbles/contentCultureValidationError" ) ;
2018-08-06 16:50:40 +10:00
}
}
2018-11-12 17:29:38 +11:00
2018-08-06 16:50:40 +10:00
base . HandleInvalidModelState ( display ) ;
2019-03-14 00:59:51 +11:00
}
2019-11-05 12:54:22 +01:00
2018-06-29 19:52:40 +02:00
/// <summary>
2018-08-14 20:21:33 +10:00
/// Maps the dto property values and names to the persisted model
2018-06-29 19:52:40 +02:00
/// </summary>
2018-07-19 19:32:07 +10:00
/// <param name="contentSave"></param>
2018-08-14 20:21:33 +10:00
private void MapValuesForPersistence ( ContentItemSave contentSave )
2018-06-29 19:52:40 +02:00
{
2019-10-11 14:13:49 +02:00
// inline method to determine the culture and segment to persist the property
2020-01-21 10:18:03 +01:00
( string culture , string segment ) PropertyCultureAndSegment ( IProperty property , ContentVariantSave variant )
2019-10-11 14:13:49 +02:00
{
var culture = property . PropertyType . VariesByCulture ( ) ? variant . Culture : null ;
var segment = property . PropertyType . VariesBySegment ( ) ? variant . Segment : null ;
return ( culture , segment ) ;
2020-01-21 10:18:03 +01:00
}
2018-07-31 17:50:24 +10:00
2018-08-10 15:39:57 +10:00
var variantIndex = 0 ;
2019-01-10 07:58:45 +01:00
2018-07-31 17:50:24 +10:00
//loop through each variant, set the correct name and property values
foreach ( var variant in contentSave . Variants )
2018-06-29 19:52:40 +02:00
{
2018-08-06 18:05:04 +10:00
//Don't update anything for this variant if Save is not true
2019-01-10 10:06:42 +01:00
if ( ! variant . Save ) continue ;
2018-08-06 18:05:04 +10:00
2018-07-19 19:32:07 +10:00
//Don't update the name if it is empty
if ( ! variant . Name . IsNullOrWhiteSpace ( ) )
2018-06-29 19:52:40 +02:00
{
2018-07-19 19:32:07 +10:00
if ( contentSave . PersistedContent . ContentType . VariesByCulture ( ) )
{
if ( variant . Culture . IsNullOrWhiteSpace ( ) )
throw new InvalidOperationException ( $"Cannot set culture name without a culture." ) ;
contentSave . PersistedContent . SetCultureName ( variant . Name , variant . Culture ) ;
}
else
{
contentSave . PersistedContent . Name = variant . Name ;
}
2018-06-29 19:52:40 +02:00
}
2018-07-31 17:50:24 +10:00
2018-08-10 15:39:57 +10:00
//This is important! We only want to process invariant properties with the first variant, for any other variant
// we need to exclude invariant properties from being processed, otherwise they will be double processed for the
// same value which can cause some problems with things such as file uploads.
var propertyCollection = variantIndex = = 0
? variant . PropertyCollectionDto
: new ContentPropertyCollectionDto
{
2020-02-10 09:16:03 +01:00
Properties = variant . PropertyCollectionDto . Properties . Where (
x = > ! x . Culture . IsNullOrWhiteSpace ( ) | | ! x . Segment . IsNullOrWhiteSpace ( ) )
2018-08-10 15:39:57 +10:00
} ;
2018-07-31 17:50:24 +10:00
//for each variant, map the property values
2018-08-02 15:12:26 +10:00
MapPropertyValuesForPersistence < IContent , ContentItemSave > (
2018-07-31 17:50:24 +10:00
contentSave ,
2018-08-10 15:39:57 +10:00
propertyCollection ,
2019-10-11 14:13:49 +02:00
( save , property ) = >
{
// Get property value
2020-01-21 10:18:03 +01:00
( var culture , var segment ) = PropertyCultureAndSegment ( property , variant ) ;
return property . GetValue ( culture , segment ) ;
2019-10-11 14:13:49 +02:00
} ,
( save , property , v ) = >
{
// Set property value
( var culture , var segment ) = PropertyCultureAndSegment ( property , variant ) ;
2020-01-21 10:18:03 +01:00
property . SetValue ( v , culture , segment ) ;
} ,
2018-11-20 13:24:06 +01:00
variant . Culture ) ;
2018-08-10 15:39:57 +10:00
variantIndex + + ;
2018-06-29 19:52:40 +02:00
}
2018-11-12 17:29:38 +11:00
2019-01-10 18:31:13 +01:00
// handle template
if ( string . IsNullOrWhiteSpace ( contentSave . TemplateAlias ) ) // cleared: clear if not already null
{
if ( contentSave . PersistedContent . TemplateId ! = null )
{
contentSave . PersistedContent . TemplateId = null ;
}
}
else // set: update if different
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var template = _fileService . GetTemplate ( contentSave . TemplateAlias ) ;
2019-01-10 18:31:13 +01:00
if ( template = = null )
2018-06-29 19:52:40 +02:00
{
2019-01-10 18:31:13 +01:00
//ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias);
Logger . Warn < ContentController > ( "No template exists with the specified alias: {TemplateAlias}" , contentSave . TemplateAlias ) ;
}
else if ( template . Id ! = contentSave . PersistedContent . TemplateId )
{
contentSave . PersistedContent . TemplateId = template . Id ;
2018-06-29 19:52:40 +02: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 )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
2020-06-09 13:48:50 +02:00
var contentService = _contentService ;
2018-06-29 19:52:40 +02:00
var toMove = contentService . GetById ( model . Id ) ;
if ( toMove = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
if ( model . ParentId < 0 )
{
2019-10-15 17:57:06 +02:00
//cannot move if the content item is not allowed at the root
if ( toMove . ContentType . AllowedAsRoot = = false )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateNotificationValidationErrorResponse (
_localizedTextService . Localize ( "moveOrCopy/notAllowedAtRoot" ) ) ;
2018-06-29 19:52:40 +02:00
}
}
else
{
var parent = contentService . GetById ( model . ParentId ) ;
if ( parent = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
2020-06-09 13:48:50 +02:00
var parentContentType = _contentTypeService . Get ( parent . ContentTypeId ) ;
2018-06-29 19:52:40 +02:00
//check if the item is allowed under this one
2019-01-03 09:27:52 +01:00
if ( parentContentType . AllowedContentTypes . Select ( x = > x . Id ) . ToArray ( )
2018-06-29 19:52:40 +02:00
. Any ( x = > x . Value = = toMove . ContentType . Id ) = = false )
{
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateNotificationValidationErrorResponse (
_localizedTextService . Localize ( "moveOrCopy/notAllowedByContentType" ) ) ;
2018-06-29 19:52:40 +02:00
}
// Check on paths
if ( ( string . Format ( ",{0}," , parent . Path ) ) . IndexOf ( string . Format ( ",{0}," , toMove . Id ) , StringComparison . Ordinal ) > - 1 )
{
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateNotificationValidationErrorResponse (
_localizedTextService . Localize ( "moveOrCopy/notAllowedByPath" ) ) ;
2018-06-29 19:52:40 +02:00
}
}
return toMove ;
}
2018-09-04 01:13:26 +10:00
/// <summary>
/// Adds notification messages to the outbound display model for a given published status
/// </summary>
2019-03-27 12:41:02 +11:00
/// <param name="statuses"></param>
2018-09-04 01:13:26 +10:00
/// <param name="display"></param>
/// <param name="successfulCultures">
2019-01-26 10:52:19 -05:00
/// This is null when dealing with invariant content, else it's the cultures that were successfully published
2018-09-04 01:13:26 +10:00
/// </param>
2019-03-27 12:41:02 +11:00
private void AddMessageForPublishStatus ( IReadOnlyCollection < PublishResult > statuses , INotificationModel display , string [ ] successfulCultures = null )
2018-06-29 19:52:40 +02:00
{
2018-11-15 17:20:00 +11:00
var totalStatusCount = statuses . Count ( ) ;
2018-11-15 15:24:09 +11:00
//Put the statuses into groups, each group results in a different message
var statusGroup = statuses . GroupBy ( x = >
2018-06-29 19:52:40 +02:00
{
2018-11-15 15:24:09 +11:00
switch ( x . Result )
{
case PublishResultType . SuccessPublish :
case PublishResultType . SuccessPublishCulture :
2018-11-15 17:20:00 +11:00
//these 2 belong to a single group
2018-12-07 13:44:41 +01:00
return PublishResultType . SuccessPublish ;
2018-11-15 15:24:09 +11:00
case PublishResultType . FailedPublishAwaitingRelease :
case PublishResultType . FailedPublishCultureAwaitingRelease :
//these 2 belong to a single group
return PublishResultType . FailedPublishAwaitingRelease ;
case PublishResultType . FailedPublishHasExpired :
case PublishResultType . FailedPublishCultureHasExpired :
//these 2 belong to a single group
return PublishResultType . FailedPublishHasExpired ;
2018-11-15 17:20:00 +11:00
case PublishResultType . SuccessPublishAlready :
2018-11-15 15:24:09 +11:00
case PublishResultType . FailedPublishPathNotPublished :
case PublishResultType . FailedPublishCancelledByEvent :
case PublishResultType . FailedPublishIsTrashed :
case PublishResultType . FailedPublishContentInvalid :
case PublishResultType . FailedPublishMandatoryCultureMissing :
//the rest that we are looking for each belong in their own group
return x . Result ;
default :
throw new IndexOutOfRangeException ( $"{x.Result}\" was not expected . ");
}
} ) ;
foreach ( var status in statusGroup )
2018-12-07 13:44:41 +01:00
{
2018-11-15 15:24:09 +11:00
switch ( status . Key )
{
2018-11-15 17:20:00 +11:00
case PublishResultType . SuccessPublishAlready :
2018-09-04 01:13:26 +10:00
{
2019-01-27 01:17:32 -05:00
// TODO: Here we should have messaging for when there are release dates specified like https://github.com/umbraco/Umbraco-CMS/pull/3507
2018-12-07 13:13:00 +11:00
// but this will take a bit of effort because we need to deal with variants, different messaging, etc... A quick attempt was made here:
// http://github.com/umbraco/Umbraco-CMS/commit/9b3de7b655e07c612c824699b48a533c0448131a
2018-11-15 17:20:00 +11:00
//special case, we will only show messages for this if:
// * it's not a bulk publish operation
// * it's a bulk publish operation and all successful statuses are this one
var itemCount = status . Count ( ) ;
if ( totalStatusCount = = 1 | | totalStatusCount = = itemCount )
{
if ( successfulCultures = = null | | totalStatusCount = = itemCount )
{
//either invariant single publish, or bulk publish where all statuses are already published
display . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentPublishedHeader" ) ,
_localizedTextService . Localize ( "speechBubbles/editContentPublishedText" ) ) ;
2018-11-15 17:20:00 +11:00
}
else
{
foreach ( var c in successfulCultures )
{
display . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentPublishedHeader" ) ,
_localizedTextService . Localize ( "speechBubbles/editVariantPublishedText" , new [ ] { _allLangs . Value [ c ] . CultureName } ) ) ;
2018-11-15 17:20:00 +11:00
}
}
}
2018-09-04 01:13:26 +10:00
}
2018-11-15 17:20:00 +11:00
break ;
case PublishResultType . SuccessPublish :
2018-11-15 15:24:09 +11:00
{
2019-01-27 01:17:32 -05:00
// TODO: Here we should have messaging for when there are release dates specified like https://github.com/umbraco/Umbraco-CMS/pull/3507
2018-12-07 13:13:00 +11:00
// but this will take a bit of effort because we need to deal with variants, different messaging, etc... A quick attempt was made here:
// http://github.com/umbraco/Umbraco-CMS/commit/9b3de7b655e07c612c824699b48a533c0448131a
2018-11-15 17:20:00 +11:00
var itemCount = status . Count ( ) ;
if ( successfulCultures = = null )
2018-11-15 15:24:09 +11:00
{
display . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentPublishedHeader" ) ,
2018-11-15 17:20:00 +11:00
totalStatusCount > 1
2020-06-09 13:48:50 +02:00
? _localizedTextService . Localize ( "speechBubbles/editMultiContentPublishedText" , new [ ] { itemCount . ToInvariantString ( ) } )
: _localizedTextService . Localize ( "speechBubbles/editContentPublishedText" ) ) ;
2018-11-15 17:20:00 +11:00
}
else
{
foreach ( var c in successfulCultures )
{
display . AddSuccessNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/editContentPublishedHeader" ) ,
2018-11-15 17:20:00 +11:00
totalStatusCount > 1
2020-06-09 13:48:50 +02:00
? _localizedTextService . Localize ( "speechBubbles/editMultiVariantPublishedText" , new [ ] { itemCount . ToInvariantString ( ) , _allLangs . Value [ c ] . CultureName } )
: _localizedTextService . Localize ( "speechBubbles/editVariantPublishedText" , new [ ] { _allLangs . Value [ c ] . CultureName } ) ) ;
2018-11-15 17:20:00 +11:00
}
2018-11-15 15:24:09 +11:00
}
}
break ;
case PublishResultType . FailedPublishPathNotPublished :
{
2019-03-27 16:31:53 +11:00
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
2018-11-15 15:24:09 +11:00
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedByParent" ,
2018-11-15 15:24:09 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
break ;
case PublishResultType . FailedPublishCancelledByEvent :
{
2019-03-27 16:31:53 +11:00
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
2018-11-15 15:24:09 +11:00
AddCancelMessage ( display , message : "publish/contentPublishedFailedByEvent" , messageParams : new [ ] { names } ) ;
}
break ;
case PublishResultType . FailedPublishAwaitingRelease :
{
2019-03-27 16:31:53 +11:00
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
2018-11-15 15:24:09 +11:00
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedAwaitingRelease" ,
2018-11-15 15:24:09 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
break ;
case PublishResultType . FailedPublishHasExpired :
{
2019-03-27 16:31:53 +11:00
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
2018-11-15 15:24:09 +11:00
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedExpired" ,
2018-11-15 15:24:09 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
break ;
case PublishResultType . FailedPublishIsTrashed :
{
2019-03-27 16:31:53 +11:00
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
2018-11-15 15:24:09 +11:00
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedIsTrashed" ,
2018-11-15 15:24:09 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
break ;
case PublishResultType . FailedPublishContentInvalid :
{
2019-03-27 16:31:53 +11:00
if ( successfulCultures = = null )
{
var names = string . Join ( ", " , status . Select ( x = > $"'{x.Content.Name}'" ) ) ;
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedInvalid" ,
2019-03-27 16:31:53 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
else
{
foreach ( var c in successfulCultures )
{
2019-06-07 14:20:44 +10:00
var names = string . Join ( ", " , status . Select ( x = > $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'" ) ) ;
2019-03-27 16:31:53 +11:00
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
_localizedTextService . Localize ( "publish/contentPublishedFailedInvalid" ,
2019-03-27 16:31:53 +11:00
new [ ] { names } ) . Trim ( ) ) ;
}
}
2018-11-15 15:24:09 +11:00
}
break ;
case PublishResultType . FailedPublishMandatoryCultureMissing :
display . AddWarningNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "publish" ) ,
2019-01-21 15:57:48 +01:00
"publish/contentPublishedFailedByCulture" ) ;
2018-11-15 15:24:09 +11:00
break ;
default :
throw new IndexOutOfRangeException ( $"PublishedResultType \" { status . Key } \ " was not expected." ) ;
2018-06-29 19:52:40 +02: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>
/// <returns></returns>
2018-07-13 12:45:04 +10:00
private ContentItemDisplay MapToDisplay ( IContent content )
2018-06-29 19:52:40 +02:00
{
2020-06-09 13:48:50 +02:00
var display = _umbracoMapper . Map < ContentItemDisplay > ( content , context = >
2020-02-12 13:47:48 +11:00
{
2020-06-09 13:48:50 +02:00
context . Items [ "CurrentUser" ] = _webSecurity . CurrentUser ;
2020-02-12 13:47:48 +11:00
} ) ;
2019-02-14 13:13:19 +01:00
display . AllowPreview = display . AllowPreview & & content . Trashed = = false & & content . ContentType . IsElement = = false ;
2018-06-29 19:52:40 +02:00
return display ;
2018-05-08 00:37:41 +10:00
}
2018-11-12 17:29:38 +11:00
2019-01-27 13:47:22 +01:00
[EnsureUserPermissionForContent("contentId", ActionBrowse.ActionLetter)]
2020-06-09 13:48:50 +02:00
public ActionResult < IEnumerable < NotifySetting > > GetNotificationOptions ( int contentId )
2018-08-07 15:48:21 +01:00
{
var notifications = new List < NotifySetting > ( ) ;
2020-06-09 13:48:50 +02:00
if ( contentId < = 0 ) return NotFound ( ) ;
2018-08-07 15:48:21 +01:00
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( contentId ) ;
if ( content = = null ) return NotFound ( ) ;
2018-08-07 15:48:21 +01:00
2020-06-09 13:48:50 +02:00
var userNotifications = _notificationService . GetUserNotifications ( _webSecurity . CurrentUser , content . Path ) . ToList ( ) ;
2018-08-07 15:48:21 +01:00
2020-06-09 13:48:50 +02:00
foreach ( var a in _actionCollection . Where ( x = > x . ShowInNotifier ) )
2018-08-07 15:48:21 +01:00
{
var n = new NotifySetting
{
2020-06-09 13:48:50 +02:00
Name = _localizedTextService . Localize ( "actions" , a . Alias ) ,
2018-11-12 17:29:38 +11:00
Checked = userNotifications . FirstOrDefault ( x = > x . Action = = a . Letter . ToString ( ) ) ! = null ,
2018-08-07 15:48:21 +01:00
NotifyCode = a . Letter . ToString ( )
} ;
notifications . Add ( n ) ;
}
return notifications ;
}
2020-06-09 13:48:50 +02:00
public IActionResult PostNotificationOptions ( int contentId , [ FromQuery ] string [ ] notifyOptions )
2018-08-07 15:48:21 +01:00
{
2020-06-09 13:48:50 +02:00
if ( contentId < = 0 ) return NotFound ( ) ;
var content = _contentService . GetById ( contentId ) ;
if ( content = = null ) return NotFound ( ) ;
_notificationService . SetNotifications ( _webSecurity . CurrentUser , content , notifyOptions ) ;
2018-08-07 15:48:21 +01:00
2020-06-09 13:48:50 +02:00
return NoContent ( ) ;
2018-08-07 15:48:21 +01:00
}
2018-10-17 12:15:57 +01:00
[HttpGet]
2018-10-17 14:39:11 +01:00
public IEnumerable < RollbackVersion > GetRollbackVersions ( int contentId , string culture = null )
2018-10-17 12:15:57 +01:00
{
var rollbackVersions = new List < RollbackVersion > ( ) ;
2018-10-22 08:45:30 +02:00
var writerIds = new HashSet < int > ( ) ;
2018-10-17 12:15:57 +01:00
2020-06-09 13:48:50 +02:00
var versions = _contentService . GetVersionsSlim ( contentId , 0 , 50 ) ;
2018-10-18 13:13:11 +01:00
2018-10-17 12:15:57 +01:00
//Not all nodes are variants & thus culture can be null
2018-10-18 13:13:11 +01:00
if ( culture ! = null )
{
2018-10-22 08:45:30 +02:00
//Get cultures that were published with the version = their update date is equal to the version's
2018-10-18 13:13:11 +01:00
versions = versions . Where ( x = > x . UpdateDate = = x . GetUpdateDate ( culture ) ) ;
}
2018-10-17 12:15:57 +01:00
2018-10-18 14:24:32 +01:00
//First item is our current item/state (cant rollback to ourselves)
versions = versions . Skip ( 1 ) ;
2018-10-18 13:13:11 +01:00
foreach ( var version in versions )
2018-10-17 12:15:57 +01:00
{
2018-10-22 08:45:30 +02:00
var rollbackVersion = new RollbackVersion
{
VersionId = version . VersionId ,
VersionDate = version . UpdateDate ,
VersionAuthorId = version . WriterId
} ;
2018-10-17 12:15:57 +01:00
2018-10-22 08:45:30 +02:00
rollbackVersions . Add ( rollbackVersion ) ;
2018-10-17 12:15:57 +01:00
2018-10-22 08:45:30 +02:00
writerIds . Add ( version . WriterId ) ;
}
2018-10-17 12:15:57 +01:00
2020-06-09 13:48:50 +02:00
var users = _userService
2018-10-22 08:45:30 +02:00
. GetUsersById ( writerIds . ToArray ( ) )
. ToDictionary ( x = > x . Id , x = > x . Name ) ;
foreach ( var rollbackVersion in rollbackVersions )
{
if ( users . TryGetValue ( rollbackVersion . VersionAuthorId , out var userName ) )
rollbackVersion . VersionAuthorName = userName ;
2018-10-17 12:15:57 +01:00
}
2018-11-12 17:29:38 +11:00
2018-10-17 12:15:57 +01:00
return rollbackVersions ;
}
2018-10-17 14:39:11 +01:00
[HttpGet]
2018-10-17 15:09:29 +01:00
public ContentVariantDisplay GetRollbackVersion ( int versionId , string culture = null )
2018-10-17 14:39:11 +01:00
{
2020-06-09 13:48:50 +02:00
var version = _contentService . GetVersion ( versionId ) ;
2018-10-17 14:39:11 +01:00
var content = MapToDisplay ( version ) ;
2018-11-12 17:29:38 +11:00
return culture = = null
? content . Variants . FirstOrDefault ( ) //No culture set - so this is an invariant node - so just list me the first item in here
2018-10-18 14:24:32 +01:00
: content . Variants . FirstOrDefault ( x = > x . Language . IsoCode = = culture ) ;
2018-10-17 14:39:11 +01:00
}
2018-10-30 00:34:43 +11:00
[EnsureUserPermissionForContent("contentId", ActionRollback.ActionLetter)]
2018-10-17 12:15:57 +01:00
[HttpPost]
2020-06-09 13:48:50 +02:00
public IActionResult PostRollbackContent ( int contentId , int versionId , string culture = "*" )
2018-10-17 12:15:57 +01:00
{
2020-06-09 13:48:50 +02:00
var rollbackResult = _contentService . Rollback ( contentId , versionId , culture , _webSecurity . GetUserId ( ) . ResultOr ( 0 ) ) ;
2018-10-18 13:13:11 +01:00
2018-11-12 17:29:38 +11:00
if ( rollbackResult . Success )
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-10-17 14:39:11 +01:00
2018-10-18 14:24:32 +01:00
var notificationModel = new SimpleNotificationModel ( ) ;
2018-10-18 13:13:11 +01:00
2018-10-18 14:24:32 +01:00
switch ( rollbackResult . Result )
2018-10-17 14:39:11 +01:00
{
2018-10-18 14:24:32 +01:00
case OperationResultType . Failed :
case OperationResultType . FailedCannot :
case OperationResultType . FailedExceptionThrown :
case OperationResultType . NoOperation :
default :
notificationModel . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/operationFailedHeader" ) ,
2019-01-27 01:17:32 -05:00
null ) ; // TODO: There is no specific failed to save error message AFAIK
2018-10-18 14:24:32 +01:00
break ;
case OperationResultType . FailedCancelledByEvent :
notificationModel . AddErrorNotification (
2020-06-09 13:48:50 +02:00
_localizedTextService . Localize ( "speechBubbles/operationCancelledHeader" ) ,
_localizedTextService . Localize ( "speechBubbles/operationCancelledText" ) ) ;
2018-10-18 14:24:32 +01:00
break ;
2018-10-17 14:39:11 +01:00
}
2020-06-09 13:48:50 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( notificationModel ) ;
2018-10-17 14:39:11 +01:00
}
2018-11-24 19:39:15 +01:00
[EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)]
[HttpGet]
2020-06-09 13:48:50 +02:00
public IActionResult GetPublicAccess ( int contentId )
2018-11-24 19:39:15 +01:00
{
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( contentId ) ;
2018-11-24 19:39:15 +01:00
if ( content = = null )
{
2020-06-09 13:48:50 +02:00
return NotFound ( ) ;
2018-11-24 19:39:15 +01:00
}
2020-06-09 13:48:50 +02:00
var entry = _publicAccessService . GetEntryForContent ( content ) ;
2019-08-13 21:14:28 +02:00
if ( entry = = null | | entry . ProtectedNodeId ! = content . Id )
2018-11-24 19:39:15 +01:00
{
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-11-24 19:39:15 +01:00
}
2020-06-09 13:48:50 +02:00
var loginPageEntity = _entityService . Get ( entry . LoginNodeId , UmbracoObjectTypes . Document ) ;
var errorPageEntity = _entityService . Get ( entry . NoAccessNodeId , UmbracoObjectTypes . Document ) ;
2018-11-24 19:39:15 +01:00
// unwrap the current public access setup for the client
// - this API method is the single point of entry for both "modes" of public access (single user and role based)
2018-12-12 09:55:35 +01:00
var usernames = entry . Rules
2018-12-09 14:58:52 +01:00
. Where ( rule = > rule . RuleType = = Constants . Conventions . PublicAccess . MemberUsernameRuleType )
2018-12-12 09:55:35 +01:00
. Select ( rule = > rule . RuleValue ) . ToArray ( ) ;
2019-11-25 21:20:00 +11:00
var members = usernames
2020-06-09 13:48:50 +02:00
. Select ( username = > _memberService . GetByUsername ( username ) )
2019-11-25 21:20:00 +11:00
. Where ( member = > member ! = null )
2020-06-09 13:48:50 +02:00
. Select ( _umbracoMapper . Map < MemberDisplay > )
2019-11-25 21:20:00 +11:00
. ToArray ( ) ;
2018-12-12 09:55:35 +01:00
2020-06-09 13:48:50 +02:00
var allGroups = _memberGroupService . GetAll ( ) . ToArray ( ) ;
2018-12-12 14:27:20 +01:00
var groups = entry . Rules
2018-11-24 19:39:15 +01:00
. Where ( rule = > rule . RuleType = = Constants . Conventions . PublicAccess . MemberRoleRuleType )
2018-12-12 14:27:20 +01:00
. Select ( rule = > allGroups . FirstOrDefault ( g = > g . Name = = rule . RuleValue ) )
. Where ( memberGroup = > memberGroup ! = null )
2020-06-09 13:48:50 +02:00
. Select ( _umbracoMapper . Map < MemberGroupDisplay > )
2018-11-24 19:39:15 +01:00
. ToArray ( ) ;
2020-06-09 13:48:50 +02:00
return Ok ( new PublicAccess
2018-11-24 19:39:15 +01:00
{
2018-12-09 14:58:52 +01:00
Members = members ,
2018-12-12 14:27:20 +01:00
Groups = groups ,
2020-06-09 13:48:50 +02:00
LoginPage = loginPageEntity ! = null ? _umbracoMapper . Map < EntityBasic > ( loginPageEntity ) : null ,
ErrorPage = errorPageEntity ! = null ? _umbracoMapper . Map < EntityBasic > ( errorPageEntity ) : null
2018-11-24 19:39:15 +01:00
} ) ;
}
// set up public access using role based access
[EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)]
[HttpPost]
2020-06-09 13:48:50 +02:00
public IActionResult PostPublicAccess ( int contentId , [ FromQuery ] string [ ] groups , [ FromQuery ] string [ ] usernames , int loginPageId , int errorPageId )
2018-11-24 19:39:15 +01:00
{
2018-12-12 14:27:20 +01:00
if ( ( groups = = null | | groups . Any ( ) = = false ) & & ( usernames = = null | | usernames . Any ( ) = = false ) )
2018-11-24 19:39:15 +01:00
{
2020-06-09 13:48:50 +02:00
return BadRequest ( ) ;
2018-11-24 19:39:15 +01:00
}
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( contentId ) ;
var loginPage = _contentService . GetById ( loginPageId ) ;
var errorPage = _contentService . GetById ( errorPageId ) ;
2018-11-24 19:39:15 +01:00
if ( content = = null | | loginPage = = null | | errorPage = = null )
{
2020-06-09 13:48:50 +02:00
return BadRequest ( ) ;
2018-11-24 19:39:15 +01:00
}
2018-12-12 14:27:20 +01:00
var isGroupBased = groups ! = null & & groups . Any ( ) ;
var candidateRuleValues = isGroupBased
? groups
2018-12-12 09:27:03 +01:00
: usernames ;
2018-12-12 14:27:20 +01:00
var newRuleType = isGroupBased
2018-12-09 14:58:52 +01:00
? Constants . Conventions . PublicAccess . MemberRoleRuleType
: Constants . Conventions . PublicAccess . MemberUsernameRuleType ;
2020-06-09 13:48:50 +02:00
var entry = _publicAccessService . GetEntryForContent ( content ) ;
2018-11-24 19:39:15 +01:00
2019-08-13 21:14:28 +02:00
if ( entry = = null | | entry . ProtectedNodeId ! = content . Id )
2018-11-24 19:39:15 +01:00
{
entry = new PublicAccessEntry ( content , loginPage , errorPage , new List < PublicAccessRule > ( ) ) ;
2018-12-09 14:58:52 +01:00
2018-12-12 14:27:20 +01:00
foreach ( var ruleValue in candidateRuleValues )
2018-11-24 19:39:15 +01:00
{
2018-12-12 14:27:20 +01:00
entry . AddRule ( ruleValue , newRuleType ) ;
2018-11-24 19:39:15 +01:00
}
}
else
{
entry . LoginNodeId = loginPage . Id ;
entry . NoAccessNodeId = errorPage . Id ;
var currentRules = entry . Rules . ToArray ( ) ;
var obsoleteRules = currentRules . Where ( rule = >
2018-12-09 14:58:52 +01:00
rule . RuleType ! = newRuleType
| | candidateRuleValues . Contains ( rule . RuleValue ) = = false
2018-11-24 19:39:15 +01:00
) ;
2018-12-09 14:58:52 +01:00
var newRuleValues = candidateRuleValues . Where ( group = >
2018-11-24 19:39:15 +01:00
currentRules . Any ( rule = >
2018-12-09 14:58:52 +01:00
rule . RuleType = = newRuleType
2018-11-24 19:39:15 +01:00
& & rule . RuleValue = = group
) = = false
) ;
foreach ( var rule in obsoleteRules )
{
entry . RemoveRule ( rule ) ;
}
2018-12-12 14:27:20 +01:00
foreach ( var ruleValue in newRuleValues )
2018-11-24 19:39:15 +01:00
{
2018-12-12 14:27:20 +01:00
entry . AddRule ( ruleValue , newRuleType ) ;
2018-11-24 19:39:15 +01:00
}
}
2020-06-09 13:48:50 +02:00
return _publicAccessService . Save ( entry ) . Success
? ( IActionResult ) Ok ( )
: Problem ( ) ;
2018-11-24 19:39:15 +01:00
}
2018-11-25 09:17:04 +01:00
[EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)]
[HttpPost]
2020-06-09 13:48:50 +02:00
public IActionResult RemovePublicAccess ( int contentId )
2018-11-25 09:17:04 +01:00
{
2020-06-09 13:48:50 +02:00
var content = _contentService . GetById ( contentId ) ;
2018-11-25 09:17:04 +01:00
if ( content = = null )
{
2020-06-09 13:48:50 +02:00
return NotFound ( ) ;
2018-11-25 09:17:04 +01:00
}
2020-06-09 13:48:50 +02:00
var entry = _publicAccessService . GetEntryForContent ( content ) ;
2018-11-25 09:17:04 +01:00
if ( entry = = null )
{
2020-06-09 13:48:50 +02:00
return Ok ( ) ;
2018-11-25 09:17:04 +01:00
}
2020-06-09 13:48:50 +02:00
return _publicAccessService . Delete ( entry ) . Success
? ( IActionResult ) Ok ( )
: Problem ( ) ;
2018-11-25 09:17:04 +01:00
}
2018-06-29 19:52:40 +02:00
}
}