Merge branch 'v8/dev' into v8/contrib
This commit is contained in:
@@ -7,6 +7,5 @@
|
||||
-->
|
||||
<packageSources>
|
||||
<add key="UmbracoCoreMyGet" value="https://www.myget.org/F/umbracocore/api/v3/index.json" />
|
||||
<add key="ExamineAppVeyor" value="https://ci.appveyor.com/nuget/examine-f73l6qv0oqfh/" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class SqlTemplates
|
||||
{
|
||||
public static class VersionableRepository
|
||||
{
|
||||
public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds";
|
||||
public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion";
|
||||
public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions";
|
||||
public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName";
|
||||
public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder";
|
||||
public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode";
|
||||
public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,19 @@ namespace Umbraco.Core
|
||||
|
||||
#endregion
|
||||
|
||||
internal static bool IsMoving(this IContentBase entity)
|
||||
{
|
||||
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
|
||||
// When this occurs, only Path + Level + UpdateDate are being changed. 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.IsPropertyDirty(nameof(entity.Path))
|
||||
&& entity.IsPropertyDirty(nameof(entity.Level))
|
||||
&& entity.IsPropertyDirty(nameof(entity.UpdateDate));
|
||||
|
||||
return isMoving;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes characters that are not valid XML characters from all entity properties
|
||||
/// of type string. See: http://stackoverflow.com/a/961504/5018
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class ContentDataIntegrityReport
|
||||
{
|
||||
public ContentDataIntegrityReport(IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> detectedIssues)
|
||||
{
|
||||
DetectedIssues = detectedIssues;
|
||||
}
|
||||
|
||||
public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed);
|
||||
|
||||
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> DetectedIssues { get; }
|
||||
|
||||
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> FixedIssues
|
||||
=> DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
public enum IssueType
|
||||
{
|
||||
/// <summary>
|
||||
/// The item's level and path are inconsistent with it's parent's path and level
|
||||
/// </summary>
|
||||
InvalidPathAndLevelByParentId,
|
||||
|
||||
/// <summary>
|
||||
/// The item's path doesn't contain all required parts
|
||||
/// </summary>
|
||||
InvalidPathEmpty,
|
||||
|
||||
/// <summary>
|
||||
/// The item's path parts are inconsistent with it's level value
|
||||
/// </summary>
|
||||
InvalidPathLevelMismatch,
|
||||
|
||||
/// <summary>
|
||||
/// The item's path does not end with it's own ID
|
||||
/// </summary>
|
||||
InvalidPathById,
|
||||
|
||||
/// <summary>
|
||||
/// The item's path does not have it's parent Id as the 2nd last entry
|
||||
/// </summary>
|
||||
InvalidPathByParentId,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class ContentDataIntegrityReportEntry
|
||||
{
|
||||
public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType)
|
||||
{
|
||||
IssueType = issueType;
|
||||
}
|
||||
|
||||
public ContentDataIntegrityReport.IssueType IssueType { get; }
|
||||
public bool Fixed { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
public class ContentDataIntegrityReportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Set to true to try to automatically resolve data integrity issues
|
||||
/// </summary>
|
||||
public bool FixIssues { get; set; }
|
||||
|
||||
// TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc...
|
||||
// things like Tag data consistency, etc...
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
@@ -13,27 +14,41 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
private string _name;
|
||||
private string _alias;
|
||||
private bool _isBidrectional;
|
||||
private bool _isBidirectional;
|
||||
private Guid? _parentObjectType;
|
||||
private Guid? _childObjectType;
|
||||
|
||||
//TODO: Should we put back the broken ctors with obsolete attributes?
|
||||
|
||||
[Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")]
|
||||
public RelationType(string alias, string name)
|
||||
: this(name, alias, false, null, null)
|
||||
: this(name: name, alias: alias, false, null, null)
|
||||
{
|
||||
}
|
||||
|
||||
public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
|
||||
if (alias == null) throw new ArgumentNullException(nameof(alias));
|
||||
if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias));
|
||||
|
||||
_name = name;
|
||||
_alias = alias;
|
||||
_isBidrectional = isBidrectional;
|
||||
_isBidirectional = isBidrectional;
|
||||
_parentObjectType = parentObjectType;
|
||||
_childObjectType = childObjectType;
|
||||
}
|
||||
|
||||
[Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")]
|
||||
public RelationType(Guid childObjectType, Guid parentObjectType, string alias)
|
||||
: this(name: alias, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")]
|
||||
public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name)
|
||||
: this(name: name, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Name of the RelationType
|
||||
@@ -61,8 +76,8 @@ namespace Umbraco.Core.Models
|
||||
[DataMember]
|
||||
public bool IsBidirectional
|
||||
{
|
||||
get => _isBidrectional;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _isBidrectional, nameof(IsBidirectional));
|
||||
get => _isBidirectional;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
|
||||
public static IRelationType BuildEntity(RelationTypeDto dto)
|
||||
{
|
||||
var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ChildObjectType, dto.ParentObjectType);
|
||||
var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -18,6 +18,48 @@ namespace Umbraco.Core.Persistence
|
||||
/// </summary>
|
||||
public static partial class NPocoDatabaseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Iterates over the result of a paged data set with a db reader
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="database"></param>
|
||||
/// <param name="pageSize">
|
||||
/// The number of rows to load per page
|
||||
/// </param>
|
||||
/// <param name="sql"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to
|
||||
/// iterate over each row with a reader using Query vs Fetch.
|
||||
/// </remarks>
|
||||
internal static IEnumerable<T> QueryPaged<T>(this IDatabase database, long pageSize, Sql sql)
|
||||
{
|
||||
var sqlString = sql.SQL;
|
||||
var sqlArgs = sql.Arguments;
|
||||
|
||||
int? itemCount = null;
|
||||
long pageIndex = 0;
|
||||
do
|
||||
{
|
||||
// Get the paged queries
|
||||
database.BuildPageQueries<T>(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage);
|
||||
|
||||
// get the item count once
|
||||
if (itemCount == null)
|
||||
{
|
||||
itemCount = database.ExecuteScalar<int>(sqlCount, sqlArgs);
|
||||
}
|
||||
pageIndex++;
|
||||
|
||||
// iterate over rows without allocating all items to memory (Query vs Fetch)
|
||||
foreach (var row in database.Query<T>(sqlPage, sqlArgs))
|
||||
{
|
||||
yield return row;
|
||||
}
|
||||
|
||||
} while ((pageIndex * pageSize) < itemCount);
|
||||
}
|
||||
|
||||
// NOTE
|
||||
//
|
||||
// proper way to do it with TSQL and SQLCE
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
@@ -77,5 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// <remarks>Here, <paramref name="filter"/> can be null but <paramref name="ordering"/> cannot.</remarks>
|
||||
IEnumerable<TEntity> GetPage(IQuery<TEntity> query, long pageIndex, int pageSize, out long totalRecords,
|
||||
IQuery<TEntity> filter, Ordering ordering);
|
||||
|
||||
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
// gets all version ids, current first
|
||||
public virtual IEnumerable<int> GetVersionIds(int nodeId, int maxRows)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql =>
|
||||
tsql.Select<ContentVersionDto>(x => x.Id)
|
||||
.From<ContentVersionDto>()
|
||||
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId"))
|
||||
@@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
// TODO: test object node type?
|
||||
|
||||
// get the version we want to delete
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql =>
|
||||
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.Id == SqlTemplate.Arg<int>("versionId"))
|
||||
);
|
||||
var versionDto = Database.Fetch<ContentVersionDto>(template.Sql(new { versionId })).FirstOrDefault();
|
||||
@@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
// TODO: test object node type?
|
||||
|
||||
// get the versions we want to delete, excluding the current one
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql =>
|
||||
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
|
||||
);
|
||||
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
|
||||
@@ -403,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
}
|
||||
|
||||
// content type alias is invariant
|
||||
if(ordering.OrderBy.InvariantEquals("contentTypeAlias"))
|
||||
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
|
||||
{
|
||||
var joins = Sql()
|
||||
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
|
||||
@@ -477,6 +477,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
IQuery<TEntity> filter,
|
||||
Ordering ordering);
|
||||
|
||||
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
|
||||
{
|
||||
var report = new Dictionary<int, ContentDataIntegrityReportEntry>();
|
||||
|
||||
var sql = SqlContext.Sql()
|
||||
.Select<NodeDto>()
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
var nodesToRebuild = new Dictionary<int, List<NodeDto>>();
|
||||
var validNodes = new Dictionary<int, NodeDto>();
|
||||
var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia};
|
||||
var currentParentIds = new HashSet<int>(rootIds);
|
||||
var prevParentIds = currentParentIds;
|
||||
var lastLevel = -1;
|
||||
|
||||
// use a forward cursor (query)
|
||||
foreach (var node in Database.Query<NodeDto>(sql))
|
||||
{
|
||||
if (node.Level != lastLevel)
|
||||
{
|
||||
// changing levels
|
||||
prevParentIds = currentParentIds;
|
||||
currentParentIds = null;
|
||||
lastLevel = node.Level;
|
||||
}
|
||||
|
||||
if (currentParentIds == null)
|
||||
{
|
||||
// we're reset
|
||||
currentParentIds = new HashSet<int>();
|
||||
}
|
||||
|
||||
currentParentIds.Add(node.NodeId);
|
||||
|
||||
// paths parts without the roots
|
||||
var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray();
|
||||
|
||||
if (!prevParentIds.Contains(node.ParentId))
|
||||
{
|
||||
// invalid, this will be because the level is wrong (which prob means path is wrong too)
|
||||
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId));
|
||||
AppendNodeToFix(nodesToRebuild, node);
|
||||
}
|
||||
else if (pathParts.Length == 0)
|
||||
{
|
||||
// invalid path
|
||||
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty));
|
||||
AppendNodeToFix(nodesToRebuild, node);
|
||||
}
|
||||
else if (pathParts.Length != node.Level)
|
||||
{
|
||||
// invalid, either path or level is wrong
|
||||
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch));
|
||||
AppendNodeToFix(nodesToRebuild, node);
|
||||
}
|
||||
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById));
|
||||
AppendNodeToFix(nodesToRebuild, node);
|
||||
}
|
||||
else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId));
|
||||
AppendNodeToFix(nodesToRebuild, node);
|
||||
}
|
||||
else
|
||||
{
|
||||
// it's valid!
|
||||
|
||||
// don't track unless we are configured to fix
|
||||
if (options.FixIssues)
|
||||
validNodes.Add(node.NodeId, node);
|
||||
}
|
||||
}
|
||||
|
||||
var updated = new List<NodeDto>();
|
||||
|
||||
if (options.FixIssues)
|
||||
{
|
||||
// iterate all valid nodes to see if these are parents for invalid nodes
|
||||
foreach (var (nodeId, node) in validNodes)
|
||||
{
|
||||
if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue;
|
||||
|
||||
// now we can try to rebuild the invalid paths.
|
||||
|
||||
foreach (var invalidNode in invalidNodes)
|
||||
{
|
||||
invalidNode.Level = (short)(node.Level + 1);
|
||||
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
|
||||
updated.Add(invalidNode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in updated)
|
||||
{
|
||||
Database.Update(node);
|
||||
if (report.TryGetValue(node.NodeId, out var entry))
|
||||
entry.Fixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new ContentDataIntegrityReport(report);
|
||||
}
|
||||
|
||||
private static void AppendNodeToFix(IDictionary<int, List<NodeDto>> nodesToRebuild, NodeDto node)
|
||||
{
|
||||
if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds))
|
||||
childIds.Add(node);
|
||||
else
|
||||
nodesToRebuild[node.ParentId] = new List<NodeDto> { node };
|
||||
}
|
||||
|
||||
// here, filter can be null and ordering cannot
|
||||
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity> query,
|
||||
long pageIndex, int pageSize, out long totalRecords,
|
||||
@@ -770,7 +887,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql
|
||||
.Select<NodeDto>(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType") && x.ParentId == SqlTemplate.Arg<int>("parentId")));
|
||||
@@ -783,7 +900,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected virtual int GetNewChildSortOrder(int parentId, int first)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql =>
|
||||
tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From<NodeDto>().Where<NodeDto>(x => x.ParentId == SqlTemplate.Arg<int>("parentId") && x.NodeObjectType == NodeObjectTypeId)
|
||||
);
|
||||
|
||||
@@ -792,7 +909,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected virtual NodeDto GetParentNodeDto(int parentId)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql =>
|
||||
tsql.Select<NodeDto>().From<NodeDto>().Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("parentId"))
|
||||
);
|
||||
|
||||
@@ -801,7 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected virtual int GetReservedId(Guid uniqueId)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql =>
|
||||
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql =>
|
||||
tsql.Select<NodeDto>(x => x.NodeId).From<NodeDto>().Where<NodeDto>(x => x.UniqueId == SqlTemplate.Arg<Guid>("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
|
||||
);
|
||||
var id = Database.ExecuteScalar<int?>(template.Sql(new { uniqueId = uniqueId }));
|
||||
|
||||
@@ -321,7 +321,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
.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"))
|
||||
.Where<DocumentVersionDto>( x => !x.Published)
|
||||
.Where<DocumentVersionDto>(x => !x.Published)
|
||||
);
|
||||
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
|
||||
foreach (var versionDto in versionDtos)
|
||||
@@ -519,8 +519,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected override void PersistUpdatedItem(IContent entity)
|
||||
{
|
||||
var entityBase = entity as EntityBase;
|
||||
var isEntityDirty = entityBase != null && entityBase.IsDirty();
|
||||
var isEntityDirty = entity.IsDirty();
|
||||
|
||||
// check if we need to make any database changes at all
|
||||
if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished)
|
||||
@@ -535,29 +534,41 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
// update
|
||||
entity.UpdatingEntity();
|
||||
|
||||
// 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();
|
||||
// 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
|
||||
|
||||
|
||||
var publishing = entity.PublishedState == PublishedState.Publishing;
|
||||
|
||||
// check if we need to create a new version
|
||||
if (publishing && entity.PublishedVersionId > 0)
|
||||
if (!isMoving)
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
|
||||
// sanitize names
|
||||
SanitizeNames(entity, publishing);
|
||||
// sanitize names
|
||||
SanitizeNames(entity, publishing);
|
||||
|
||||
// ensure that strings don't contain characters that are invalid in xml
|
||||
// TODO: do we really want to keep doing this here?
|
||||
entity.SanitizeEntityPropertiesForXmlStorage();
|
||||
// ensure that strings don't contain characters that are invalid in xml
|
||||
// TODO: do we really want to keep doing this here?
|
||||
entity.SanitizeEntityPropertiesForXmlStorage();
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// create the dto
|
||||
@@ -568,146 +579,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
nodeDto.ValidatePathWithException();
|
||||
Database.Update(nodeDto);
|
||||
|
||||
// update the content dto
|
||||
Database.Update(dto.ContentDto);
|
||||
|
||||
// update the content & document version dtos
|
||||
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
|
||||
var documentVersionDto = dto.DocumentVersionDto;
|
||||
if (publishing)
|
||||
if (!isMoving)
|
||||
{
|
||||
documentVersionDto.Published = true; // now published
|
||||
contentVersionDto.Current = false; // no more current
|
||||
}
|
||||
Database.Update(contentVersionDto);
|
||||
Database.Update(documentVersionDto);
|
||||
// update the content dto
|
||||
Database.Update(dto.ContentDto);
|
||||
|
||||
// and, if publishing, insert new content & document version dtos
|
||||
if (publishing)
|
||||
{
|
||||
entity.PublishedVersionId = entity.VersionId;
|
||||
|
||||
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
|
||||
|
||||
documentVersionDto.Published = false; // non-published version
|
||||
Database.Insert(documentVersionDto);
|
||||
}
|
||||
|
||||
// replace the property data (rather than updating)
|
||||
// only need to delete for the version that existed, the new version (if any) has no property data yet
|
||||
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
|
||||
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
|
||||
Database.Execute(deletePropertyDataSql);
|
||||
|
||||
// insert property data
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
|
||||
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
|
||||
foreach (var propertyDataDto in propertyDataDtos)
|
||||
Database.Insert(propertyDataDto);
|
||||
|
||||
// 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
|
||||
// update the content & document version dtos
|
||||
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
|
||||
var documentVersionDto = dto.DocumentVersionDto;
|
||||
if (publishing)
|
||||
entity.AdjustDates(contentVersionDto.VersionDate);
|
||||
{
|
||||
documentVersionDto.Published = true; // now published
|
||||
contentVersionDto.Current = false; // no more current
|
||||
}
|
||||
Database.Update(contentVersionDto);
|
||||
Database.Update(documentVersionDto);
|
||||
|
||||
// names also impact 'edited'
|
||||
// ReSharper disable once UseDeconstruction
|
||||
foreach (var cultureInfo in entity.CultureInfos)
|
||||
if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture))
|
||||
{
|
||||
edited = true;
|
||||
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture);
|
||||
// and, if publishing, insert new content & document version dtos
|
||||
if (publishing)
|
||||
{
|
||||
entity.PublishedVersionId = entity.VersionId;
|
||||
|
||||
// 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.
|
||||
}
|
||||
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
|
||||
|
||||
// replace the content version variations (rather than updating)
|
||||
documentVersionDto.Published = false; // non-published version
|
||||
Database.Insert(documentVersionDto);
|
||||
}
|
||||
|
||||
// replace the property data (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);
|
||||
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
|
||||
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
|
||||
Database.Execute(deletePropertyDataSql);
|
||||
|
||||
// replace the document version variations (rather than updating)
|
||||
var deleteDocumentVariations = Sql().Delete<DocumentCultureVariationDto>().Where<DocumentCultureVariationDto>(x => x.NodeId == entity.Id);
|
||||
Database.Execute(deleteDocumentVariations);
|
||||
// insert property data
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
|
||||
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
|
||||
foreach (var propertyDataDto in propertyDataDtos)
|
||||
Database.Insert(propertyDataDto);
|
||||
|
||||
// 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)
|
||||
// if !publishing, we may have a new name != current publish name,
|
||||
// also impacts 'edited'
|
||||
if (!publishing && entity.PublishName != entity.Name)
|
||||
edited = true;
|
||||
|
||||
// insert content variations
|
||||
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
|
||||
if (entity.ContentType.VariesByCulture())
|
||||
{
|
||||
// bump dates to align cultures to version
|
||||
if (publishing)
|
||||
entity.AdjustDates(contentVersionDto.VersionDate);
|
||||
|
||||
// insert document variations
|
||||
Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
|
||||
// names also impact 'edited'
|
||||
// ReSharper disable once UseDeconstruction
|
||||
foreach (var cultureInfo in entity.CultureInfos)
|
||||
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));
|
||||
}
|
||||
|
||||
// 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)
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
}
|
||||
|
||||
// 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)
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
|
||||
// trigger here, before we reset Published etc
|
||||
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
|
||||
|
||||
// flip the entity's published property
|
||||
// this also flips its published state
|
||||
if (entity.PublishedState == PublishedState.Publishing)
|
||||
if (!isMoving)
|
||||
{
|
||||
entity.Published = true;
|
||||
entity.PublishTemplateId = entity.TemplateId;
|
||||
entity.PublisherId = entity.WriterId;
|
||||
entity.PublishName = entity.Name;
|
||||
entity.PublishDate = entity.UpdateDate;
|
||||
// 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;
|
||||
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
}
|
||||
else if (entity.PublishedState == PublishedState.Unpublishing)
|
||||
{
|
||||
entity.Published = false;
|
||||
entity.PublishTemplateId = null;
|
||||
entity.PublisherId = null;
|
||||
entity.PublishName = null;
|
||||
entity.PublishDate = null;
|
||||
|
||||
ClearEntityTags(entity, _tagRepository);
|
||||
}
|
||||
|
||||
PersistRelations(entity);
|
||||
|
||||
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
|
||||
}
|
||||
else if (entity.PublishedState == PublishedState.Unpublishing)
|
||||
{
|
||||
entity.Published = false;
|
||||
entity.PublishTemplateId = null;
|
||||
entity.PublisherId = null;
|
||||
entity.PublishName = null;
|
||||
entity.PublishDate = null;
|
||||
|
||||
ClearEntityTags(entity, _tagRepository);
|
||||
}
|
||||
|
||||
PersistRelations(entity);
|
||||
|
||||
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
|
||||
@@ -1183,7 +1200,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value))
|
||||
temp.Content.PublishTemplateId = temp.Template2Id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// set properties
|
||||
if (loadProperties)
|
||||
@@ -1216,7 +1233,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
SetVariations(temp.Content, contentVariations, documentVariations);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
foreach (var c in content)
|
||||
@@ -1430,7 +1447,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
yield return dto;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private class ContentVariation
|
||||
|
||||
@@ -219,7 +219,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected override void PersistNewItem(IMedia entity)
|
||||
{
|
||||
var media = (Models.Media) entity;
|
||||
entity.AddingEntity();
|
||||
|
||||
// ensure unique name on the same level
|
||||
@@ -274,15 +273,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
contentVersionDto.NodeId = nodeDto.NodeId;
|
||||
contentVersionDto.Current = true;
|
||||
Database.Insert(contentVersionDto);
|
||||
media.VersionId = contentVersionDto.Id;
|
||||
entity.VersionId = contentVersionDto.Id;
|
||||
|
||||
// persist the media version dto
|
||||
var mediaVersionDto = dto.MediaVersionDto;
|
||||
mediaVersionDto.Id = media.VersionId;
|
||||
mediaVersionDto.Id = entity.VersionId;
|
||||
Database.Insert(mediaVersionDto);
|
||||
|
||||
// persist the property data
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
|
||||
foreach (var propertyDataDto in propertyDataDtos)
|
||||
Database.Insert(propertyDataDto);
|
||||
|
||||
@@ -298,26 +297,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
|
||||
protected override void PersistUpdatedItem(IMedia entity)
|
||||
{
|
||||
var media = (Models.Media) entity;
|
||||
|
||||
// update
|
||||
media.UpdatingEntity();
|
||||
entity.UpdatingEntity();
|
||||
|
||||
// ensure unique name on the same level
|
||||
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
|
||||
// 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();
|
||||
|
||||
// ensure that strings don't contain characters that are invalid in xml
|
||||
// TODO: do we really want to keep doing this here?
|
||||
entity.SanitizeEntityPropertiesForXmlStorage();
|
||||
|
||||
// if parent has changed, get path, level and sort order
|
||||
if (entity.IsPropertyDirty("ParentId"))
|
||||
if (!isMoving)
|
||||
{
|
||||
var parent = GetParentNodeDto(entity.ParentId);
|
||||
// ensure unique name on the same level
|
||||
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
|
||||
|
||||
entity.Path = string.Concat(parent.Path, ",", entity.Id);
|
||||
entity.Level = parent.Level + 1;
|
||||
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
|
||||
// ensure that strings don't contain characters that are invalid in xml
|
||||
// TODO: do we really want to keep doing this here?
|
||||
entity.SanitizeEntityPropertiesForXmlStorage();
|
||||
|
||||
// if parent has changed, get path, level and sort order
|
||||
if (entity.IsPropertyDirty(nameof(entity.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);
|
||||
}
|
||||
}
|
||||
|
||||
// create the dto
|
||||
@@ -328,26 +333,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
nodeDto.ValidatePathWithException();
|
||||
Database.Update(nodeDto);
|
||||
|
||||
// update the content dto
|
||||
Database.Update(dto.ContentDto);
|
||||
if (!isMoving)
|
||||
{
|
||||
// update the content dto
|
||||
Database.Update(dto.ContentDto);
|
||||
|
||||
// update the content & media version dtos
|
||||
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
|
||||
var mediaVersionDto = dto.MediaVersionDto;
|
||||
contentVersionDto.Current = true;
|
||||
Database.Update(contentVersionDto);
|
||||
Database.Update(mediaVersionDto);
|
||||
// update the content & media version dtos
|
||||
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
|
||||
var mediaVersionDto = dto.MediaVersionDto;
|
||||
contentVersionDto.Current = true;
|
||||
Database.Update(contentVersionDto);
|
||||
Database.Update(mediaVersionDto);
|
||||
|
||||
// replace the property data
|
||||
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == media.VersionId);
|
||||
Database.Execute(deletePropertyDataSql);
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
|
||||
foreach (var propertyDataDto in propertyDataDtos)
|
||||
Database.Insert(propertyDataDto);
|
||||
// replace the property data
|
||||
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == entity.VersionId);
|
||||
Database.Execute(deletePropertyDataSql);
|
||||
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
|
||||
foreach (var propertyDataDto in propertyDataDtos)
|
||||
Database.Insert(propertyDataDto);
|
||||
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
SetEntityTags(entity, _tagRepository);
|
||||
|
||||
PersistRelations(entity);
|
||||
PersistRelations(entity);
|
||||
}
|
||||
|
||||
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Umbraco.Core.Runtime
|
||||
// unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
|
||||
_lockId = Guid.NewGuid().ToString();
|
||||
_logger = logger;
|
||||
|
||||
_dbFactory = new UmbracoDatabaseFactory(
|
||||
Constants.System.UmbracoConnectionName,
|
||||
_logger,
|
||||
@@ -40,6 +41,12 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
{
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
// if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider))
|
||||
throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server");
|
||||
|
||||
@@ -126,6 +133,12 @@ namespace Umbraco.Core.Runtime
|
||||
// poll every 1 second
|
||||
Thread.Sleep(1000);
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
// if we aren't configured, we just keep looping since we can't query the db
|
||||
continue;
|
||||
}
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
// If cancellation has been requested we will just exit. Depending on timing of the shutdown,
|
||||
@@ -358,41 +371,44 @@ namespace Umbraco.Core.Runtime
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
var db = GetDatabase();
|
||||
try
|
||||
if (_dbFactory.Configured)
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// When we are disposed, it means we have released the MainDom lock
|
||||
// and called all MainDom release callbacks, in this case
|
||||
// if another maindom is actually coming online we need
|
||||
// to signal to the MainDom coming online that we have shutdown.
|
||||
// To do that, we update the existing main dom DB record with a suffixed "_updated" string.
|
||||
// Otherwise, if we are just shutting down, we want to just delete the row.
|
||||
if (_mainDomChanging)
|
||||
var db = GetDatabase();
|
||||
try
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// When we are disposed, it means we have released the MainDom lock
|
||||
// and called all MainDom release callbacks, in this case
|
||||
// if another maindom is actually coming online we need
|
||||
// to signal to the MainDom coming online that we have shutdown.
|
||||
// To do that, we update the existing main dom DB record with a suffixed "_updated" string.
|
||||
// Otherwise, if we are just shutting down, we want to just delete the row.
|
||||
if (_mainDomChanging)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +526,6 @@ namespace Umbraco.Core.Services
|
||||
OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
namespace Umbraco.Core.Services
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder for sharing logic between the content, media (and member) services
|
||||
/// TODO: Start sharing the logic!
|
||||
/// </summary>
|
||||
public interface IContentServiceBase : IService
|
||||
{ }
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks/fixes the data integrity of node paths/levels stored in the database
|
||||
/// </summary>
|
||||
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,23 +601,27 @@ namespace Umbraco.Core.Services.Implement
|
||||
totalChildren = 0;
|
||||
return Enumerable.Empty<IContent>();
|
||||
}
|
||||
return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren,
|
||||
private IQuery<IContent> GetPagedDescendantQuery(string contentPath)
|
||||
{
|
||||
var query = Query<IContent>();
|
||||
if (!contentPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
|
||||
return query;
|
||||
}
|
||||
|
||||
private IEnumerable<IContent> GetPagedLocked(IQuery<IContent> query, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IContent> filter, Ordering ordering)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
|
||||
|
||||
var query = Query<IContent>();
|
||||
if (!contentPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
|
||||
|
||||
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
@@ -1866,7 +1870,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
public OperationResult MoveToRecycleBin(IContent content, int userId)
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var moves = new List<Tuple<IContent, string>>();
|
||||
var moves = new List<(IContent, string)>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
@@ -1925,7 +1929,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
return;
|
||||
}
|
||||
|
||||
var moves = new List<Tuple<IContent, string>>();
|
||||
var moves = new List<(IContent, string)>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
@@ -1978,7 +1982,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// MUST be called from within WriteLock
|
||||
// trash indicates whether we are trashing, un-trashing, or not changing anything
|
||||
private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId,
|
||||
ICollection<Tuple<IContent, string>> moves,
|
||||
ICollection<(IContent, string)> moves,
|
||||
bool? trash)
|
||||
{
|
||||
content.WriterId = userId;
|
||||
@@ -1990,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
var paths = new Dictionary<int, string>();
|
||||
|
||||
moves.Add(Tuple.Create(content, content.Path)); // capture original path
|
||||
moves.Add((content, content.Path)); // capture original path
|
||||
|
||||
//need to store the original path to lookup descendants based on it below
|
||||
var originalPath = content.Path;
|
||||
@@ -2007,20 +2011,24 @@ namespace Umbraco.Core.Services.Implement
|
||||
paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id;
|
||||
|
||||
const int pageSize = 500;
|
||||
var total = long.MaxValue;
|
||||
while (total > 0)
|
||||
var query = GetPagedDescendantQuery(originalPath);
|
||||
long total;
|
||||
do
|
||||
{
|
||||
var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
|
||||
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
|
||||
moves.Add((descendant, descendant.Path)); // capture original path
|
||||
|
||||
// update path and level since we do not update parentId
|
||||
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
|
||||
descendant.Level += levelDelta;
|
||||
PerformMoveContentLocked(descendant, userId, trash);
|
||||
}
|
||||
}
|
||||
|
||||
} while (total > pageSize);
|
||||
|
||||
}
|
||||
|
||||
@@ -2375,6 +2383,25 @@ namespace Umbraco.Core.Services.Implement
|
||||
return OperationResult.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var report = _documentRepository.CheckDataIntegrity(options);
|
||||
|
||||
if (report.FixedIssues.Count > 0)
|
||||
{
|
||||
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
|
||||
var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty};
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>.EventArgs(new TreeChange<IContent>(root, TreeChangeTypes.RefreshAll)));
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Methods
|
||||
@@ -2812,7 +2839,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
|
||||
|
||||
var changes = new List<TreeChange<IContent>>();
|
||||
var moves = new List<Tuple<IContent, string>>();
|
||||
var moves = new List<(IContent, string)>();
|
||||
var contentTypeIdsA = contentTypeIds.ToArray();
|
||||
|
||||
// using an immediate uow here because we keep making changes with
|
||||
|
||||
@@ -530,23 +530,27 @@ namespace Umbraco.Core.Services.Implement
|
||||
totalChildren = 0;
|
||||
return Enumerable.Empty<IMedia>();
|
||||
}
|
||||
return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IMedia> GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
|
||||
private IQuery<IMedia> GetPagedDescendantQuery(string mediaPath)
|
||||
{
|
||||
var query = Query<IMedia>();
|
||||
if (!mediaPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
|
||||
return query;
|
||||
}
|
||||
|
||||
private IEnumerable<IMedia> GetPagedLocked(IQuery<IMedia> query, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IMedia> filter, Ordering ordering)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
|
||||
|
||||
var query = Query<IMedia>();
|
||||
if (!mediaPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
|
||||
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
@@ -888,7 +892,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
public Attempt<OperationResult> MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var moves = new List<Tuple<IMedia, string>>();
|
||||
var moves = new List<(IMedia, string)>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
@@ -940,7 +944,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
return OperationResult.Attempt.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
var moves = new List<Tuple<IMedia, string>>();
|
||||
var moves = new List<(IMedia, string)>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
@@ -979,7 +983,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
// MUST be called from within WriteLock
|
||||
// trash indicates whether we are trashing, un-trashing, or not changing anything
|
||||
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<Tuple<IMedia, string>> moves, bool? trash)
|
||||
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash)
|
||||
{
|
||||
media.ParentId = parentId;
|
||||
|
||||
@@ -989,7 +993,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
var paths = new Dictionary<int, string>();
|
||||
|
||||
moves.Add(Tuple.Create(media, media.Path)); // capture original path
|
||||
moves.Add((media, media.Path)); // capture original path
|
||||
|
||||
//need to store the original path to lookup descendants based on it below
|
||||
var originalPath = media.Path;
|
||||
@@ -1006,21 +1010,25 @@ namespace Umbraco.Core.Services.Implement
|
||||
paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id;
|
||||
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
var query = GetPagedDescendantQuery(originalPath);
|
||||
long total;
|
||||
do
|
||||
{
|
||||
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
|
||||
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
|
||||
moves.Add((descendant, descendant.Path)); // capture original path
|
||||
|
||||
// update path and level since we do not update parentId
|
||||
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
|
||||
descendant.Level += levelDelta;
|
||||
PerformMoveMediaLocked(descendant, userId, trash);
|
||||
}
|
||||
}
|
||||
|
||||
} while (total > pageSize);
|
||||
|
||||
}
|
||||
|
||||
private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
|
||||
@@ -1139,6 +1147,26 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.MediaTree);
|
||||
|
||||
var report = _mediaRepository.CheckDataIntegrity(options);
|
||||
|
||||
if (report.FixedIssues.Count > 0)
|
||||
{
|
||||
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
|
||||
var root = new Models.Media("root", -1, new MediaType(-1)) { Id = -1, Key = Guid.Empty };
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>.EventArgs(new TreeChange<IMedia>(root, TreeChangeTypes.RefreshAll)));
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1277,7 +1305,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
|
||||
|
||||
var changes = new List<TreeChange<IMedia>>();
|
||||
var moves = new List<Tuple<IMedia, string>>();
|
||||
var moves = new List<(IMedia, string)>();
|
||||
var mediaTypeIdsA = mediaTypeIds.ToArray();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
@@ -1358,5 +1386,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.Security;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
@@ -116,5 +117,17 @@ namespace Umbraco.Core.Services
|
||||
var permissionCollection = userService.GetPermissions(user, nodeId);
|
||||
return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray();
|
||||
}
|
||||
|
||||
internal static IEnumerable<IProfile> GetProfilesById(this IUserService userService, params int[] ids)
|
||||
{
|
||||
var fullUsers = userService.GetUsersById(ids);
|
||||
|
||||
return fullUsers.Select(user =>
|
||||
{
|
||||
var asProfile = user as IProfile;
|
||||
return asProfile ?? new UserProfile(user.Id, user.Name);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +128,13 @@
|
||||
</Compile>
|
||||
-->
|
||||
<Compile Include="AssemblyExtensions.cs" />
|
||||
<Compile Include="Constants-SqlTemplates.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\ContentTypeDto80.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyDataDto80.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyTypeDto80.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReport.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReportEntry.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReportOptions.cs" />
|
||||
<Compile Include="Models\InstallLog.cs" />
|
||||
<Compile Include="Persistence\Repositories\IInstallationRepository.cs" />
|
||||
<Compile Include="Persistence\Repositories\Implement\InstallationRepository.cs" />
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using Examine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
@@ -16,20 +20,46 @@ namespace Umbraco.Examine
|
||||
{
|
||||
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
|
||||
[Obsolete("Use the other ctor instead")]
|
||||
public ContentValueSetBuilder(PropertyEditorCollection propertyEditors,
|
||||
UrlSegmentProviderCollection urlSegmentProviders,
|
||||
IUserService userService,
|
||||
bool publishedValuesOnly)
|
||||
: this(propertyEditors, urlSegmentProviders, userService, Current.ScopeProvider, publishedValuesOnly)
|
||||
{
|
||||
}
|
||||
|
||||
public ContentValueSetBuilder(PropertyEditorCollection propertyEditors,
|
||||
UrlSegmentProviderCollection urlSegmentProviders,
|
||||
IUserService userService,
|
||||
IScopeProvider scopeProvider,
|
||||
bool publishedValuesOnly)
|
||||
: base(propertyEditors, publishedValuesOnly)
|
||||
{
|
||||
_urlSegmentProviders = urlSegmentProviders;
|
||||
_userService = userService;
|
||||
_scopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ValueSet> GetValueSets(params IContent[] content)
|
||||
{
|
||||
Dictionary<int, IProfile> creatorIds;
|
||||
Dictionary<int, IProfile> writerIds;
|
||||
|
||||
// We can lookup all of the creator/writer names at once which can save some
|
||||
// processing below instead of one by one.
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray())
|
||||
.ToDictionary(x => x.Id, x => x);
|
||||
writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray())
|
||||
.ToDictionary(x => x.Id, x => x);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways
|
||||
// but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since
|
||||
// Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]`
|
||||
@@ -58,8 +88,8 @@ namespace Umbraco.Examine
|
||||
{"urlName", urlValue?.Yield() ?? Enumerable.Empty<string>()}, //Always add invariant urlName
|
||||
{"path", c.Path?.Yield() ?? Enumerable.Empty<string>()},
|
||||
{"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty<string>()},
|
||||
{"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() },
|
||||
{"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() },
|
||||
{"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() },
|
||||
{"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() },
|
||||
{"writerID", new object[] {c.WriterId}},
|
||||
{"templateID", new object[] {c.TemplateId ?? 0}},
|
||||
{UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}},
|
||||
|
||||
@@ -107,13 +107,16 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
var repository = CreateRepository(provider);
|
||||
|
||||
// Act
|
||||
var relationType = repository.Get(RelationTypeDto.NodeIdSeed);
|
||||
var relationType = repository.Get(RelationTypeDto.NodeIdSeed + 2);
|
||||
|
||||
// Assert
|
||||
Assert.That(relationType, Is.Not.Null);
|
||||
Assert.That(relationType.HasIdentity, Is.True);
|
||||
Assert.That(relationType.Alias, Is.EqualTo("relateContentOnCopy"));
|
||||
Assert.That(relationType.Name, Is.EqualTo("Relate Content on Copy"));
|
||||
Assert.That(relationType.IsBidirectional, Is.True);
|
||||
Assert.That(relationType.Alias, Is.EqualTo("relateContentToMedia"));
|
||||
Assert.That(relationType.Name, Is.EqualTo("Relate Content to Media"));
|
||||
Assert.That(relationType.ChildObjectType, Is.EqualTo(Constants.ObjectTypes.Media));
|
||||
Assert.That(relationType.ParentObjectType, Is.EqualTo(Constants.ObjectTypes.Document));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +136,7 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
Assert.That(relationTypes, Is.Not.Null);
|
||||
Assert.That(relationTypes.Any(), Is.True);
|
||||
Assert.That(relationTypes.Any(x => x == null), Is.False);
|
||||
Assert.That(relationTypes.Count(), Is.EqualTo(7));
|
||||
Assert.That(relationTypes.Count(), Is.EqualTo(8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
int count = repository.Count(query);
|
||||
|
||||
// Assert
|
||||
Assert.That(count, Is.EqualTo(5));
|
||||
Assert.That(count, Is.EqualTo(6));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +227,9 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
|
||||
public void CreateTestData()
|
||||
{
|
||||
var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"));
|
||||
var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB"));
|
||||
var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document);
|
||||
var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType);
|
||||
var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media);
|
||||
|
||||
var provider = TestObjects.GetScopeProvider(Logger);
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
@@ -234,6 +238,7 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
|
||||
repository.Save(relateContent);//Id 2
|
||||
repository.Save(relateContentType);//Id 3
|
||||
repository.Save(relateContentMedia);//Id 4
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1200,7 +1200,7 @@ namespace Umbraco.Tests.PublishedContent
|
||||
/// 2) Save and publish it
|
||||
/// 3) Publish it with descendants
|
||||
/// 4) Repeat steps 2 and 3
|
||||
///
|
||||
///
|
||||
/// Which has caused an exception. To replicate this test:
|
||||
/// 1) RefreshBranch with kits for a branch where the top most node is unpublished
|
||||
/// 2) RefreshBranch with kits for the branch where the top most node is published
|
||||
@@ -1370,7 +1370,6 @@ namespace Umbraco.Tests.PublishedContent
|
||||
var items = snapshot.Content.GetByXPath("/root/itype");
|
||||
Assert.AreEqual(items.Count(), items.Count());
|
||||
}
|
||||
|
||||
private void AssertLinkedNode(ContentNode node, int parent, int prevSibling, int nextSibling, int firstChild, int lastChild)
|
||||
{
|
||||
Assert.AreEqual(parent, node.ParentContentId);
|
||||
|
||||
@@ -110,8 +110,8 @@ namespace Umbraco.Tests.Services
|
||||
Assert.AreEqual("Test", rt.Name);
|
||||
Assert.AreEqual("repeatedEventOccurence", rt.Alias);
|
||||
Assert.AreEqual(false, rt.IsBidirectional);
|
||||
Assert.AreEqual(Constants.ObjectTypes.Document, rt.ChildObjectType.Value);
|
||||
Assert.AreEqual(Constants.ObjectTypes.Media, rt.ParentObjectType.Value);
|
||||
Assert.AreEqual(Constants.ObjectTypes.Document, rt.ParentObjectType.Value);
|
||||
Assert.AreEqual(Constants.ObjectTypes.Media, rt.ChildObjectType.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -924,6 +924,24 @@ namespace Umbraco.Tests.Services
|
||||
Assert.AreEqual(user.Id, profile.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_By_Profile_Id_Must_return_null_if_user_not_exists()
|
||||
{
|
||||
var profile = ServiceContext.UserService.GetProfileById(42);
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(profile);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetProfilesById_Must_empty_if_users_not_exists()
|
||||
{
|
||||
var profiles = ServiceContext.UserService.GetProfilesById(42);
|
||||
|
||||
// Assert
|
||||
CollectionAssert.IsEmpty(profiles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_User_By_Username()
|
||||
{
|
||||
|
||||
@@ -30,16 +30,22 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
/// </summary>
|
||||
internal static class IndexInitializer
|
||||
{
|
||||
public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly)
|
||||
public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, bool publishedValuesOnly)
|
||||
{
|
||||
var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), GetMockUserService(), publishedValuesOnly);
|
||||
var contentValueSetBuilder = new ContentValueSetBuilder(
|
||||
propertyEditors,
|
||||
new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }),
|
||||
GetMockUserService(),
|
||||
scopeProvider,
|
||||
publishedValuesOnly);
|
||||
|
||||
return contentValueSetBuilder;
|
||||
}
|
||||
|
||||
public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly)
|
||||
public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly)
|
||||
{
|
||||
var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly);
|
||||
var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder);
|
||||
var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly);
|
||||
var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder);
|
||||
return contentIndexDataSource;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
[Test]
|
||||
public void Index_Property_Data_With_Value_Indexer()
|
||||
{
|
||||
var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance<PropertyEditorCollection>(), false);
|
||||
var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance<PropertyEditorCollection>(), ScopeProvider, false);
|
||||
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir,
|
||||
@@ -121,7 +121,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
[Test]
|
||||
public void Rebuild_Index()
|
||||
{
|
||||
var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false);
|
||||
var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider, false);
|
||||
var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockMediaService());
|
||||
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
@@ -149,7 +149,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
[Test]
|
||||
public void Index_Protected_Content_Not_Indexed()
|
||||
{
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false);
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider, false);
|
||||
|
||||
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
@@ -274,7 +274,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
[Test]
|
||||
public void Index_Reindex_Content()
|
||||
{
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false);
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider, false);
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir,
|
||||
validator: new ContentValueSetValidator(false)))
|
||||
@@ -315,7 +315,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed()
|
||||
{
|
||||
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false);
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance<PropertyEditorCollection>(), IndexInitializer.GetMockContentService(), ScopeProvider, false);
|
||||
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir))
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Umbraco.Tests.UmbracoExamine
|
||||
allRecs);
|
||||
|
||||
var propertyEditors = Factory.GetInstance<PropertyEditorCollection>();
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true);
|
||||
var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider, true);
|
||||
|
||||
using (var luceneDir = new RandomIdRamDirectory())
|
||||
using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */
|
||||
|
||||
/*
|
||||
Core app events:
|
||||
Core app events:
|
||||
|
||||
app.ready
|
||||
app.authenticated
|
||||
@@ -12,9 +12,9 @@
|
||||
*/
|
||||
|
||||
function eventsService($q, $rootScope) {
|
||||
|
||||
|
||||
return {
|
||||
|
||||
|
||||
/** raise an event with a given name */
|
||||
emit: function (name, args) {
|
||||
|
||||
|
||||
@@ -185,7 +185,19 @@ angular.module('umbraco.services')
|
||||
authenticate: function (login, password) {
|
||||
|
||||
return authResource.performLogin(login, password)
|
||||
.then(this.setAuthenticationSuccessful);
|
||||
.then(function(data) {
|
||||
|
||||
// Check if user has a start node set.
|
||||
if(data.startContentIds.length === 0 && data.startMediaIds.length === 0){
|
||||
var errorMsg = "User has no start-nodes";
|
||||
var result = { errorMsg: errorMsg, user: data, authenticated: false, lastUserId: lastUserId, loginType: "credentials" };
|
||||
eventsService.emit("app.notAuthenticated", result);
|
||||
throw result;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}).then(this.setAuthenticationSuccessful);
|
||||
},
|
||||
setAuthenticationSuccessful: function (data) {
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16);
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.16);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ function MainController($scope, $location, appState, treeService, notificationsS
|
||||
$scope.authenticated = null;
|
||||
$scope.user = null;
|
||||
const isTimedOut = data && data.isTimedOut ? true : false;
|
||||
|
||||
$scope.showLoginScreen(isTimedOut);
|
||||
|
||||
// Remove the localstorage items for tours shown
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>8700</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8700</IISUrl>
|
||||
<IISUrl>http://localhost:8700</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
@@ -429,4 +429,4 @@
|
||||
<Message Text="ConfigFile: $(OriginalFileName) -> $(OutputFileName)" Importance="high" Condition="Exists('$(ModifiedFileName)')" />
|
||||
<Copy SourceFiles="$(ModifiedFileName)" DestinationFiles="$(OutputFileName)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="Exists('$(ModifiedFileName)')" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck.Checks.Data
|
||||
{
|
||||
[HealthCheck(
|
||||
"73DD0C1C-E0CA-4C31-9564-1DCA509788AF",
|
||||
"Database data integrity check",
|
||||
Description = "Checks for various data integrity issues in the Umbraco database.",
|
||||
Group = "Data Integrity")]
|
||||
public class DatabaseIntegrityCheck : HealthCheck
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private const string _fixMediaPaths = "fixMediaPaths";
|
||||
private const string _fixContentPaths = "fixContentPaths";
|
||||
private const string _fixMediaPathsTitle = "Fix media paths";
|
||||
private const string _fixContentPathsTitle = "Fix content paths";
|
||||
|
||||
public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the status for this health check
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override IEnumerable<HealthCheckStatus> GetStatus()
|
||||
{
|
||||
//return the statuses
|
||||
return new[]
|
||||
{
|
||||
CheckDocuments(false),
|
||||
CheckMedia(false)
|
||||
};
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckMedia(bool fix)
|
||||
{
|
||||
return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix,
|
||||
() => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix}));
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckDocuments(bool fix)
|
||||
{
|
||||
return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix,
|
||||
() => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix}));
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func<ContentDataIntegrityReport> doCheck)
|
||||
{
|
||||
var report = doCheck();
|
||||
|
||||
var actions = new List<HealthCheckAction>();
|
||||
if (!report.Ok)
|
||||
{
|
||||
actions.Add(new HealthCheckAction(actionAlias, Id)
|
||||
{
|
||||
Name = actionName
|
||||
});
|
||||
}
|
||||
|
||||
return new HealthCheckStatus(GetReport(report, entityType, detailedReport))
|
||||
{
|
||||
ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error,
|
||||
Actions = actions
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (report.Ok)
|
||||
{
|
||||
sb.AppendLine($"<p>All {entityType} paths are valid</p>");
|
||||
|
||||
if (!detailed)
|
||||
return sb.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"<p>{report.DetectedIssues.Count} invalid {entityType} paths detected.</p>");
|
||||
}
|
||||
|
||||
if (detailed && report.DetectedIssues.Count > 0)
|
||||
{
|
||||
sb.AppendLine("<ul>");
|
||||
foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType))
|
||||
{
|
||||
var countByGroup = issueGroup.Count();
|
||||
var fixedByGroup = issueGroup.Count(x => x.Value.Fixed);
|
||||
sb.AppendLine("<li>");
|
||||
sb.AppendLine($"{countByGroup} issues of type <code>{issueGroup.Key}</code> ... {fixedByGroup} fixed");
|
||||
sb.AppendLine("</li>");
|
||||
}
|
||||
sb.AppendLine("</ul>");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
|
||||
{
|
||||
switch (action.Alias)
|
||||
{
|
||||
case _fixContentPaths:
|
||||
return CheckDocuments(true);
|
||||
case _fixMediaPaths:
|
||||
return CheckMedia(true);
|
||||
default:
|
||||
throw new InvalidOperationException("Action not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,6 +502,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate the <see cref="ContentNodeKit"/> and try to create a parent <see cref="LinkedNode{ContentNode}"/>
|
||||
/// </summary>
|
||||
/// <param name="kit"></param>
|
||||
/// <param name="parent"></param>
|
||||
/// <returns>
|
||||
/// Returns false if the parent was not found or if the kit validation failed
|
||||
/// </returns>
|
||||
private bool BuildKit(ContentNodeKit kit, out LinkedNode<ContentNode> parent)
|
||||
{
|
||||
// make sure parent exists
|
||||
@@ -512,6 +520,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return false;
|
||||
}
|
||||
|
||||
// We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path
|
||||
// data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null
|
||||
// because the data sort operation is by path.
|
||||
if (parent.Value == null)
|
||||
{
|
||||
_logger.Warn<ContentStore>($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure the kit is valid
|
||||
if (kit.DraftData == null && kit.PublishedData == null)
|
||||
{
|
||||
@@ -800,7 +817,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
//this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen
|
||||
ClearBranchLocked(existing);
|
||||
//TODO: This removes the current GEN from the tree - do we really want to do that?
|
||||
//TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....)
|
||||
RemoveTreeNodeLocked(existing);
|
||||
}
|
||||
|
||||
@@ -865,6 +882,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void ClearBranchLocked(ContentNode content)
|
||||
{
|
||||
// This should never be null, all code that calls this method is null checking but we've seen
|
||||
// issues of null ref exceptions in issue reports so we'll double check here
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
|
||||
SetValueLocked(_contentNodes, content.Id, null);
|
||||
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
|
||||
|
||||
@@ -1032,6 +1053,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
var parent = parentLink.Value;
|
||||
|
||||
// We are doing a null check here but this should no longer be possible because we have a null check in BuildKit
|
||||
// for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place.
|
||||
// see https://github.com/umbraco/Umbraco-CMS/issues/7868
|
||||
if (parent == null)
|
||||
throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted.");
|
||||
|
||||
// if parent has no children, clone parent + add as first child
|
||||
if (parent.FirstChildContentId < 0)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
// provides efficient database access for NuCache
|
||||
internal class DatabaseDataSource : IDataSource
|
||||
{
|
||||
private const int PageSize = 500;
|
||||
|
||||
// we want arrays, we want them all loaded, not an enumerable
|
||||
|
||||
private Sql<ISqlContext> ContentSourcesSelect(IScope scope, Func<Sql<ISqlContext>, Sql<ISqlContext>> joins = null)
|
||||
@@ -79,33 +81,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateContentNodeKit);
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateContentNodeKit(row);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetBranchContentSources(IScope scope, int id)
|
||||
{
|
||||
var syntax = scope.SqlContext.SqlSyntax;
|
||||
var sql = ContentSourcesSelect(scope, s => s
|
||||
var sql = ContentSourcesSelect(scope,
|
||||
s => s.InnerJoin<NodeDto>("x").On<NodeDto, NodeDto>((left, right) => left.NodeId == right.NodeId || SqlText<bool>(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x"))
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed)
|
||||
.Where<NodeDto>(x => x.NodeId == id, "x")
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
.InnerJoin<NodeDto>("x").On<NodeDto, NodeDto>((left, right) => left.NodeId == right.NodeId || SqlText<bool>(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x"))
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed)
|
||||
.Where<NodeDto>(x => x.NodeId == id, "x")
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateContentNodeKit);
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateContentNodeKit(row);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetTypeContentSources(IScope scope, IEnumerable<int> ids)
|
||||
{
|
||||
if (!ids.Any()) return Enumerable.Empty<ContentNodeKit>();
|
||||
if (!ids.Any()) yield break;
|
||||
|
||||
var sql = ContentSourcesSelect(scope)
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed)
|
||||
.WhereIn<ContentDto>(x => x.ContentTypeId, ids)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateContentNodeKit);
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateContentNodeKit(row);
|
||||
}
|
||||
|
||||
private Sql<ISqlContext> MediaSourcesSelect(IScope scope, Func<Sql<ISqlContext>, Sql<ISqlContext>> joins = null)
|
||||
@@ -116,11 +128,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"),
|
||||
x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId"))
|
||||
.AndSelect<ContentDto>(x => Alias(x.ContentTypeId, "ContentTypeId"))
|
||||
|
||||
.AndSelect<ContentVersionDto>(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId"))
|
||||
|
||||
.AndSelect<ContentNuDto>("nuEdit", x => Alias(x.Data, "EditData"))
|
||||
|
||||
.From<NodeDto>();
|
||||
|
||||
if (joins != null)
|
||||
@@ -128,9 +137,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
|
||||
sql = sql
|
||||
.InnerJoin<ContentDto>().On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
|
||||
|
||||
.InnerJoin<ContentVersionDto>().On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId && right.Current)
|
||||
|
||||
.LeftJoin<ContentNuDto>("nuEdit").On<NodeDto, ContentNuDto>((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit");
|
||||
|
||||
return sql;
|
||||
@@ -152,33 +159,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateMediaNodeKit);
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateMediaNodeKit(row);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetBranchMediaSources(IScope scope, int id)
|
||||
{
|
||||
var syntax = scope.SqlContext.SqlSyntax;
|
||||
var sql = MediaSourcesSelect(scope, s => s
|
||||
|
||||
.InnerJoin<NodeDto>("x").On<NodeDto, NodeDto>((left, right) => left.NodeId == right.NodeId || SqlText<bool>(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x"))
|
||||
|
||||
var sql = MediaSourcesSelect(scope,
|
||||
s => s.InnerJoin<NodeDto>("x").On<NodeDto, NodeDto>((left, right) => left.NodeId == right.NodeId || SqlText<bool>(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x"))
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed)
|
||||
.Where<NodeDto>(x => x.NodeId == id, "x")
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateMediaNodeKit);
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateMediaNodeKit(row);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetTypeMediaSources(IScope scope, IEnumerable<int> ids)
|
||||
{
|
||||
if (!ids.Any()) return Enumerable.Empty<ContentNodeKit>();
|
||||
if (!ids.Any()) yield break;
|
||||
|
||||
var sql = MediaSourcesSelect(scope)
|
||||
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed)
|
||||
.WhereIn<ContentDto>(x => x.ContentTypeId, ids)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
return scope.Database.Query<ContentSourceDto>(sql).Select(CreateMediaNodeKit);
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
|
||||
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql))
|
||||
yield return CreateMediaNodeKit(row);
|
||||
}
|
||||
|
||||
private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CSharpTest.Net.Collections;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core;
|
||||
@@ -866,12 +867,37 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
//into a new DLL for the application which includes both content types and media types.
|
||||
//Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated
|
||||
//to use the newest version of the class.
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var draftChanged, out var publishedChanged);
|
||||
NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var anythingChanged);
|
||||
}
|
||||
|
||||
// NOTE: Ideally this can be run on background threads here which would prevent blocking the UI
|
||||
// as is the case when saving a content type. Intially one would think that it won't be any different
|
||||
// between running this here or in another background thread immediately after with regards to how the
|
||||
// UI will respond because we already know between calling `WithSafeLiveFactoryReset` to reset the PureLive models
|
||||
// and this code here, that many front-end requests could be attempted to be processed. If that is the case, those pages are going to get a
|
||||
// model binding error and our ModelBindingExceptionFilter is going to to its magic to reload those pages so the end user is none the wiser.
|
||||
// So whether or not this executes 'here' or on a background thread immediately wouldn't seem to make any difference except that we can return
|
||||
// execution to the UI sooner.
|
||||
// BUT!... there is a difference IIRC. There is still execution logic that continues after this call on this thread with the cache refreshers
|
||||
// and those cache refreshers need to have the up-to-date data since other user cache refreshers will be expecting the data to be 'live'. If
|
||||
// we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which
|
||||
// they require.
|
||||
|
||||
// These can be run side by side in parallel.
|
||||
|
||||
Parallel.Invoke(
|
||||
() =>
|
||||
{
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode<TValue> next = null)
|
||||
{
|
||||
Value = value;
|
||||
Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked
|
||||
Gen = gen;
|
||||
Next = next;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Examine;
|
||||
@@ -36,12 +37,14 @@ namespace Umbraco.Web.Search
|
||||
factory.GetInstance<PropertyEditorCollection>(),
|
||||
factory.GetInstance<UrlSegmentProviderCollection>(),
|
||||
factory.GetInstance<IUserService>(),
|
||||
factory.GetInstance<IScopeProvider>(),
|
||||
true));
|
||||
composition.RegisterUnique<IContentValueSetBuilder>(factory =>
|
||||
new ContentValueSetBuilder(
|
||||
factory.GetInstance<PropertyEditorCollection>(),
|
||||
factory.GetInstance<UrlSegmentProviderCollection>(),
|
||||
factory.GetInstance<IUserService>(),
|
||||
factory.GetInstance<IScopeProvider>(),
|
||||
false));
|
||||
composition.RegisterUnique<IValueSetBuilder<IMedia>, MediaValueSetBuilder>();
|
||||
composition.RegisterUnique<IValueSetBuilder<IMember>, MemberValueSetBuilder>();
|
||||
|
||||
@@ -121,7 +121,9 @@ namespace Umbraco.Web.Security
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null);
|
||||
return SignInStatus.Failure;
|
||||
|
||||
// We will say its a sucessful login which it is, but they have no node access
|
||||
return SignInStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
<Compile Include="Editors\MacrosController.cs" />
|
||||
<Compile Include="Editors\RelationTypeController.cs" />
|
||||
<Compile Include="Editors\TinyMceController.cs" />
|
||||
<Compile Include="HealthCheck\Checks\Data\DatabaseIntegrityCheck.cs" />
|
||||
<Compile Include="ImageCropperTemplateCoreExtensions.cs" />
|
||||
<Compile Include="IUmbracoContextFactory.cs" />
|
||||
<Compile Include="Install\ChangesMonitor.cs" />
|
||||
|
||||
Reference in New Issue
Block a user