2017-12-07 16:45:25 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging ;
2017-12-07 16:45:25 +01:00
using NPoco ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Cache ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Entities ;
using Umbraco.Cms.Core.Models.Membership ;
using Umbraco.Cms.Core.Persistence ;
using Umbraco.Cms.Core.Persistence.Querying ;
using Umbraco.Cms.Core.Persistence.Repositories ;
using Umbraco.Cms.Core.PropertyEditors ;
using Umbraco.Cms.Core.Serialization ;
using Umbraco.Cms.Core.Services ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Cache ;
using Umbraco.Core.Models ;
2017-12-28 09:06:33 +01:00
using Umbraco.Core.Persistence.Dtos ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Persistence.Factories ;
using Umbraco.Core.Persistence.Querying ;
using Umbraco.Core.Persistence.SqlSyntax ;
2019-12-10 08:37:19 +01:00
using Umbraco.Core.PropertyEditors ;
2017-12-12 15:04:13 +01:00
using Umbraco.Core.Scoping ;
2020-11-17 20:27:10 +01:00
using Umbraco.Core.Serialization ;
2018-09-18 11:53:33 +02:00
using Umbraco.Core.Services ;
2017-12-07 16:45:25 +01:00
namespace Umbraco.Core.Persistence.Repositories.Implement
{
/// <summary>
/// Represents a repository for doing CRUD operations for <see cref="IContent"/>.
/// </summary>
2019-12-17 16:30:26 +01:00
public class DocumentRepository : ContentRepositoryBase < int , IContent , DocumentRepository > , IDocumentRepository
2017-12-07 16:45:25 +01:00
{
private readonly IContentTypeRepository _contentTypeRepository ;
private readonly ITemplateRepository _templateRepository ;
private readonly ITagRepository _tagRepository ;
2020-11-17 20:27:10 +01:00
private readonly IJsonSerializer _serializer ;
2019-01-17 08:34:29 +01:00
private readonly AppCaches _appCaches ;
2020-09-17 09:42:55 +02:00
private readonly ILoggerFactory _loggerFactory ;
2017-12-07 16:45:25 +01:00
private PermissionRepository < IContent > _permissionRepository ;
private readonly ContentByGuidReadRepository _contentByGuidReadRepository ;
2017-12-14 17:04:44 +01:00
private readonly IScopeAccessor _scopeAccessor ;
2017-12-07 16:45:25 +01:00
2019-10-23 19:08:03 +11:00
/// <summary>
/// Constructor
/// </summary>
/// <param name="scopeAccessor"></param>
/// <param name="appCaches"></param>
/// <param name="logger"></param>
2020-09-17 09:42:55 +02:00
/// <param name="loggerFactory"></param>
2019-10-23 19:08:03 +11:00
/// <param name="contentTypeRepository"></param>
/// <param name="templateRepository"></param>
/// <param name="tagRepository"></param>
/// <param name="languageRepository"></param>
/// <param name="propertyEditors">
/// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
/// </param>
2019-12-10 08:37:19 +01:00
public DocumentRepository (
IScopeAccessor scopeAccessor ,
AppCaches appCaches ,
2020-09-17 09:42:55 +02:00
ILogger < DocumentRepository > logger ,
ILoggerFactory loggerFactory ,
2019-12-10 08:37:19 +01:00
IContentTypeRepository contentTypeRepository ,
ITemplateRepository templateRepository ,
ITagRepository tagRepository ,
ILanguageRepository languageRepository ,
2019-12-11 08:13:51 +01:00
IRelationRepository relationRepository ,
IRelationTypeRepository relationTypeRepository ,
Lazy < PropertyEditorCollection > propertyEditors ,
DataValueReferenceFactoryCollection dataValueReferenceFactories ,
2020-11-17 20:27:10 +01:00
IDataTypeService dataTypeService ,
IJsonSerializer serializer )
2019-12-11 08:13:51 +01:00
: base ( scopeAccessor , appCaches , logger , languageRepository , relationRepository , relationTypeRepository , propertyEditors , dataValueReferenceFactories , dataTypeService )
2017-12-07 16:45:25 +01:00
{
_contentTypeRepository = contentTypeRepository ? ? throw new ArgumentNullException ( nameof ( contentTypeRepository ) ) ;
_templateRepository = templateRepository ? ? throw new ArgumentNullException ( nameof ( templateRepository ) ) ;
_tagRepository = tagRepository ? ? throw new ArgumentNullException ( nameof ( tagRepository ) ) ;
2020-11-17 20:27:10 +01:00
_serializer = serializer ;
2019-01-17 08:34:29 +01:00
_appCaches = appCaches ;
2020-09-17 09:42:55 +02:00
_loggerFactory = loggerFactory ;
2017-12-14 17:04:44 +01:00
_scopeAccessor = scopeAccessor ;
2020-09-17 09:42:55 +02:00
_contentByGuidReadRepository = new ContentByGuidReadRepository ( this , scopeAccessor , appCaches , loggerFactory . CreateLogger < ContentByGuidReadRepository > ( ) ) ;
2017-12-07 16:45:25 +01:00
}
protected override DocumentRepository This = > this ;
2019-01-30 23:42:25 +11:00
/// <summary>
/// Default is to always ensure all documents have unique names
/// </summary>
protected virtual bool EnsureUniqueNaming { get ; } = true ;
2017-12-07 16:45:25 +01:00
// note: is ok to 'new' the repo here as it's a sub-repo really
private PermissionRepository < IContent > PermissionRepository = > _permissionRepository
2020-09-17 09:42:55 +02:00
? ? ( _permissionRepository = new PermissionRepository < IContent > ( _scopeAccessor , _appCaches , _loggerFactory . CreateLogger < PermissionRepository < IContent > > ( ) ) ) ;
2017-12-07 16:45:25 +01:00
#region Repository Base
2021-02-09 10:22:42 +01:00
protected override Guid NodeObjectTypeId = > Cms . Core . Constants . ObjectTypes . Document ;
2017-12-07 16:45:25 +01:00
protected override IContent PerformGet ( int id )
{
var sql = GetBaseQuery ( QueryType . Single )
. Where < NodeDto > ( x = > x . NodeId = = id )
. SelectTop ( 1 ) ;
var dto = Database . Fetch < DocumentDto > ( sql ) . FirstOrDefault ( ) ;
return dto = = null
? null
: MapDtoToContent ( dto ) ;
}
protected override IEnumerable < IContent > PerformGetAll ( params int [ ] ids )
{
var sql = GetBaseQuery ( QueryType . Many ) ;
if ( ids . Any ( ) )
sql . WhereIn < NodeDto > ( x = > x . NodeId , ids ) ;
2020-01-07 14:53:17 +11:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) ) ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < IContent > PerformGetByQuery ( IQuery < IContent > query )
{
var sqlClause = GetBaseQuery ( QueryType . Many ) ;
var translator = new SqlTranslator < IContent > ( sqlClause , query ) ;
var sql = translator . Translate ( ) ;
2018-11-08 16:33:19 +01:00
AddGetByQueryOrderBy ( sql ) ;
2020-01-07 14:53:17 +11:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) ) ;
2018-11-08 16:33:19 +01:00
}
private void AddGetByQueryOrderBy ( Sql < ISqlContext > sql )
{
2019-01-21 15:39:19 +01:00
sql
2017-12-07 16:45:25 +01:00
. OrderBy < NodeDto > ( x = > x . Level )
. OrderBy < NodeDto > ( x = > x . SortOrder ) ;
}
protected override Sql < ISqlContext > GetBaseQuery ( QueryType queryType )
{
return GetBaseQuery ( queryType , true ) ;
}
2018-10-18 14:16:54 +02:00
// gets the COALESCE expression for variant/invariant name
private string VariantNameSqlExpression
= > SqlContext . VisitDto < ContentVersionCultureVariationDto , NodeDto > ( ( ccv , node ) = > ccv . Name ? ? node . Text , "ccv" ) . Sql ;
2018-10-30 17:32:27 +11:00
protected Sql < ISqlContext > GetBaseQuery ( QueryType queryType , bool current )
2017-12-07 16:45:25 +01:00
{
var sql = SqlContext . Sql ( ) ;
switch ( queryType )
{
case QueryType . Count :
sql = sql . SelectCount ( ) ;
break ;
case QueryType . Ids :
sql = sql . Select < DocumentDto > ( x = > x . NodeId ) ;
break ;
case QueryType . Single :
case QueryType . Many :
2019-02-06 17:28:48 +01:00
// R# may flag this ambiguous and red-squiggle it, but it is not
sql = sql . Select < DocumentDto > ( r = >
r . Select ( documentDto = > documentDto . ContentDto , r1 = >
r1 . Select ( contentDto = > contentDto . NodeDto ) )
. Select ( documentDto = > documentDto . DocumentVersionDto , r1 = >
r1 . Select ( documentVersionDto = > documentVersionDto . ContentVersionDto ) )
. Select ( documentDto = > documentDto . PublishedVersionDto , "pdv" , r1 = >
r1 . Select ( documentVersionDto = > documentVersionDto . ContentVersionDto , "pcv" ) ) )
2018-10-18 14:16:54 +02:00
2018-11-05 17:20:26 +11:00
// select the variant name, coalesce to the invariant name, as "variantName"
2018-10-18 14:16:54 +02:00
. AndSelect ( VariantNameSqlExpression + " AS variantName" ) ;
2017-12-07 16:45:25 +01:00
break ;
}
sql
. From < DocumentDto > ( )
. InnerJoin < ContentDto > ( ) . On < DocumentDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < NodeDto > ( ) . On < ContentDto , NodeDto > ( left = > left . NodeId , right = > right . NodeId )
// inner join on mandatory edited version
2018-10-04 13:15:01 +02:00
. InnerJoin < ContentVersionDto > ( )
. On < DocumentDto , ContentVersionDto > ( ( left , right ) = > left . NodeId = = right . NodeId )
. InnerJoin < DocumentVersionDto > ( )
. On < ContentVersionDto , DocumentVersionDto > ( ( left , right ) = > left . Id = = right . Id )
2017-12-07 16:45:25 +01:00
// left join on optional published version
. LeftJoin < ContentVersionDto > ( nested = >
2018-10-04 13:15:01 +02:00
nested . InnerJoin < DocumentVersionDto > ( "pdv" )
. On < ContentVersionDto , DocumentVersionDto > ( ( left , right ) = > left . Id = = right . Id & & right . Published , "pcv" , "pdv" ) , "pcv" )
2018-10-18 14:16:54 +02:00
. On < DocumentDto , ContentVersionDto > ( ( left , right ) = > left . NodeId = = right . NodeId , aliasRight : "pcv" )
2017-12-07 16:45:25 +01:00
2019-01-26 09:42:14 -05:00
// TODO: should we be joining this when the query type is not single/many?
2018-10-18 14:16:54 +02:00
// left join on optional culture variation
//the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code
2018-10-04 13:15:01 +02:00
. LeftJoin < ContentVersionCultureVariationDto > ( nested = >
2018-11-05 17:20:26 +11:00
nested . InnerJoin < LanguageDto > ( "lang" ) . On < ContentVersionCultureVariationDto , LanguageDto > ( ( ccv , lang ) = > ccv . LanguageId = = lang . Id & & lang . IsoCode = = "[[[ISOCODE]]]" , "ccv" , "lang" ) , "ccv" )
. On < ContentVersionDto , ContentVersionCultureVariationDto > ( ( version , ccv ) = > version . Id = = ccv . VersionId , aliasRight : "ccv" ) ;
2018-10-04 13:15:01 +02:00
2017-12-07 16:45:25 +01:00
sql
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId ) ;
// this would ensure we don't get the published version - keep for reference
//sql
// .WhereAny(
// x => x.Where<ContentVersionDto, ContentVersionDto>((x1, x2) => x1.Id != x2.Id, alias2: "pcv"),
// x => x.WhereNull<ContentVersionDto>(x1 => x1.Id, "pcv")
// );
if ( current )
sql . Where < ContentVersionDto > ( x = > x . Current ) ; // always get the current version
return sql ;
}
protected override Sql < ISqlContext > GetBaseQuery ( bool isCount )
{
return GetBaseQuery ( isCount ? QueryType . Count : QueryType . Single ) ;
}
// ah maybe not, that what's used for eg Exists in base repo
protected override string GetBaseWhereClause ( )
{
2021-02-09 10:22:42 +01:00
return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id" ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < string > GetDeleteClauses ( )
{
var list = new List < string >
{
2021-02-09 10:22:42 +01:00
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentSchedule + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Node + " WHERE id = @id)" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . User2NodeNotify + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . UserGroup2NodePermission + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . UserStartNode + " WHERE startNode = @id" ,
"UPDATE " + Cms . Core . Constants . DatabaseSchema . Tables . UserGroup + " SET startContentId = NULL WHERE startContentId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Relation + " WHERE parentId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Relation + " WHERE childId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . TagRelationship + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Domain + " WHERE domainRootStructureID = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Document + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . DocumentCultureVariation + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . DocumentVersion + " WHERE id IN (SELECT id FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id)" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id)" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id)" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Content + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . AccessRule + " WHERE accessId IN (SELECT id FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Access + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Access + " WHERE loginNodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Access + " WHERE noAccessNodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Node + " WHERE id = @id"
2017-12-07 16:45:25 +01:00
} ;
return list ;
}
#endregion
#region Versions
public override IEnumerable < IContent > GetAllVersions ( int nodeId )
{
var sql = GetBaseQuery ( QueryType . Many , false )
. Where < NodeDto > ( x = > x . NodeId = = nodeId )
. OrderByDescending < ContentVersionDto > ( x = > x . Current )
. AndByDescending < ContentVersionDto > ( x = > x . VersionDate ) ;
2020-01-07 14:53:17 +11:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) , true ) ;
2017-12-07 16:45:25 +01:00
}
2020-01-07 14:53:17 +11:00
// TODO: This method needs to return a readonly version of IContent! The content returned
// from this method does not contain all of the data required to re-persist it and if that
// is attempted some odd things will occur.
// Either we create an IContentReadOnly (which ultimately we should for vNext so we can
// differentiate between methods that return entities that can be re-persisted or not), or
// in the meantime to not break API compatibility, we can add a property to IContentBase
// (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw
// an exception if that entity is passed to a Save method.
// Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService.
// Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly
// which can return IContentReadOnly.
// We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a
// content item. Ideally for paged data that populates list views, these would be ultra slim
// content items, there's no reason to populate those with really anything apart from property data,
// but until we do something like the above, we can't do that since it would be breaking and unclear.
2018-10-22 08:45:30 +02:00
public override IEnumerable < IContent > GetAllVersionsSlim ( int nodeId , int skip , int take )
{
var sql = GetBaseQuery ( QueryType . Many , false )
. Where < NodeDto > ( x = > x . NodeId = = nodeId )
. OrderByDescending < ContentVersionDto > ( x = > x . Current )
2019-03-22 13:55:46 +01:00
. AndByDescending < ContentVersionDto > ( x = > x . VersionDate ) ;
2019-12-11 16:31:03 +01:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) , true ,
2020-01-07 15:34:53 +11:00
// load bare minimum, need variants though since this is used to rollback with variants
2020-01-06 15:43:00 +01:00
false , false , false , true ) . Skip ( skip ) . Take ( take ) ;
2018-10-22 08:45:30 +02:00
}
2017-12-07 16:45:25 +01:00
public override IContent GetVersion ( int versionId )
{
var sql = GetBaseQuery ( QueryType . Single , false )
. Where < ContentVersionDto > ( x = > x . Id = = versionId ) ;
var dto = Database . Fetch < DocumentDto > ( sql ) . FirstOrDefault ( ) ;
return dto = = null ? null : MapDtoToContent ( dto ) ;
}
2019-10-01 22:31:42 +01:00
// deletes a specific version
public override void DeleteVersion ( int versionId )
{
// TODO: test object node type?
// get the version we want to delete
var template = SqlContext . Templates . Get ( "Umbraco.Core.DocumentRepository.GetVersion" , tsql = >
tsql . Select < ContentVersionDto > ( )
. AndSelect < DocumentVersionDto > ( )
. From < ContentVersionDto > ( )
. InnerJoin < DocumentVersionDto > ( )
. On < ContentVersionDto , DocumentVersionDto > ( ( c , d ) = > c . Id = = d . Id )
. Where < ContentVersionDto > ( x = > x . Id = = SqlTemplate . Arg < int > ( "versionId" ) )
) ;
var versionDto = Database . Fetch < DocumentVersionDto > ( template . Sql ( new { versionId } ) ) . FirstOrDefault ( ) ;
// nothing to delete
if ( versionDto = = null )
return ;
// don't delete the current or published version
if ( versionDto . ContentVersionDto . Current )
throw new InvalidOperationException ( "Cannot delete the current version." ) ;
else if ( versionDto . Published )
throw new InvalidOperationException ( "Cannot delete the published version." ) ;
PerformDeleteVersion ( versionDto . ContentVersionDto . NodeId , versionId ) ;
}
// deletes all versions of an entity, older than a date.
public override void DeleteVersions ( int nodeId , DateTime versionDate )
{
// TODO: test object node type?
// get the versions we want to delete, excluding the current one
var template = SqlContext . Templates . Get ( "Umbraco.Core.DocumentRepository.GetVersions" , tsql = >
tsql . Select < ContentVersionDto > ( )
. From < ContentVersionDto > ( )
. InnerJoin < DocumentVersionDto > ( )
. On < ContentVersionDto , DocumentVersionDto > ( ( c , d ) = > c . Id = = d . Id )
. Where < ContentVersionDto > ( x = > x . NodeId = = SqlTemplate . Arg < int > ( "nodeId" ) & & ! x . Current & & x . VersionDate < SqlTemplate . Arg < DateTime > ( "versionDate" ) )
2020-04-07 15:02:08 +10:00
. Where < DocumentVersionDto > ( x = > ! x . Published )
2019-10-01 22:31:42 +01:00
) ;
var versionDtos = Database . Fetch < ContentVersionDto > ( template . Sql ( new { nodeId , versionDate } ) ) ;
foreach ( var versionDto in versionDtos )
PerformDeleteVersion ( versionDto . NodeId , versionDto . Id ) ;
}
2017-12-07 16:45:25 +01:00
protected override void PerformDeleteVersion ( int id , int versionId )
{
// raise event first else potential FK issues
2017-12-12 15:04:13 +01:00
OnUowRemovingVersion ( new ScopedVersionEventArgs ( AmbientScope , id , versionId ) ) ;
2017-12-07 16:45:25 +01:00
Database . Delete < PropertyDataDto > ( "WHERE versionId = @versionId" , new { versionId } ) ;
2019-10-01 22:31:42 +01:00
Database . Delete < ContentVersionCultureVariationDto > ( "WHERE versionId = @versionId" , new { versionId } ) ;
2017-12-07 16:45:25 +01:00
Database . Delete < DocumentVersionDto > ( "WHERE id = @versionId" , new { versionId } ) ;
2019-10-01 22:31:42 +01:00
Database . Delete < ContentVersionDto > ( "WHERE id = @versionId" , new { versionId } ) ;
2017-12-07 16:45:25 +01:00
}
#endregion
#region Persist
protected override void PersistNewItem ( IContent entity )
{
2019-06-28 09:19:11 +02:00
entity . AddingEntity ( ) ;
2017-12-07 16:45:25 +01:00
2019-06-28 09:19:11 +02:00
var publishing = entity . PublishedState = = PublishedState . Publishing ;
2017-12-07 16:45:25 +01:00
// ensure that the default template is assigned
2018-11-15 06:48:03 +00:00
if ( entity . TemplateId . HasValue = = false )
entity . TemplateId = entity . ContentType . DefaultTemplate ? . Id ;
2017-12-07 16:45:25 +01:00
2018-06-20 14:18:57 +02:00
// sanitize names
2019-06-28 09:19:11 +02:00
SanitizeNames ( entity , publishing ) ;
2017-12-07 16:45:25 +01:00
// ensure that strings don't contain characters that are invalid in xml
2019-01-26 09:42:14 -05:00
// TODO: do we really want to keep doing this here?
2017-12-07 16:45:25 +01:00
entity . SanitizeEntityPropertiesForXmlStorage ( ) ;
// create the dto
2018-11-05 17:20:26 +11:00
var dto = ContentBaseFactory . BuildDto ( entity , NodeObjectTypeId ) ;
2017-12-07 16:45:25 +01:00
// derive path and level from parent
var parent = GetParentNodeDto ( entity . ParentId ) ;
var level = parent . Level + 1 ;
// get sort order
var sortOrder = GetNewChildSortOrder ( entity . ParentId , 0 ) ;
// persist the node dto
var nodeDto = dto . ContentDto . NodeDto ;
nodeDto . Path = parent . Path ;
nodeDto . Level = Convert . ToInt16 ( level ) ;
nodeDto . SortOrder = sortOrder ;
// see if there's a reserved identifier for this unique id
// and then either update or insert the node dto
var id = GetReservedId ( nodeDto . UniqueId ) ;
if ( id > 0 )
nodeDto . NodeId = id ;
else
Database . Insert ( nodeDto ) ;
nodeDto . Path = string . Concat ( parent . Path , "," , nodeDto . NodeId ) ;
nodeDto . ValidatePathWithException ( ) ;
Database . Update ( nodeDto ) ;
// update entity
entity . Id = nodeDto . NodeId ;
entity . Path = nodeDto . Path ;
entity . SortOrder = sortOrder ;
entity . Level = level ;
// persist the content dto
var contentDto = dto . ContentDto ;
contentDto . NodeId = nodeDto . NodeId ;
Database . Insert ( contentDto ) ;
// persist the content version dto
var contentVersionDto = dto . DocumentVersionDto . ContentVersionDto ;
contentVersionDto . NodeId = nodeDto . NodeId ;
contentVersionDto . Current = ! publishing ;
Database . Insert ( contentVersionDto ) ;
2019-06-28 09:19:11 +02:00
entity . VersionId = contentVersionDto . Id ;
2017-12-07 16:45:25 +01:00
// persist the document version dto
var documentVersionDto = dto . DocumentVersionDto ;
2019-06-28 09:19:11 +02:00
documentVersionDto . Id = entity . VersionId ;
2017-12-07 16:45:25 +01:00
if ( publishing )
documentVersionDto . Published = true ;
Database . Insert ( documentVersionDto ) ;
// and again in case we're publishing immediately
if ( publishing )
{
2019-06-28 09:19:11 +02:00
entity . PublishedVersionId = entity . VersionId ;
2017-12-07 16:45:25 +01:00
contentVersionDto . Id = 0 ;
contentVersionDto . Current = true ;
2019-06-28 09:19:11 +02:00
contentVersionDto . Text = entity . Name ;
2017-12-07 16:45:25 +01:00
Database . Insert ( contentVersionDto ) ;
2019-06-28 09:19:11 +02:00
entity . VersionId = contentVersionDto . Id ;
2017-12-07 16:45:25 +01:00
2019-06-28 09:19:11 +02:00
documentVersionDto . Id = entity . VersionId ;
2017-12-07 16:45:25 +01:00
documentVersionDto . Published = false ;
Database . Insert ( documentVersionDto ) ;
}
// persist the property data
2019-06-28 09:19:11 +02:00
var propertyDataDtos = PropertyFactory . BuildDtos ( entity . ContentType . Variations , entity . VersionId , entity . PublishedVersionId , entity . Properties , LanguageRepository , out var edited , out var editedCultures ) ;
2017-12-07 16:45:25 +01:00
foreach ( var propertyDataDto in propertyDataDtos )
Database . Insert ( propertyDataDto ) ;
2018-06-20 14:18:57 +02:00
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
2019-06-28 09:19:11 +02:00
if ( ! publishing & & entity . PublishName ! = entity . Name )
2018-04-23 12:53:17 +02:00
edited = true ;
2017-12-07 16:45:25 +01:00
// persist the document dto
// at that point, when publishing, the entity still has its old Published value
2018-11-14 13:59:53 +01:00
// so we need to explicitly update the dto to persist the correct value
2019-06-28 09:19:11 +02:00
if ( entity . PublishedState = = PublishedState . Publishing )
2017-12-07 16:45:25 +01:00
dto . Published = true ;
dto . NodeId = nodeDto . NodeId ;
2019-06-28 09:19:11 +02:00
entity . Edited = dto . Edited = ! dto . Published | | edited ; // if not published, always edited
2017-12-07 16:45:25 +01:00
Database . Insert ( dto ) ;
2018-11-02 14:55:34 +11:00
//insert the schedule
2019-06-28 09:19:11 +02:00
PersistContentSchedule ( entity , false ) ;
2018-11-02 14:55:34 +11:00
2020-10-14 11:08:55 +02:00
2018-04-12 22:53:04 +02:00
// persist the variations
2019-06-28 09:19:11 +02:00
if ( entity . ContentType . VariesByCulture ( ) )
2018-04-12 22:53:04 +02:00
{
2018-10-17 17:27:39 +02:00
// bump dates to align cultures to version
if ( publishing )
2019-06-28 09:19:11 +02:00
entity . AdjustDates ( contentVersionDto . VersionDate ) ;
2018-10-17 17:27:39 +02:00
2018-04-23 12:53:17 +02:00
// names also impact 'edited'
2019-02-05 14:13:03 +11:00
// ReSharper disable once UseDeconstruction
2019-06-28 09:19:11 +02:00
foreach ( var cultureInfo in entity . CultureInfos )
if ( cultureInfo . Name ! = entity . GetPublishName ( cultureInfo . Culture ) )
2019-02-05 14:13:03 +11:00
( editedCultures ? ? ( editedCultures = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ) ) . Add ( cultureInfo . Culture ) ;
2018-04-23 12:53:17 +02:00
// insert content variations
2019-06-28 09:19:11 +02:00
Database . BulkInsertRecords ( GetContentVariationDtos ( entity , publishing ) ) ;
2018-04-23 12:53:17 +02:00
// insert document variations
2019-07-31 18:30:34 +10:00
Database . BulkInsertRecords ( GetDocumentVariationDtos ( entity , editedCultures ) ) ;
2018-04-12 22:53:04 +02:00
}
2018-04-23 12:53:17 +02:00
// refresh content
2019-06-28 09:19:11 +02:00
entity . SetCultureEdited ( editedCultures ) ;
2018-04-23 12:53:17 +02:00
2017-12-07 16:45:25 +01:00
// trigger here, before we reset Published etc
2017-12-12 15:04:13 +01:00
OnUowRefreshedEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
// flip the entity's published property
// this also flips its published state
2018-04-12 22:53:04 +02:00
// note: what depends on variations (eg PublishNames) is managed directly by the content
2019-06-28 09:19:11 +02:00
if ( entity . PublishedState = = PublishedState . Publishing )
2017-12-07 16:45:25 +01:00
{
2019-06-28 09:19:11 +02:00
entity . Published = true ;
entity . PublishTemplateId = entity . TemplateId ;
entity . PublisherId = entity . WriterId ;
entity . PublishName = entity . Name ;
entity . PublishDate = entity . UpdateDate ;
2018-02-01 14:14:45 +01:00
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2017-12-07 16:45:25 +01:00
}
2019-06-28 09:19:11 +02:00
else if ( entity . PublishedState = = PublishedState . Unpublishing )
2017-12-07 16:45:25 +01:00
{
2019-06-28 09:19:11 +02:00
entity . Published = false ;
entity . PublishTemplateId = null ;
entity . PublisherId = null ;
entity . PublishName = null ;
entity . PublishDate = null ;
2017-12-07 16:45:25 +01:00
2018-02-01 14:14:45 +01:00
ClearEntityTags ( entity , _tagRepository ) ;
}
2017-12-07 16:45:25 +01:00
2019-10-25 14:17:18 +11:00
PersistRelations ( entity ) ;
2017-12-07 16:45:25 +01:00
entity . ResetDirtyProperties ( ) ;
// troubleshooting
2019-11-05 14:59:10 +01:00
//if (Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1)
2017-12-07 16:45:25 +01:00
//{
// Debugger.Break();
// throw new Exception("oops");
//}
2019-11-05 14:59:10 +01:00
//if (Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1)
2017-12-07 16:45:25 +01:00
//{
// Debugger.Break();
// throw new Exception("oops");
//}
}
protected override void PersistUpdatedItem ( IContent entity )
{
2020-04-07 15:02:08 +10:00
var isEntityDirty = entity . IsDirty ( ) ;
2017-12-07 16:45:25 +01:00
// check if we need to make any database changes at all
2019-06-28 09:19:11 +02:00
if ( ( entity . PublishedState = = PublishedState . Published | | entity . PublishedState = = PublishedState . Unpublished )
& & ! isEntityDirty & & ! entity . IsAnyUserPropertyDirty ( ) )
2017-12-07 16:45:25 +01:00
return ; // no change to save, do nothing, don't even update dates
// whatever we do, we must check that we are saving the current version
2019-06-28 09:19:11 +02:00
var version = Database . Fetch < ContentVersionDto > ( SqlContext . Sql ( ) . Select < ContentVersionDto > ( ) . From < ContentVersionDto > ( ) . Where < ContentVersionDto > ( x = > x . Id = = entity . VersionId ) ) . FirstOrDefault ( ) ;
2018-10-04 13:15:01 +02:00
if ( version = = null | | ! version . Current )
2017-12-07 16:45:25 +01:00
throw new InvalidOperationException ( "Cannot save a non-current version." ) ;
// update
2019-06-28 09:19:11 +02:00
entity . UpdatingEntity ( ) ;
2020-04-07 15:02:08 +10:00
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// In this case we can bypass a lot of the below operations which will make this whole operation go much faster.
// When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity . IsMoving ( ) ;
2020-04-08 11:22:15 +10:00
// TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below.
// There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost
// copy performance by 95% just like we did for Move
2017-12-07 16:45:25 +01:00
2019-06-28 09:19:11 +02:00
var publishing = entity . PublishedState = = PublishedState . Publishing ;
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
if ( ! isMoving )
2017-12-07 16:45:25 +01:00
{
2020-04-07 15:02:08 +10:00
// check if we need to create a new version
if ( publishing & & entity . PublishedVersionId > 0 )
{
// published version is not published anymore
Database . Execute ( Sql ( ) . Update < DocumentVersionDto > ( u = > u . Set ( x = > x . Published , false ) ) . Where < DocumentVersionDto > ( x = > x . Id = = entity . PublishedVersionId ) ) ;
}
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
// sanitize names
SanitizeNames ( entity , publishing ) ;
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity . SanitizeEntityPropertiesForXmlStorage ( ) ;
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
// if parent has changed, get path, level and sort order
if ( entity . IsPropertyDirty ( "ParentId" ) )
{
var parent = GetParentNodeDto ( entity . ParentId ) ;
entity . Path = string . Concat ( parent . Path , "," , entity . Id ) ;
entity . Level = parent . Level + 1 ;
entity . SortOrder = GetNewChildSortOrder ( entity . ParentId , 0 ) ;
}
2017-12-07 16:45:25 +01:00
}
// create the dto
2018-11-05 17:20:26 +11:00
var dto = ContentBaseFactory . BuildDto ( entity , NodeObjectTypeId ) ;
2017-12-07 16:45:25 +01:00
// update the node dto
var nodeDto = dto . ContentDto . NodeDto ;
nodeDto . ValidatePathWithException ( ) ;
Database . Update ( nodeDto ) ;
2020-04-07 15:02:08 +10:00
if ( ! isMoving )
2017-12-07 16:45:25 +01:00
{
2020-04-07 15:02:08 +10:00
// update the content dto
Database . Update ( dto . ContentDto ) ;
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
// update the content & document version dtos
var contentVersionDto = dto . DocumentVersionDto . ContentVersionDto ;
var documentVersionDto = dto . DocumentVersionDto ;
2018-10-17 17:27:39 +02:00
if ( publishing )
2020-04-07 15:02:08 +10:00
{
documentVersionDto . Published = true ; // now published
contentVersionDto . Current = false ; // no more current
}
Database . Update ( contentVersionDto ) ;
Database . Update ( documentVersionDto ) ;
2018-04-23 12:53:17 +02:00
2020-04-07 15:02:08 +10:00
// and, if publishing, insert new content & document version dtos
2018-10-17 17:27:39 +02:00
if ( publishing )
2020-04-07 15:02:08 +10:00
{
entity . PublishedVersionId = entity . VersionId ;
2018-10-17 17:27:39 +02:00
2020-04-07 15:02:08 +10:00
contentVersionDto . Id = 0 ; // want a new id
contentVersionDto . Current = true ; // current version
contentVersionDto . Text = entity . Name ;
Database . Insert ( contentVersionDto ) ;
entity . VersionId = documentVersionDto . Id = contentVersionDto . Id ; // get the new id
2018-04-12 22:53:04 +02:00
2020-04-07 15:02:08 +10:00
documentVersionDto . Published = false ; // non-published version
Database . Insert ( documentVersionDto ) ;
}
2018-09-25 18:05:14 +02:00
2020-10-22 14:18:21 +02:00
// replace the property data (rather than updating)
2018-04-23 12:53:17 +02:00
// only need to delete for the version that existed, the new version (if any) has no property data yet
2020-10-22 14:18:21 +02:00
var versionToDelete = publishing ? entity . PublishedVersionId : entity . VersionId ;
// insert property data
ReplacePropertyValues ( entity , versionToDelete , publishing ? entity . PublishedVersionId : 0 , out var edited , out var editedCultures ) ;
2020-04-07 15:02:08 +10:00
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
if ( ! publishing & & entity . PublishName ! = entity . Name )
edited = true ;
if ( entity . ContentType . VariesByCulture ( ) )
{
// bump dates to align cultures to version
if ( publishing )
entity . AdjustDates ( contentVersionDto . VersionDate ) ;
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
2020-10-14 11:08:55 +02:00
foreach ( var cultureInfo in entity . CultureInfos )
2020-04-07 15:02:08 +10:00
if ( cultureInfo . Name ! = entity . GetPublishName ( cultureInfo . Culture ) )
{
edited = true ;
( editedCultures ? ? ( editedCultures = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ) ) . Add ( cultureInfo . Culture ) ;
// TODO: change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
// replace the content version variations (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql ( ) . Delete < ContentVersionCultureVariationDto > ( ) . Where < ContentVersionCultureVariationDto > ( x = > x . VersionId = = versionToDelete ) ;
Database . Execute ( deleteContentVariations ) ;
// replace the document version variations (rather than updating)
var deleteDocumentVariations = Sql ( ) . Delete < DocumentCultureVariationDto > ( ) . Where < DocumentCultureVariationDto > ( x = > x . NodeId = = entity . Id ) ;
Database . Execute ( deleteDocumentVariations ) ;
// TODO: NPoco InsertBulk issue?
// we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
// but by using SQL Server and updating a variants name will cause: Unable to cast object of type
// 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
// (same in PersistNewItem above)
// insert content variations
Database . BulkInsertRecords ( GetContentVariationDtos ( entity , publishing ) ) ;
// insert document variations
Database . BulkInsertRecords ( GetDocumentVariationDtos ( entity , editedCultures ) ) ;
}
2018-04-23 12:53:17 +02:00
2020-04-07 15:02:08 +10:00
// refresh content
entity . SetCultureEdited ( editedCultures ) ;
// update the document dto
// at that point, when un/publishing, the entity still has its old Published value
// so we need to explicitly update the dto to persist the correct value
if ( entity . PublishedState = = PublishedState . Publishing )
dto . Published = true ;
else if ( entity . PublishedState = = PublishedState . Unpublishing )
dto . Published = false ;
entity . Edited = dto . Edited = ! dto . Published | | edited ; // if not published, always edited
Database . Update ( dto ) ;
//update the schedule
if ( entity . IsPropertyDirty ( "ContentSchedule" ) )
PersistContentSchedule ( entity , true ) ;
// if entity is publishing, update tags, else leave tags there
// means that implicitly unpublished, or trashed, entities *still* have tags in db
if ( entity . PublishedState = = PublishedState . Publishing )
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2018-04-12 22:53:04 +02:00
}
2017-12-07 16:45:25 +01:00
// trigger here, before we reset Published etc
2017-12-12 15:04:13 +01:00
OnUowRefreshedEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
if ( ! isMoving )
2017-12-07 16:45:25 +01:00
{
2020-04-07 15:02:08 +10:00
// flip the entity's published property
// this also flips its published state
if ( entity . PublishedState = = PublishedState . Publishing )
{
entity . Published = true ;
entity . PublishTemplateId = entity . TemplateId ;
entity . PublisherId = entity . WriterId ;
entity . PublishName = entity . Name ;
entity . PublishDate = entity . UpdateDate ;
2018-02-01 14:14:45 +01:00
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2020-04-07 15:02:08 +10:00
}
else if ( entity . PublishedState = = PublishedState . Unpublishing )
{
entity . Published = false ;
entity . PublishTemplateId = null ;
entity . PublisherId = null ;
entity . PublishName = null ;
entity . PublishDate = null ;
2018-02-01 14:14:45 +01:00
2020-04-07 15:02:08 +10:00
ClearEntityTags ( entity , _tagRepository ) ;
}
2017-12-07 16:45:25 +01:00
2020-04-07 15:02:08 +10:00
PersistRelations ( entity ) ;
2019-10-25 14:17:18 +11:00
2020-04-07 15:02:08 +10:00
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
2017-12-07 16:45:25 +01:00
}
entity . ResetDirtyProperties ( ) ;
// troubleshooting
2019-11-05 14:59:10 +01:00
//if (Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1)
2017-12-07 16:45:25 +01:00
//{
// Debugger.Break();
// throw new Exception("oops");
//}
2019-11-05 14:59:10 +01:00
//if (Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1)
2017-12-07 16:45:25 +01:00
//{
// Debugger.Break();
// throw new Exception("oops");
//}
}
2018-11-14 13:59:53 +01:00
private void PersistContentSchedule ( IContent content , bool update )
{
var schedules = ContentBaseFactory . BuildScheduleDto ( content , LanguageRepository ) . ToList ( ) ;
//remove any that no longer exist
if ( update )
{
var ids = schedules . Where ( x = > x . Model . Id ! = Guid . Empty ) . Select ( x = > x . Model . Id ) . Distinct ( ) ;
Database . Execute ( Sql ( )
. Delete < ContentScheduleDto > ( )
. Where < ContentScheduleDto > ( x = > x . NodeId = = content . Id )
. WhereNotIn < ContentScheduleDto > ( x = > x . Id , ids ) ) ;
}
//add/update the rest
foreach ( var schedule in schedules )
{
if ( schedule . Model . Id = = Guid . Empty )
{
schedule . Model . Id = schedule . Dto . Id = Guid . NewGuid ( ) ;
Database . Insert ( schedule . Dto ) ;
}
else
{
Database . Update ( schedule . Dto ) ;
}
}
}
2017-12-07 16:45:25 +01:00
protected override void PersistDeletedItem ( IContent entity )
{
// raise event first else potential FK issues
2017-12-12 15:04:13 +01:00
OnUowRemovingEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
//We need to clear out all access rules but we need to do this in a manual way since
// nothing in that table is joined to a content id
var subQuery = SqlContext . Sql ( )
. Select < AccessRuleDto > ( x = > x . AccessId )
. From < AccessRuleDto > ( )
. InnerJoin < AccessDto > ( )
. On < AccessRuleDto , AccessDto > ( left = > left . AccessId , right = > right . Id )
. Where < AccessDto > ( dto = > dto . NodeId = = entity . Id ) ;
Database . Execute ( SqlContext . SqlSyntax . GetDeleteSubquery ( "umbracoAccessRule" , "accessId" , subQuery ) ) ;
//now let the normal delete clauses take care of everything else
base . PersistDeletedItem ( entity ) ;
}
#endregion
#region Content Repository
public int CountPublished ( string contentTypeAlias = null )
{
var sql = SqlContext . Sql ( ) ;
if ( contentTypeAlias . IsNullOrWhiteSpace ( ) )
{
sql . SelectCount ( )
. From < NodeDto > ( )
. InnerJoin < DocumentDto > ( )
. On < NodeDto , DocumentDto > ( left = > left . NodeId , right = > right . NodeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId & & x . Trashed = = false )
. Where < DocumentDto > ( x = > x . Published ) ;
}
else
{
sql . SelectCount ( )
. From < NodeDto > ( )
. InnerJoin < ContentDto > ( )
. On < NodeDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < DocumentDto > ( )
. On < NodeDto , DocumentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < ContentTypeDto > ( )
. On < ContentTypeDto , ContentDto > ( left = > left . NodeId , right = > right . ContentTypeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId & & x . Trashed = = false )
. Where < ContentTypeDto > ( x = > x . Alias = = contentTypeAlias )
. Where < DocumentDto > ( x = > x . Published ) ;
}
return Database . ExecuteScalar < int > ( sql ) ;
}
public void ReplaceContentPermissions ( EntityPermissionSet permissionSet )
{
PermissionRepository . ReplaceEntityPermissions ( permissionSet ) ;
}
/// <summary>
/// Assigns a single permission to the current content item for the specified group ids
/// </summary>
/// <param name="entity"></param>
/// <param name="permission"></param>
/// <param name="groupIds"></param>
public void AssignEntityPermission ( IContent entity , char permission , IEnumerable < int > groupIds )
{
PermissionRepository . AssignEntityPermission ( entity , permission , groupIds ) ;
}
public EntityPermissionCollection GetPermissionsForEntity ( int entityId )
{
return PermissionRepository . GetPermissionsForEntity ( entityId ) ;
}
/// <summary>
/// Used to add/update a permission for a content item
/// </summary>
/// <param name="permission"></param>
public void AddOrUpdatePermissions ( ContentPermissionSet permission )
{
PermissionRepository . Save ( permission ) ;
}
2018-09-18 11:53:33 +02:00
/// <inheritdoc />
2017-12-07 16:45:25 +01:00
public override IEnumerable < IContent > GetPage ( IQuery < IContent > query ,
2018-10-04 13:15:01 +02:00
long pageIndex , int pageSize , out long totalRecords ,
2018-09-18 11:53:33 +02:00
IQuery < IContent > filter , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
Sql < ISqlContext > filterSql = null ;
2018-10-18 14:16:54 +02:00
// if we have a filter, map its clauses to an Sql statement
2017-12-07 16:45:25 +01:00
if ( filter ! = null )
{
2018-10-18 14:16:54 +02:00
// if the clause works on "name", we need to swap the field and use the variantName instead,
// so that querying also works on variant content (for instance when searching a listview).
// figure out how the "name" field is going to look like - so we can look for it
var nameField = SqlContext . VisitModelField < IContent > ( x = > x . Name ) ;
2017-12-07 16:45:25 +01:00
filterSql = Sql ( ) ;
foreach ( var filterClause in filter . GetWhereClauses ( ) )
2018-10-04 13:15:01 +02:00
{
2018-10-18 14:16:54 +02:00
var clauseSql = filterClause . Item1 ;
2018-11-05 17:20:26 +11:00
var clauseArgs = filterClause . Item2 ;
2018-10-18 14:16:54 +02:00
// replace the name field
// we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here
clauseSql = clauseSql . Replace ( nameField , VariantNameSqlExpression ) ;
// append the clause
filterSql . Append ( $"AND ({clauseSql})" , clauseArgs ) ;
2018-10-04 13:15:01 +02:00
}
2017-12-07 16:45:25 +01:00
}
2017-12-12 15:04:13 +01:00
return GetPage < DocumentDto > ( query , pageIndex , pageSize , out totalRecords ,
2020-01-07 14:53:17 +11:00
x = > MapDtosToContent ( x ) ,
2018-09-18 11:53:33 +02:00
filterSql ,
ordering ) ;
2017-12-07 16:45:25 +01:00
}
public bool IsPathPublished ( IContent content )
{
// fail fast
if ( content . Path . StartsWith ( "-1,-20," ) )
return false ;
// succeed fast
if ( content . ParentId = = - 1 )
return content . Published ;
var ids = content . Path . Split ( ',' ) . Skip ( 1 ) . Select ( int . Parse ) ;
var sql = SqlContext . Sql ( )
. SelectCount < NodeDto > ( x = > x . NodeId )
. From < NodeDto > ( )
. InnerJoin < DocumentDto > ( ) . On < NodeDto , DocumentDto > ( ( n , d ) = > n . NodeId = = d . NodeId & & d . Published )
. WhereIn < NodeDto > ( x = > x . NodeId , ids ) ;
var count = Database . ExecuteScalar < int > ( sql ) ;
return count = = content . Level ;
}
#endregion
#region Recycle Bin
2021-02-09 10:22:42 +01:00
public override int RecycleBinId = > Cms . Core . Constants . System . RecycleBinContent ;
2017-12-07 16:45:25 +01:00
#endregion
#region Read Repository implementation for Guid keys
public IContent Get ( Guid id )
{
return _contentByGuidReadRepository . Get ( id ) ;
}
IEnumerable < IContent > IReadRepository < Guid , IContent > . GetMany ( params Guid [ ] ids )
{
return _contentByGuidReadRepository . GetMany ( ids ) ;
}
public bool Exists ( Guid id )
{
return _contentByGuidReadRepository . Exists ( id ) ;
}
// reading repository purely for looking up by GUID
2019-01-26 09:42:14 -05:00
// TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things!
2020-12-22 10:30:16 +11:00
private class ContentByGuidReadRepository : EntityRepositoryBase < Guid , IContent >
2017-12-07 16:45:25 +01:00
{
private readonly DocumentRepository _outerRepo ;
2020-09-17 09:42:55 +02:00
public ContentByGuidReadRepository ( DocumentRepository outerRepo , IScopeAccessor scopeAccessor , AppCaches cache , ILogger < ContentByGuidReadRepository > logger )
2017-12-14 17:04:44 +01:00
: base ( scopeAccessor , cache , logger )
2017-12-07 16:45:25 +01:00
{
_outerRepo = outerRepo ;
}
protected override Guid NodeObjectTypeId = > _outerRepo . NodeObjectTypeId ;
protected override IContent PerformGet ( Guid id )
{
var sql = _outerRepo . GetBaseQuery ( QueryType . Single )
. Where < NodeDto > ( x = > x . UniqueId = = id ) ;
var dto = Database . Fetch < DocumentDto > ( sql . SelectTop ( 1 ) ) . FirstOrDefault ( ) ;
if ( dto = = null )
return null ;
var content = _outerRepo . MapDtoToContent ( dto ) ;
return content ;
}
protected override IEnumerable < IContent > PerformGetAll ( params Guid [ ] ids )
{
var sql = _outerRepo . GetBaseQuery ( QueryType . Many ) ;
if ( ids . Length > 0 )
sql . WhereIn < NodeDto > ( x = > x . UniqueId , ids ) ;
2020-01-07 14:53:17 +11:00
return _outerRepo . MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) ) ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < IContent > PerformGetByQuery ( IQuery < IContent > query )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < string > GetDeleteClauses ( )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
protected override void PersistNewItem ( IContent entity )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
protected override void PersistUpdatedItem ( IContent entity )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
protected override Sql < ISqlContext > GetBaseQuery ( bool isCount )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
protected override string GetBaseWhereClause ( )
{
2019-10-06 11:34:25 +02:00
throw new InvalidOperationException ( "This method won't be implemented." ) ;
2017-12-07 16:45:25 +01:00
}
}
#endregion
2018-11-05 13:59:55 +11:00
#region Schedule
2018-11-13 17:51:59 +11:00
/// <inheritdoc />
public void ClearSchedule ( DateTime date )
{
var sql = Sql ( ) . Delete < ContentScheduleDto > ( ) . Where < ContentScheduleDto > ( x = > x . Date < = date ) ;
Database . Execute ( sql ) ;
}
2020-07-08 18:16:12 +10:00
/// <inheritdoc />
public void ClearSchedule ( DateTime date , ContentScheduleAction action )
{
var a = action . ToString ( ) ;
var sql = Sql ( ) . Delete < ContentScheduleDto > ( ) . Where < ContentScheduleDto > ( x = > x . Date < = date & & x . Action = = a ) ;
Database . Execute ( sql ) ;
}
private Sql GetSqlForHasScheduling ( ContentScheduleAction action , DateTime date )
{
var template = SqlContext . Templates . Get ( "Umbraco.Core.DocumentRepository.GetSqlForHasScheduling" , tsql = > tsql
. SelectCount ( )
. From < ContentScheduleDto > ( )
. Where < ContentScheduleDto > ( x = > x . Action = = SqlTemplate . Arg < string > ( "action" ) & & x . Date < = SqlTemplate . Arg < DateTime > ( "date" ) ) ) ;
var sql = template . Sql ( action . ToString ( ) , date ) ;
return sql ;
}
public bool HasContentForExpiration ( DateTime date )
{
var sql = GetSqlForHasScheduling ( ContentScheduleAction . Expire , date ) ;
return Database . ExecuteScalar < int > ( sql ) > 0 ;
}
public bool HasContentForRelease ( DateTime date )
{
var sql = GetSqlForHasScheduling ( ContentScheduleAction . Release , date ) ;
return Database . ExecuteScalar < int > ( sql ) > 0 ;
}
2018-11-05 13:59:55 +11:00
/// <inheritdoc />
2018-11-07 19:42:49 +11:00
public IEnumerable < IContent > GetContentForRelease ( DateTime date )
2018-11-05 13:59:55 +11:00
{
2018-11-14 09:16:22 +01:00
var action = ContentScheduleAction . Release . ToString ( ) ;
2018-11-08 16:33:19 +01:00
var sql = GetBaseQuery ( QueryType . Many )
. WhereIn < NodeDto > ( x = > x . NodeId , Sql ( )
. Select < ContentScheduleDto > ( x = > x . NodeId )
. From < ContentScheduleDto > ( )
. Where < ContentScheduleDto > ( x = > x . Action = = action & & x . Date < = date ) ) ;
2018-11-14 11:59:16 +01:00
2018-11-08 16:33:19 +01:00
AddGetByQueryOrderBy ( sql ) ;
2020-01-07 14:53:17 +11:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) ) ;
2018-11-05 13:59:55 +11:00
}
/// <inheritdoc />
2018-11-07 19:42:49 +11:00
public IEnumerable < IContent > GetContentForExpiration ( DateTime date )
2018-11-05 13:59:55 +11:00
{
2018-11-14 09:16:22 +01:00
var action = ContentScheduleAction . Expire . ToString ( ) ;
2018-11-08 16:33:19 +01:00
var sql = GetBaseQuery ( QueryType . Many )
. WhereIn < NodeDto > ( x = > x . NodeId , Sql ( )
. Select < ContentScheduleDto > ( x = > x . NodeId )
. From < ContentScheduleDto > ( )
. Where < ContentScheduleDto > ( x = > x . Action = = action & & x . Date < = date ) ) ;
AddGetByQueryOrderBy ( sql ) ;
2020-01-07 14:53:17 +11:00
return MapDtosToContent ( Database . Fetch < DocumentDto > ( sql ) ) ;
2018-11-05 13:59:55 +11:00
}
#endregion
2018-09-18 11:53:33 +02:00
protected override string ApplySystemOrdering ( ref Sql < ISqlContext > sql , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
// note: 'updater' is the user who created the latest draft version,
// we don't have an 'updater' per culture (should we?)
if ( ordering . OrderBy . InvariantEquals ( "updater" ) )
{
var joins = Sql ( )
. InnerJoin < UserDto > ( "updaterUser" ) . On < ContentVersionDto , UserDto > ( ( version , user ) = > version . UserId = = user . Id , aliasRight : "updaterUser" ) ;
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased
2018-10-09 10:21:32 +02:00
sql = Sql ( InsertBefore ( sql , "FROM" , ", " + SqlSyntax . GetFieldName < UserDto > ( x = > x . UserName , "updaterUser" ) + " AS ordering " ) , sql . Arguments ) ;
2018-09-19 17:50:43 +02:00
2018-09-18 11:53:33 +02:00
sql = InsertJoins ( sql , joins ) ;
2017-12-07 16:45:25 +01:00
2018-09-19 17:50:43 +02:00
return "ordering" ;
2018-09-18 11:53:33 +02:00
}
if ( ordering . OrderBy . InvariantEquals ( "published" ) )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
// no culture = can only work on the global 'published' flag
if ( ordering . Culture . IsNullOrWhiteSpace ( ) )
2018-09-19 17:50:43 +02:00
{
// see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have
// the whole CASE fragment in ORDER BY due to it not being detected by NPoco
sql = Sql ( InsertBefore ( sql , "FROM" , ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering " ) , sql . Arguments ) ;
return "ordering" ;
}
2018-09-18 11:53:33 +02:00
// invariant: left join will yield NULL and we must use pcv to determine published
// variant: left join may yield NULL or something, and that determines published
2019-03-18 14:52:50 +01:00
2018-09-18 11:53:33 +02:00
var joins = Sql ( )
2019-03-18 14:52:50 +01:00
. InnerJoin < ContentTypeDto > ( "ctype" ) . On < ContentDto , ContentTypeDto > ( ( content , contentType ) = > content . ContentTypeId = = contentType . NodeId , aliasRight : "ctype" )
// left join on optional culture variation
//the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code
. LeftJoin < ContentVersionCultureVariationDto > ( nested = >
nested . InnerJoin < LanguageDto > ( "langp" ) . On < ContentVersionCultureVariationDto , LanguageDto > ( ( ccv , lang ) = > ccv . LanguageId = = lang . Id & & lang . IsoCode = = "[[[ISOCODE]]]" , "ccvp" , "langp" ) , "ccvp" )
. On < ContentVersionDto , ContentVersionCultureVariationDto > ( ( version , ccv ) = > version . Id = = ccv . VersionId , aliasLeft : "pcv" , aliasRight : "ccvp" ) ;
2018-09-18 11:53:33 +02:00
sql = InsertJoins ( sql , joins ) ;
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have
// the whole CASE fragment in ORDER BY due to it not being detected by NPoco
2018-09-18 11:53:33 +02:00
var sqlText = InsertBefore ( sql . SQL , "FROM" ,
// when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id,
// otherwise check if there's a version culture variation for the lang, via ccv.id
2019-03-18 14:52:50 +01:00
", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering " ) ; // trailing space is important!
2018-09-18 11:53:33 +02:00
sql = Sql ( sqlText , sql . Arguments ) ;
2018-09-19 17:50:43 +02:00
return "ordering" ;
2017-12-07 16:45:25 +01:00
}
2018-09-18 11:53:33 +02:00
return base . ApplySystemOrdering ( ref sql , ordering ) ;
2017-12-07 16:45:25 +01:00
}
2019-12-11 16:31:03 +01:00
private IEnumerable < IContent > MapDtosToContent ( List < DocumentDto > dtos ,
2020-01-07 14:53:17 +11:00
bool withCache = false ,
bool loadProperties = true ,
bool loadTemplates = true ,
bool loadSchedule = true ,
bool loadVariants = true )
2017-12-07 16:45:25 +01:00
{
var temps = new List < TempContent < Content > > ( ) ;
var contentTypes = new Dictionary < int , IContentType > ( ) ;
var templateIds = new List < int > ( ) ;
var content = new Content [ dtos . Count ] ;
for ( var i = 0 ; i < dtos . Count ; i + + )
{
var dto = dtos [ i ] ;
if ( withCache )
{
// if the cache contains the (proper version of the) item, use it
var cached = IsolatedCache . GetCacheItem < IContent > ( RepositoryCacheKeys . GetKey < IContent > ( dto . NodeId ) ) ;
if ( cached ! = null & & cached . VersionId = = dto . DocumentVersionDto . ContentVersionDto . Id )
{
2018-11-05 17:20:26 +11:00
content [ i ] = ( Content ) cached ;
2017-12-07 16:45:25 +01:00
continue ;
}
}
// else, need to build it
// get the content type - the repository is full cache *but* still deep-clones
// whatever comes out of it, so use our own local index here to avoid this
var contentTypeId = dto . ContentDto . ContentTypeId ;
if ( contentTypes . TryGetValue ( contentTypeId , out var contentType ) = = false )
contentTypes [ contentTypeId ] = contentType = _contentTypeRepository . Get ( contentTypeId ) ;
2018-11-05 17:20:26 +11:00
var c = content [ i ] = ContentBaseFactory . BuildEntity ( dto , contentType ) ;
2017-12-07 16:45:25 +01:00
2019-12-11 16:31:03 +01:00
if ( loadTemplates )
2017-12-07 16:45:25 +01:00
{
2018-10-22 08:45:30 +02:00
// need templates
var templateId = dto . DocumentVersionDto . TemplateId ;
2017-12-07 16:45:25 +01:00
if ( templateId . HasValue & & templateId . Value > 0 )
templateIds . Add ( templateId . Value ) ;
2018-10-22 08:45:30 +02:00
if ( dto . Published )
{
templateId = dto . PublishedVersionDto . TemplateId ;
if ( templateId . HasValue & & templateId . Value > 0 )
templateIds . Add ( templateId . Value ) ;
}
2017-12-07 16:45:25 +01:00
}
2018-10-22 18:21:00 +02:00
// need temps, for properties, templates and variations
2017-12-07 16:45:25 +01:00
var versionId = dto . DocumentVersionDto . Id ;
var publishedVersionId = dto . Published ? dto . PublishedVersionDto . Id : 0 ;
var temp = new TempContent < Content > ( dto . NodeId , versionId , publishedVersionId , contentType , c )
{
Template1Id = dto . DocumentVersionDto . TemplateId
} ;
if ( dto . Published ) temp . Template2Id = dto . PublishedVersionDto . TemplateId ;
temps . Add ( temp ) ;
}
2019-12-11 16:31:03 +01:00
Dictionary < int , ITemplate > templates = null ;
if ( loadTemplates )
2018-10-22 08:45:30 +02:00
{
// load all required templates in 1 query, and index
2019-12-11 16:31:03 +01:00
templates = _templateRepository . GetMany ( templateIds . ToArray ( ) )
2018-10-22 08:45:30 +02:00
. ToDictionary ( x = > x . Id , x = > x ) ;
2019-12-11 16:31:03 +01:00
}
2017-12-07 16:45:25 +01:00
2019-12-11 16:31:03 +01:00
IDictionary < int , PropertyCollection > properties = null ;
if ( loadProperties )
{
2018-10-22 08:45:30 +02:00
// load all properties for all documents from database in 1 query - indexed by version id
2019-12-11 16:31:03 +01:00
properties = GetPropertyCollections ( temps ) ;
}
var schedule = GetContentSchedule ( temps . Select ( x = > x . Content . Id ) . ToArray ( ) ) ;
2017-12-07 16:45:25 +01:00
2019-12-11 16:31:03 +01:00
// assign templates and properties
foreach ( var temp in temps )
{
if ( loadTemplates )
2018-10-22 08:45:30 +02:00
{
2019-01-10 18:31:13 +01:00
// set the template ID if it matches an existing template
2018-11-08 14:30:01 +00:00
if ( temp . Template1Id . HasValue & & templates . ContainsKey ( temp . Template1Id . Value ) )
2018-11-15 06:48:03 +00:00
temp . Content . TemplateId = temp . Template1Id ;
2018-11-08 14:30:01 +00:00
if ( temp . Template2Id . HasValue & & templates . ContainsKey ( temp . Template2Id . Value ) )
2018-11-15 06:48:03 +00:00
temp . Content . PublishTemplateId = temp . Template2Id ;
2019-12-11 16:31:03 +01:00
}
2018-10-22 17:04:58 +11:00
2019-12-11 16:31:03 +01:00
// set properties
if ( loadProperties )
{
2018-11-05 17:20:26 +11:00
if ( properties . ContainsKey ( temp . VersionId ) )
temp . Content . Properties = properties [ temp . VersionId ] ;
else
throw new InvalidOperationException ( $"No property data found for version: '{temp.VersionId}'." ) ;
2019-12-11 16:31:03 +01:00
}
2018-11-05 17:20:26 +11:00
2019-12-11 16:31:03 +01:00
if ( loadSchedule )
{
2019-01-10 18:31:13 +01:00
// load in the schedule
2018-11-05 17:20:26 +11:00
if ( schedule . TryGetValue ( temp . Content . Id , out var s ) )
temp . Content . ContentSchedule = s ;
2018-10-22 08:45:30 +02:00
}
2019-12-11 16:31:03 +01:00
2017-12-07 16:45:25 +01:00
}
2019-12-11 16:31:03 +01:00
if ( loadVariants )
2018-04-12 22:53:04 +02:00
{
2019-12-11 16:31:03 +01:00
// set variations, if varying
temps = temps . Where ( x = > x . ContentType . VariesByCulture ( ) ) . ToList ( ) ;
if ( temps . Count > 0 )
{
// load all variations for all documents from database, in one query
var contentVariations = GetContentVariations ( temps ) ;
var documentVariations = GetDocumentVariations ( temps ) ;
foreach ( var temp in temps )
SetVariations ( temp . Content , contentVariations , documentVariations ) ;
}
2018-04-12 22:53:04 +02:00
}
2019-12-11 16:31:03 +01:00
2018-04-12 22:53:04 +02:00
2019-12-11 16:31:03 +01:00
foreach ( var c in content )
2018-11-07 21:32:12 +11:00
c . ResetDirtyProperties ( false ) ; // reset dirty initial properties (U4-1946)
2017-12-07 16:45:25 +01:00
return content ;
}
private IContent MapDtoToContent ( DocumentDto dto )
{
var contentType = _contentTypeRepository . Get ( dto . ContentDto . ContentTypeId ) ;
2018-11-05 17:20:26 +11:00
var content = ContentBaseFactory . BuildEntity ( dto , contentType ) ;
2017-12-07 16:45:25 +01:00
2018-11-05 17:20:26 +11:00
try
{
content . DisableChangeTracking ( ) ;
2017-12-07 16:45:25 +01:00
2018-11-05 17:20:26 +11:00
// get template
if ( dto . DocumentVersionDto . TemplateId . HasValue & & dto . DocumentVersionDto . TemplateId . Value > 0 )
2019-01-10 16:29:36 +01:00
content . TemplateId = dto . DocumentVersionDto . TemplateId ;
2017-12-07 16:45:25 +01:00
2018-11-05 17:20:26 +11:00
// get properties - indexed by version id
var versionId = dto . DocumentVersionDto . Id ;
2017-12-07 16:45:25 +01:00
2019-01-26 09:42:14 -05:00
// TODO: shall we get published properties or not?
2018-11-05 17:20:26 +11:00
//var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0;
2019-02-05 01:58:18 +11:00
var publishedVersionId = dto . PublishedVersionDto ? . Id ? ? 0 ;
2017-12-07 16:45:25 +01:00
2018-11-05 17:20:26 +11:00
var temp = new TempContent < Content > ( dto . NodeId , versionId , publishedVersionId , contentType ) ;
var ltemp = new List < TempContent < Content > > { temp } ;
var properties = GetPropertyCollections ( ltemp ) ;
content . Properties = properties [ dto . DocumentVersionDto . Id ] ;
// set variations, if varying
if ( contentType . VariesByCulture ( ) )
{
var contentVariations = GetContentVariations ( ltemp ) ;
var documentVariations = GetDocumentVariations ( ltemp ) ;
SetVariations ( content , contentVariations , documentVariations ) ;
}
//load in the schedule
var schedule = GetContentSchedule ( dto . NodeId ) ;
if ( schedule . TryGetValue ( dto . NodeId , out var s ) )
content . ContentSchedule = s ;
// reset dirty initial properties (U4-1946)
content . ResetDirtyProperties ( false ) ;
return content ;
}
finally
2018-04-12 22:53:04 +02:00
{
2018-11-05 17:20:26 +11:00
content . EnableChangeTracking ( ) ;
2018-04-12 22:53:04 +02:00
}
2018-11-05 17:20:26 +11:00
}
2018-04-12 22:53:04 +02:00
2018-11-05 17:20:26 +11:00
private IDictionary < int , ContentScheduleCollection > GetContentSchedule ( params int [ ] contentIds )
{
var result = new Dictionary < int , ContentScheduleCollection > ( ) ;
2018-11-08 16:33:19 +01:00
var scheduleDtos = Database . FetchByGroups < ContentScheduleDto , int > ( contentIds , 2000 , batch = > Sql ( )
. Select < ContentScheduleDto > ( )
. From < ContentScheduleDto > ( )
. WhereIn < ContentScheduleDto > ( x = > x . NodeId , batch ) ) ;
foreach ( var scheduleDto in scheduleDtos )
2018-11-05 17:20:26 +11:00
{
2018-11-08 16:33:19 +01:00
if ( ! result . TryGetValue ( scheduleDto . NodeId , out var col ) )
col = result [ scheduleDto . NodeId ] = new ContentScheduleCollection ( ) ;
col . Add ( new ContentSchedule ( scheduleDto . Id ,
LanguageRepository . GetIsoCodeById ( scheduleDto . LanguageId ) ? ? string . Empty ,
scheduleDto . Date ,
2018-11-14 09:16:22 +01:00
scheduleDto . Action = = ContentScheduleAction . Release . ToString ( )
? ContentScheduleAction . Release
: ContentScheduleAction . Expire ) ) ;
2018-11-05 17:20:26 +11:00
}
2018-11-08 16:33:19 +01:00
2018-11-05 17:20:26 +11:00
return result ;
2017-12-07 16:45:25 +01:00
}
2018-04-23 12:53:17 +02:00
private void SetVariations ( Content content , IDictionary < int , List < ContentVariation > > contentVariations , IDictionary < int , List < DocumentVariation > > documentVariations )
2018-04-12 22:53:04 +02:00
{
2018-04-23 12:53:17 +02:00
if ( contentVariations . TryGetValue ( content . VersionId , out var contentVariation ) )
foreach ( var v in contentVariation )
2018-06-20 14:18:57 +02:00
content . SetCultureInfo ( v . Culture , v . Name , v . Date ) ;
2019-02-06 17:28:48 +01:00
2018-04-23 12:53:17 +02:00
if ( content . PublishedVersionId > 0 & & contentVariations . TryGetValue ( content . PublishedVersionId , out contentVariation ) )
2019-01-31 14:03:09 +01:00
{
2018-04-23 12:53:17 +02:00
foreach ( var v in contentVariation )
2018-06-20 14:18:57 +02:00
content . SetPublishInfo ( v . Culture , v . Name , v . Date ) ;
2019-01-31 14:03:09 +01:00
}
2019-02-06 17:28:48 +01:00
2018-04-23 12:53:17 +02:00
if ( documentVariations . TryGetValue ( content . Id , out var documentVariation ) )
2019-02-05 14:13:03 +11:00
content . SetCultureEdited ( documentVariation . Where ( x = > x . Edited ) . Select ( x = > x . Culture ) ) ;
2018-04-12 22:53:04 +02:00
}
2018-04-23 12:53:17 +02:00
private IDictionary < int , List < ContentVariation > > GetContentVariations < T > ( List < TempContent < T > > temps )
2018-04-12 22:53:04 +02:00
where T : class , IContentBase
{
var versions = new List < int > ( ) ;
foreach ( var temp in temps )
{
versions . Add ( temp . VersionId ) ;
if ( temp . PublishedVersionId > 0 )
versions . Add ( temp . PublishedVersionId ) ;
}
2018-04-23 12:53:17 +02:00
if ( versions . Count = = 0 ) return new Dictionary < int , List < ContentVariation > > ( ) ;
2018-04-12 22:53:04 +02:00
var dtos = Database . FetchByGroups < ContentVersionCultureVariationDto , int > ( versions , 2000 , batch
= > Sql ( )
. Select < ContentVersionCultureVariationDto > ( )
. From < ContentVersionCultureVariationDto > ( )
. WhereIn < ContentVersionCultureVariationDto > ( x = > x . VersionId , batch ) ) ;
2018-04-23 12:53:17 +02:00
var variations = new Dictionary < int , List < ContentVariation > > ( ) ;
2018-04-12 22:53:04 +02:00
foreach ( var dto in dtos )
{
if ( ! variations . TryGetValue ( dto . VersionId , out var variation ) )
2018-04-23 12:53:17 +02:00
variations [ dto . VersionId ] = variation = new List < ContentVariation > ( ) ;
2018-04-12 22:53:04 +02:00
2018-04-23 12:53:17 +02:00
variation . Add ( new ContentVariation
2018-04-12 22:53:04 +02:00
{
2018-04-21 09:57:28 +02:00
Culture = LanguageRepository . GetIsoCodeById ( dto . LanguageId ) ,
2018-04-12 22:53:04 +02:00
Name = dto . Name ,
2018-09-25 18:05:14 +02:00
Date = dto . UpdateDate
2018-04-23 12:53:17 +02:00
} ) ;
}
return variations ;
}
private IDictionary < int , List < DocumentVariation > > GetDocumentVariations < T > ( List < TempContent < T > > temps )
where T : class , IContentBase
{
var ids = temps . Select ( x = > x . Id ) ;
var dtos = Database . FetchByGroups < DocumentCultureVariationDto , int > ( ids , 2000 , batch = >
Sql ( )
. Select < DocumentCultureVariationDto > ( )
. From < DocumentCultureVariationDto > ( )
. WhereIn < DocumentCultureVariationDto > ( x = > x . NodeId , batch ) ) ;
var variations = new Dictionary < int , List < DocumentVariation > > ( ) ;
foreach ( var dto in dtos )
{
if ( ! variations . TryGetValue ( dto . NodeId , out var variation ) )
variations [ dto . NodeId ] = variation = new List < DocumentVariation > ( ) ;
variation . Add ( new DocumentVariation
{
Culture = LanguageRepository . GetIsoCodeById ( dto . LanguageId ) ,
Edited = dto . Edited
2018-04-12 22:53:04 +02:00
} ) ;
}
return variations ;
}
2018-04-23 12:53:17 +02:00
private IEnumerable < ContentVersionCultureVariationDto > GetContentVariationDtos ( IContent content , bool publishing )
2018-04-12 22:53:04 +02:00
{
2018-04-23 12:53:17 +02:00
// create dtos for the 'current' (non-published) version, all cultures
2019-02-05 14:13:03 +11:00
// ReSharper disable once UseDeconstruction
foreach ( var cultureInfo in content . CultureInfos )
2018-04-12 22:53:04 +02:00
yield return new ContentVersionCultureVariationDto
{
VersionId = content . VersionId ,
2019-02-05 14:13:03 +11:00
LanguageId = LanguageRepository . GetIdByIsoCode ( cultureInfo . Culture ) ? ? throw new InvalidOperationException ( "Not a valid culture." ) ,
Culture = cultureInfo . Culture ,
Name = cultureInfo . Name ,
UpdateDate = content . GetUpdateDate ( cultureInfo . Culture ) ? ? DateTime . MinValue // we *know* there is a value
2018-04-12 22:53:04 +02:00
} ;
2018-04-23 12:53:17 +02:00
// if not publishing, we're just updating the 'current' (non-published) version,
// so there are no DTOs to create for the 'published' version which remains unchanged
2018-04-12 22:53:04 +02:00
if ( ! publishing ) yield break ;
2018-04-23 12:53:17 +02:00
// create dtos for the 'published' version, for published cultures (those having a name)
2019-02-05 14:13:03 +11:00
// ReSharper disable once UseDeconstruction
foreach ( var cultureInfo in content . PublishCultureInfos )
2018-04-12 22:53:04 +02:00
yield return new ContentVersionCultureVariationDto
{
VersionId = content . PublishedVersionId ,
2019-02-05 14:13:03 +11:00
LanguageId = LanguageRepository . GetIdByIsoCode ( cultureInfo . Culture ) ? ? throw new InvalidOperationException ( "Not a valid culture." ) ,
Culture = cultureInfo . Culture ,
Name = cultureInfo . Name ,
UpdateDate = content . GetPublishDate ( cultureInfo . Culture ) ? ? DateTime . MinValue // we *know* there is a value
2018-04-12 22:53:04 +02:00
} ;
}
2019-07-31 18:30:34 +10:00
private IEnumerable < DocumentCultureVariationDto > GetDocumentVariationDtos ( IContent content , HashSet < string > editedCultures )
2018-04-23 12:53:17 +02:00
{
2018-09-25 16:55:00 +02:00
var allCultures = content . AvailableCultures . Union ( content . PublishedCultures ) ; // union = distinct
foreach ( var culture in allCultures )
2019-07-31 18:30:34 +10:00
{
var dto = new DocumentCultureVariationDto
2018-04-23 12:53:17 +02:00
{
NodeId = content . Id ,
LanguageId = LanguageRepository . GetIdByIsoCode ( culture ) ? ? throw new InvalidOperationException ( "Not a valid culture." ) ,
Culture = culture ,
2018-08-28 15:00:45 +02:00
2018-09-25 16:55:00 +02:00
Name = content . GetCultureName ( culture ) ? ? content . GetPublishName ( culture ) ,
Available = content . IsCultureAvailable ( culture ) ,
2019-08-01 18:49:05 +10:00
Published = content . IsCulturePublished ( culture ) ,
// note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem
Edited = content . IsCultureAvailable ( culture ) & &
( ! content . IsCulturePublished ( culture ) | | ( editedCultures ! = null & & editedCultures . Contains ( culture ) ) )
2018-04-23 12:53:17 +02:00
} ;
2019-07-31 18:30:34 +10:00
yield return dto ;
}
2019-11-05 12:54:22 +01:00
2018-04-23 12:53:17 +02:00
}
private class ContentVariation
2018-04-12 22:53:04 +02:00
{
2018-04-21 09:57:28 +02:00
public string Culture { get ; set ; }
2018-04-12 22:53:04 +02:00
public string Name { get ; set ; }
2018-04-23 12:53:17 +02:00
public DateTime Date { get ; set ; }
}
private class DocumentVariation
{
public string Culture { get ; set ; }
public bool Edited { get ; set ; }
2018-04-12 22:53:04 +02:00
}
2017-12-07 16:45:25 +01:00
#region Utilities
2019-06-28 09:19:11 +02:00
private void SanitizeNames ( IContent content , bool publishing )
2018-05-02 14:52:00 +10:00
{
2018-06-20 14:18:57 +02:00
// a content item *must* have an invariant name, and invariant published name
// else we just cannot write the invariant rows (node, content version...) to the database
// ensure that we have an invariant name
// invariant content = must be there already, else throw
// variant content = update with default culture or anything really
EnsureInvariantNameExists ( content ) ;
2019-01-22 18:03:39 -05:00
// ensure that invariant name is unique
2018-06-20 14:18:57 +02:00
EnsureInvariantNameIsUnique ( content ) ;
// and finally,
// ensure that each culture has a unique node name
// no published name = not published
// else, it needs to be unique
EnsureVariantNamesAreUnique ( content , publishing ) ;
}
2018-06-01 15:20:16 +10:00
2019-02-05 14:13:03 +11:00
private void EnsureInvariantNameExists ( IContent content )
2018-06-20 14:18:57 +02:00
{
if ( content . ContentType . VariesByCulture ( ) )
2018-05-02 14:52:00 +10:00
{
2018-06-20 14:18:57 +02:00
// content varies by culture
// then it must have at least a variant name, else it makes no sense
2018-10-23 15:04:41 +02:00
if ( content . CultureInfos . Count = = 0 )
2018-05-02 17:44:14 +02:00
throw new InvalidOperationException ( "Cannot save content with an empty name." ) ;
2019-01-22 18:03:39 -05:00
// and then, we need to set the invariant name implicitly,
2018-06-20 14:18:57 +02:00
// using the default culture if it has a name, otherwise anything we can
var defaultCulture = LanguageRepository . GetDefaultIsoCode ( ) ;
2018-10-23 15:04:41 +02:00
content . Name = defaultCulture ! = null & & content . CultureInfos . TryGetValue ( defaultCulture , out var cultureName )
2018-10-17 18:09:52 +11:00
? cultureName . Name
2019-02-05 14:13:03 +11:00
: content . CultureInfos [ 0 ] . Name ;
2018-05-02 17:44:14 +02:00
}
2018-06-20 14:18:57 +02:00
else
2018-05-02 17:44:14 +02:00
{
2018-06-20 14:18:57 +02:00
// content is invariant, and invariant content must have an explicit invariant name
if ( string . IsNullOrWhiteSpace ( content . Name ) )
throw new InvalidOperationException ( "Cannot save content with an empty name." ) ;
2018-05-02 14:52:00 +10:00
}
}
2019-02-05 14:13:03 +11:00
private void EnsureInvariantNameIsUnique ( IContent content )
2018-06-20 14:18:57 +02:00
{
content . Name = EnsureUniqueNodeName ( content . ParentId , content . Name , content . Id ) ;
}
2017-12-07 16:45:25 +01:00
protected override string EnsureUniqueNodeName ( int parentId , string nodeName , int id = 0 )
{
return EnsureUniqueNaming = = false ? nodeName : base . EnsureUniqueNodeName ( parentId , nodeName , id ) ;
}
2018-06-20 14:18:57 +02:00
private SqlTemplate SqlEnsureVariantNamesAreUnique = > SqlContext . Templates . Get ( "Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique" , tsql = > tsql
2018-06-05 16:01:48 +02:00
. Select < ContentVersionCultureVariationDto > ( x = > x . Id , x = > x . Name , x = > x . LanguageId )
. From < ContentVersionCultureVariationDto > ( )
2018-06-20 14:18:57 +02:00
. InnerJoin < ContentVersionDto > ( ) . On < ContentVersionDto , ContentVersionCultureVariationDto > ( x = > x . Id , x = > x . VersionId )
. InnerJoin < NodeDto > ( ) . On < NodeDto , ContentVersionDto > ( x = > x . NodeId , x = > x . NodeId )
2018-06-05 16:01:48 +02:00
. Where < ContentVersionDto > ( x = > x . Current = = SqlTemplate . Arg < bool > ( "current" ) )
. Where < NodeDto > ( x = > x . NodeObjectType = = SqlTemplate . Arg < Guid > ( "nodeObjectType" ) & &
x . ParentId = = SqlTemplate . Arg < int > ( "parentId" ) & &
x . NodeId ! = SqlTemplate . Arg < int > ( "id" ) )
. OrderBy < ContentVersionCultureVariationDto > ( x = > x . LanguageId ) ) ;
2019-06-28 09:19:11 +02:00
private void EnsureVariantNamesAreUnique ( IContent content , bool publishing )
2018-06-01 15:20:16 +10:00
{
2018-10-23 15:04:41 +02:00
if ( ! EnsureUniqueNaming | | ! content . ContentType . VariesByCulture ( ) | | content . CultureInfos . Count = = 0 ) return ;
2018-06-01 15:20:16 +10:00
2018-06-20 14:18:57 +02:00
// get names per culture, at same level (ie all siblings)
var sql = SqlEnsureVariantNamesAreUnique . Sql ( true , NodeObjectTypeId , content . ParentId , content . Id ) ;
2018-06-01 15:20:16 +10:00
var names = Database . Fetch < CultureNodeName > ( sql )
. GroupBy ( x = > x . LanguageId )
. ToDictionary ( x = > x . Key , x = > x ) ;
if ( names . Count = = 0 ) return ;
2018-07-04 10:41:08 +02:00
// note: the code below means we are going to unique-ify every culture names, regardless
// of whether the name has changed (ie the culture has been updated) - some saving culture
// fr-FR could cause culture en-UK name to change - not sure that is clean
2019-02-05 14:13:03 +11:00
foreach ( var cultureInfo in content . CultureInfos )
2018-06-01 15:20:16 +10:00
{
2019-02-05 14:13:03 +11:00
var langId = LanguageRepository . GetIdByIsoCode ( cultureInfo . Culture ) ;
2018-06-01 15:20:16 +10:00
if ( ! langId . HasValue ) continue ;
2018-06-20 14:18:57 +02:00
if ( ! names . TryGetValue ( langId . Value , out var cultureNames ) ) continue ;
// get a unique name
var otherNames = cultureNames . Select ( x = > new SimilarNodeName { Id = x . Id , Name = x . Name } ) ;
2019-02-05 14:13:03 +11:00
var uniqueName = SimilarNodeName . GetUniqueName ( otherNames , 0 , cultureInfo . Name ) ;
2018-06-20 14:18:57 +02:00
2019-02-05 14:13:03 +11:00
if ( uniqueName = = content . GetCultureName ( cultureInfo . Culture ) ) continue ;
2018-06-20 14:18:57 +02:00
// update the name, and the publish name if published
2019-02-05 14:13:03 +11:00
content . SetCultureName ( uniqueName , cultureInfo . Culture ) ;
if ( publishing & & content . PublishCultureInfos . ContainsKey ( cultureInfo . Culture ) )
content . SetPublishInfo ( cultureInfo . Culture , uniqueName , DateTime . Now ) ; //TODO: This is weird, this call will have already been made in the SetCultureName
2018-06-01 15:20:16 +10:00
}
}
2019-02-05 01:58:18 +11:00
// ReSharper disable once ClassNeverInstantiated.Local
2018-06-01 15:20:16 +10:00
private class CultureNodeName
{
public int Id { get ; set ; }
public string Name { get ; set ; }
public int LanguageId { get ; set ; }
}
2017-12-07 16:45:25 +01:00
#endregion
}
}