2021-03-11 19:35:43 +11:00
using System ;
2017-12-07 16:45:25 +01:00
using System.Collections.Generic ;
2021-09-14 22:13:39 +02:00
using System.Globalization ;
2017-12-07 16:45:25 +01:00
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-03-05 15:36:27 +01:00
using Umbraco.Cms.Core ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Cache ;
2021-04-20 12:17:11 +02:00
using Umbraco.Cms.Core.Events ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Membership ;
2021-05-11 14:33:49 +02:00
using Umbraco.Cms.Core.Notifications ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Persistence ;
using Umbraco.Cms.Core.Persistence.Querying ;
using Umbraco.Cms.Core.Persistence.Repositories ;
using Umbraco.Cms.Core.PropertyEditors ;
2021-02-15 11:41:12 +01:00
using Umbraco.Cms.Core.Scoping ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Serialization ;
using Umbraco.Cms.Core.Services ;
2021-02-12 13:36:50 +01:00
using Umbraco.Cms.Infrastructure.Persistence.Dtos ;
using Umbraco.Cms.Infrastructure.Persistence.Factories ;
using Umbraco.Cms.Infrastructure.Persistence.Querying ;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2017-12-07 16:45:25 +01:00
2021-02-12 13:36:50 +01:00
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
2017-12-07 16:45:25 +01:00
{
/// <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 ,
2021-07-12 15:28:46 -06:00
PropertyEditorCollection propertyEditors ,
2019-12-11 08:13:51 +01:00
DataValueReferenceFactoryCollection dataValueReferenceFactories ,
2020-11-17 20:27:10 +01:00
IDataTypeService dataTypeService ,
2021-04-20 12:17:11 +02:00
IJsonSerializer serializer ,
IEventAggregator eventAggregator )
: base ( scopeAccessor , appCaches , logger , languageRepository , relationRepository , relationTypeRepository , propertyEditors , dataValueReferenceFactories , dataTypeService , eventAggregator )
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 )
{
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
2021-07-12 16:26:19 -06:00
IEnumerable < PropertyDataDto > propertyDataDtos = PropertyFactory . BuildDtos ( entity . ContentType . Variations , entity . VersionId , entity . PublishedVersionId , entity . Properties , LanguageRepository , out var edited , out HashSet < string > editedCultures ) ;
foreach ( PropertyDataDto propertyDataDto in propertyDataDtos )
{
2017-12-07 16:45:25 +01:00
Database . Insert ( propertyDataDto ) ;
2021-07-12 16:26:19 -06:00
}
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 )
2021-07-12 16:26:19 -06:00
{
2018-04-23 12:53:17 +02:00
edited = true ;
2021-07-12 16:26:19 -06:00
}
2018-04-23 12:53:17 +02:00
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 )
2021-07-12 16:26:19 -06:00
{
2017-12-07 16:45:25 +01:00
dto . Published = true ;
2021-07-12 16:26:19 -06:00
}
2017-12-07 16:45:25 +01:00
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
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-04-23 12:53:17 +02:00
// names also impact 'edited'
2019-02-05 14:13:03 +11:00
// ReSharper disable once UseDeconstruction
2021-07-12 16:26:19 -06:00
foreach ( ContentCultureInfos cultureInfo in entity . CultureInfos )
{
2019-06-28 09:19:11 +02:00
if ( cultureInfo . Name ! = entity . GetPublishName ( cultureInfo . Culture ) )
2021-07-12 16:26:19 -06:00
{
2021-07-13 09:52:31 -06:00
( editedCultures ? ? = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ) . Add ( cultureInfo . Culture ) ;
2021-07-12 16:26:19 -06:00
}
}
2018-04-23 12:53:17 +02:00
2021-07-13 09:52:31 -06:00
// refresh content
entity . SetCultureEdited ( editedCultures ) ;
// bump dates to align cultures to version
entity . AdjustDates ( contentVersionDto . VersionDate , publishing ) ;
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
}
2017-12-07 16:45:25 +01:00
// trigger here, before we reset Published etc
2021-04-20 12:17:11 +02:00
OnUowRefreshedEntity ( new ContentRefreshNotification ( entity , new EventMessages ( ) ) ) ;
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 ( ) ;
2021-10-12 09:18:42 +01:00
var editedSnapshot = entity . Edited ;
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 ( ) )
2021-07-12 16:26:19 -06:00
{
2017-12-07 16:45:25 +01:00
return ; // no change to save, do nothing, don't even update dates
2021-07-12 16:26:19 -06:00
}
2017-12-07 16:45:25 +01:00
// 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
2021-07-12 16:26:19 -06: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
2021-07-12 16:26:19 -06:00
var versionToDelete = publishing ? entity . PublishedVersionId : entity . VersionId ;
// insert property data
ReplacePropertyValues ( entity , versionToDelete , publishing ? entity . PublishedVersionId : 0 , out var edited , out HashSet < string > 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 )
2021-07-12 16:26:19 -06:00
{
2020-04-07 15:02:08 +10:00
edited = true ;
2021-07-12 16:26:19 -06:00
}
2020-04-07 15:02:08 +10:00
2021-10-12 09:18:42 +01:00
// To establish the new value of "edited" we compare all properties publishedValue to editedValue and look
Merge v8/dev 20-10-2021 (#11426)
* Adjust icon in umb-checkbox and ensure icon is centered
* Missing nl translation for blockEditor_addBlock
* Implement icon parameter for doctype editor (#11008)
* fix: implement icon parameter for doctype editor
issue #10108
* fix: move color from icon to class attribute
* fix: removed defined colors, defaulting to the standard dark grey (ie "no color picked" in icon picker)
* cleaned up unused dependencies, double quotes to single, removed unused 'color' param from the create methods, and use shorthand object creation in createDocType (if the key has the same name as the variable passed as a prop, we only need to pass the key name)
* fix comment
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
* Align sortable handle vertically in multivalues prevalue editor
* 10341: Use different picker for content types (#10896)
* 10341: Use different picker for content types
* use es6 where possible (inc removing underscore for teeny tiny performance improvement)
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
* Falling back to contentTypeName when Block List label is empty (#10963)
* Falling back to contentTypeName when Block List label is empty
* Adding $contentTypeName variable for Block List labels
* Fix incorrect attribute
* Grid: Add button styling fix (#10978)
* Add missing focus styling
* Ensure add button is perfectly rounded and remove unused / uneeded CSS.
* Remove redundant border-color property
* Revert removal of unused css
Co-authored-by: BatJan <jaskov@gmail.com>
Co-authored-by: Jan Skovgaard Olsen <jso@co3.dk>
* Create content template localization (#10945)
* Don't use self-closing element for custom HTML elements
* Use button element for close/cancel in copy dialog
* Update localization of "createBlueprintFrom"
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
* Cleanup examine search results, and adds ability to toggle fields (#9141)
* Cleanup examine search results, and adds ability to toggle fields
* update table to use joinarray filter with one-time binding to avoid recalculating filter values, updated filter to not explode when array arg is null
* fix failing tests - improve filter to not fail on non-array params, update tests accordingly
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
* Add EntityController GetUrlsByUdis
Enables loading multiple URLs in a single request for Media & Documents
* Update content picker to use GetUrlsByUdis
* Allows replacing MainDom with alternate DB
There are some cases where there is a complex hosting strategy and folks want a readonly database and are hosting on Azure. In that case, it is not entirely possible to have a readonly Umbraco database because SqlMainDom is required and part of that requirement is to have read/write access to the umbraco key value table.
This PR allows for the default MainDom to be replaced and to allow for an SqlMainDomLock to use an alternate connection string so that a separate read/write database can be used.
* Remove inherited property group id/key when local properties are added (#11231)
* Remove inherited property group id/key when local properties are added
* Rebind saved content type values
* Remove inherited from save group
* Rename parameter for clarity
* Removes annoying wait text, which causes layout jank
* v8: Backoffice Welsh language translation updates (#11240)
* Updated the Welsh language file to include newly added keys (based on the en us language file)
* Updated the searchInputDescription key
* Updated the endTitle key
* Use medium sized overlay
* Use umb-icon component for icons in content type groups and tabs
* fixes wrong reference to enterSubmitFolder method in ng-keydown
* 11251: Don't add default dashboard to url
* Fix preview of SVG when height and width not are set
* If caching a published document, make sure you use the published Name… (#11313)
* If caching a published document, make sure you use the published Name. Closes #11074.
* Fix case of new node
Co-authored-by: Moore, Douglas S <Douglas.Moore@opm.gov>
* Added missing Italian translations (#11197)
* Resolve incorrect ContentSavedState for failed publish
Closes #11290 (for v8)
* add modelValue validation for server to correctly update validation errors
* 11048: Bugfix for groups and properties that get replaced (#11257)
(cherry picked from commit 1605dc10bd91caa46d4bec1946a392f1d47c993d)
* Icon fallback to `icon-document` for existing document types (#11283)
* Align create buttons styling (#11352)
* Added button for cancelling dictionary create action
* Use hideMenu
* Align dictionary create with the other creates
* Align import documenttype
* Align for data type folder create
* Align document type create buttons
* Forgot small ng-show
* Align create media folder buttons
* Align create macro buttons
* Align create relation buttons
* Align create partial view macro folder buttons
* Align partial view folder create buttons
* Align create scripts folder buttons
* Align create scripts folder buttons
* Use primary instead of success
* V8: Duplicate MemberGroup names cause MemberGroup mixup (#11291)
* Prevented duplicate member group names
* Added English lang
* Updated 'Exist' typo
* add labels in FR and NL
* Adding property group aliases to ex.message
* Adding invalid prop group aliases as ModelState errors, so we don't introduce breaking changes
* Pointing the actual reason for invalidating composition
* Validate all content type dependencies and throw a single InvalidCompositionException
* Rename based on review comments
* Update composition validation error messages
* Update InvalidCompositionException message
* Allow switching property editor from numeric to slider (#11287)
* Make it possible to change from numeric/decimal property editor to slider without breaking editor
* Formatting
* Enables friendly pasting in multipletextbox
* UI API docs: Added reset rules for .close class
* UI API docs: Fixed incorrect method name
* 11331: Check property on instance if id is not set yet
* Fixed cypress tests
Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com>
Co-authored-by: Corné Strijkert <cornestrijkert@hotmail.com>
Co-authored-by: Søren Gregersen <soreng@gmail.com>
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
Co-authored-by: patrickdemooij9 <patrickdemooij98@hotmail.com>
Co-authored-by: Callum Whyte <hey@callumwhyte.com>
Co-authored-by: Jan Skovgaard <1932158+BatJan@users.noreply.github.com>
Co-authored-by: BatJan <jaskov@gmail.com>
Co-authored-by: Jan Skovgaard Olsen <jso@co3.dk>
Co-authored-by: Søren Kottal <sk@ecreo.dk>
Co-authored-by: Paul Johnson <pmj@umbraco.com>
Co-authored-by: Shannon <sdeminick@gmail.com>
Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
Co-authored-by: Ronald Barendse <ronald@barend.se>
Co-authored-by: Owain Jones <owain.jones@method4.co.uk>
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Doug Moore <lordscarlet@idledreams.net>
Co-authored-by: Moore, Douglas S <Douglas.Moore@opm.gov>
Co-authored-by: Martino Gabrielli <39855999+ZioTino@users.noreply.github.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Jamie Townsend <jamie.townsend@unitstack.co.uk>
Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
Co-authored-by: Anders Bjerner <abjerner@skybrud.dk>
2021-10-20 13:12:46 +02:00
// for differences.
//
// If we SaveAndPublish but the publish fails (e.g. already scheduled for release)
// we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison.
//
// This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save
// would change edited back to false.
if ( ! publishing & & editedSnapshot )
{
edited = true ;
}
// To establish the new value of "edited" we compare all properties publishedValue to editedValue and look
2021-10-12 09:18:42 +01:00
// for differences.
//
// If we SaveAndPublish but the publish fails (e.g. already scheduled for release)
// we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison.
//
// This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save
// would change edited back to false.
if ( ! publishing & & editedSnapshot )
{
edited = true ;
}
2020-04-07 15:02:08 +10:00
if ( entity . ContentType . VariesByCulture ( ) )
{
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
2021-07-12 16:26:19 -06:00
foreach ( var cultureInfo in entity . CultureInfos )
{
2020-04-07 15:02:08 +10:00
if ( cultureInfo . Name ! = entity . GetPublishName ( cultureInfo . Culture ) )
{
edited = true ;
2021-07-13 09:52:31 -06:00
( editedCultures ? ? = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ) . Add ( cultureInfo . Culture ) ;
2020-04-07 15:02:08 +10:00
// 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.
}
2021-07-12 16:26:19 -06:00
}
2020-04-07 15:02:08 +10:00
2021-07-13 09:52:31 -06:00
// refresh content
entity . SetCultureEdited ( editedCultures ) ;
// bump dates to align cultures to version
entity . AdjustDates ( contentVersionDto . VersionDate , publishing ) ;
2020-04-07 15:02:08 +10:00
// 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
// 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 )
2021-07-12 16:26:19 -06:00
{
2020-04-07 15:02:08 +10:00
dto . Published = true ;
2021-07-12 16:26:19 -06:00
}
2020-04-07 15:02:08 +10:00
else if ( entity . PublishedState = = PublishedState . Unpublishing )
2021-07-12 16:26:19 -06:00
{
2020-04-07 15:02:08 +10:00
dto . Published = false ;
2021-07-12 16:26:19 -06:00
}
2020-04-07 15:02:08 +10:00
entity . Edited = dto . Edited = ! dto . Published | | edited ; // if not published, always edited
Database . Update ( dto ) ;
//update the schedule
2021-07-12 16:26:19 -06:00
if ( entity . IsPropertyDirty ( nameof ( entity . ContentSchedule ) ) )
{
2020-04-07 15:02:08 +10:00
PersistContentSchedule ( entity , true ) ;
2021-07-12 16:26:19 -06:00
}
2020-04-07 15:02:08 +10:00
// 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 )
2021-07-12 16:26:19 -06:00
{
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2021-07-12 16:26:19 -06:00
}
2018-04-12 22:53:04 +02:00
}
2017-12-07 16:45:25 +01:00
// trigger here, before we reset Published etc
2021-04-20 12:17:11 +02:00
OnUowRefreshedEntity ( new ContentRefreshNotification ( entity , new EventMessages ( ) ) ) ;
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 )
{
2021-04-20 12:17:11 +02:00
// Raise event first else potential FK issues
OnUowRemovingEntity ( 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 ;
2021-09-14 22:13:39 +02:00
var ids = content . Path . Split ( Constants . CharArrays . Comma ) . Skip ( 1 ) . Select ( s = > int . Parse ( s , CultureInfo . InvariantCulture ) ) ;
2017-12-07 16:45:25 +01:00
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
2021-03-25 12:54:46 +01:00
public bool RecycleBinSmells ( )
{
var cache = _appCaches . RuntimeCache ;
var cacheKey = CacheKeys . ContentRecycleBinCacheKey ;
2021-03-25 13:29:27 +01:00
// always cache either true or false
2021-04-14 16:55:37 +02:00
return cache . GetCacheItem < bool > ( cacheKey , ( ) = > CountChildren ( RecycleBinId ) > 0 ) ;
2021-03-25 12:54:46 +01:00
}
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!
2021-03-11 19:35:43 +11:00
// This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this
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 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
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
var cached = IsolatedCache . GetCacheItem < IContent > ( RepositoryCacheKeys . GetKey < IContent , int > ( dto . NodeId ) ) ;
2017-12-07 16:45:25 +01:00
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
} ;
2021-07-12 16:26:19 -06:00
if ( dto . Published )
temp . Template2Id = dto . PublishedVersionDto . TemplateId ;
2017-12-07 16:45:25 +01:00
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 ) ;
}
2021-07-12 16:26:19 -06: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
2021-07-12 16:26:19 -06:00
if ( ! publishing )
yield break ;
2018-04-12 22:53:04 +02:00
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
{
2021-07-12 16:26:19 -06: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 ) ;
2021-07-12 16:26:19 -06:00
if ( names . Count = = 0 )
return ;
2018-06-01 15:20:16 +10:00
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 ) ;
2021-07-12 16:26:19 -06:00
if ( ! langId . HasValue )
continue ;
if ( ! names . TryGetValue ( langId . Value , out var cultureNames ) )
continue ;
2018-06-20 14:18:57 +02:00
// 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
2021-07-12 16:26:19 -06: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
}
}