Merge remote-tracking branch 'origin/temp8' into temp8-224-db-updates-sched-publishing-with-variants
# Conflicts: # src/Umbraco.Core/Models/Content.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs
This commit is contained in:
@@ -406,28 +406,40 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IContent"/> objects by the Id of the <see cref="IContentType"/>
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the <see cref="IContentType"/></param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetByType(int id)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords
|
||||
, IQuery<IContent> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if(pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("sortOrder");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var query = Query<IContent>().Where(x => x.ContentTypeId == id);
|
||||
return _documentRepository.Get(query);
|
||||
return _documentRepository.GetPage(
|
||||
Query<IContent>().Where(x => x.ContentTypeId == contentTypeId),
|
||||
pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable<IContent> GetPublishedContentOfContentType(int id)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery<IContent> filter, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("sortOrder");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var query = Query<IContent>().Where(x => x.ContentTypeId == id);
|
||||
return _documentRepository.Get(query);
|
||||
return _documentRepository.GetPage(
|
||||
Query<IContent>().Where(x => contentTypeIds.Contains(x.ContentTypeId)),
|
||||
pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,21 +549,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetChildren(int id)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var query = Query<IContent>().Where(x => x.ParentId == id);
|
||||
return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of published <see cref="IContent"/> objects by Parent Id
|
||||
/// </summary>
|
||||
@@ -569,18 +566,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
|
||||
string filter = null, Ordering ordering = null)
|
||||
{
|
||||
var filterQuery = filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: Query<IContent>().Where(x => x.Name.Contains(filter));
|
||||
|
||||
return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, filterQuery, ordering);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IContent> filter, Ordering ordering = null)
|
||||
IQuery<IContent> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
@@ -598,27 +584,16 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "")
|
||||
public IEnumerable<IContent> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IContent> filter = null, Ordering ordering = null)
|
||||
{
|
||||
var filterQuery = filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: Query<IContent>().Where(x => x.Name.Contains(filter));
|
||||
|
||||
return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery<IContent> filter)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("Path");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
|
||||
var query = Query<IContent>();
|
||||
|
||||
//if the id is System Root, then just get all
|
||||
if (id != Constants.System.Root)
|
||||
{
|
||||
@@ -628,65 +603,24 @@ namespace Umbraco.Core.Services.Implement
|
||||
totalChildren = 0;
|
||||
return Enumerable.Empty<IContent>();
|
||||
}
|
||||
query.Where(x => x.Path.SqlStartsWith($"{contentPath[0].Path},", TextColumnType.NVarchar));
|
||||
return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
|
||||
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IContent"/> objects by its name or partial name
|
||||
/// </summary>
|
||||
/// <param name="parentId">Id of the Parent to retrieve Children from</param>
|
||||
/// <param name="name">Full or partial name of the children</param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetChildren(int parentId, string name)
|
||||
private IEnumerable<IContent> GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IContent> filter, Ordering ordering)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var query = Query<IContent>().Where(x => x.ParentId == parentId && x.Name.Contains(name));
|
||||
return _documentRepository.Get(query);
|
||||
}
|
||||
}
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetDescendants(int id)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var content = GetById(id);
|
||||
if (content == null)
|
||||
{
|
||||
scope.Complete(); // else causes rollback
|
||||
return Enumerable.Empty<IContent>();
|
||||
}
|
||||
var pathMatch = content.Path + ",";
|
||||
var query = Query<IContent>().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch));
|
||||
return _documentRepository.Get(query);
|
||||
}
|
||||
}
|
||||
var query = Query<IContent>();
|
||||
if (!contentPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="content"><see cref="IContent"/> item to retrieve Descendants from</param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetDescendants(IContent content)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var pathMatch = content.Path + ",";
|
||||
var query = Query<IContent>().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch));
|
||||
return _documentRepository.Get(query);
|
||||
}
|
||||
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -765,13 +699,17 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// Gets a collection of an <see cref="IContent"/> objects, which resides in the Recycle Bin
|
||||
/// </summary>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IContent> GetContentInRecycleBin()
|
||||
public IEnumerable<IContent> GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
|
||||
IQuery<IContent> filter = null, Ordering ordering = null)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("Path");
|
||||
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
var query = Query<IContent>().Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix));
|
||||
return _documentRepository.Get(query);
|
||||
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,7 +802,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
var langs = string.Join(", ", _languageRepository.GetMany()
|
||||
.Where(x => culturesChanging.InvariantContains(x.IsoCode))
|
||||
.Select(x => x.CultureName));
|
||||
Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languagues: {langs}", langs);
|
||||
Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
|
||||
}
|
||||
else
|
||||
Audit(AuditType.Save, userId, content.Id);
|
||||
@@ -1401,27 +1339,35 @@ namespace Umbraco.Core.Services.Implement
|
||||
// if one fails, abort its branch
|
||||
var exclude = new HashSet<int>();
|
||||
|
||||
//fixme: should be paged to not overwhelm the database (timeouts)
|
||||
foreach (var d in GetDescendants(document))
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
// if parent is excluded, exclude document and ignore
|
||||
// if not forcing, and not publishing, exclude document and ignore
|
||||
if (exclude.Contains(d.ParentId) || !force && !d.Published)
|
||||
var descendants = GetPagedDescendants(document.Id, page++, pageSize, out total);
|
||||
|
||||
foreach (var d in descendants)
|
||||
{
|
||||
// if parent is excluded, exclude document and ignore
|
||||
// if not forcing, and not publishing, exclude document and ignore
|
||||
if (exclude.Contains(d.ParentId) || !force && !d.Published)
|
||||
{
|
||||
exclude.Add(d.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// no need to check path here,
|
||||
// 1. because we know the parent is path-published (we just published it)
|
||||
// 2. because it would not work as nothing's been written out to the db until the uow completes
|
||||
result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId);
|
||||
results.Add(result);
|
||||
if (result.Success) continue;
|
||||
|
||||
// abort branch
|
||||
exclude.Add(d.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// no need to check path here,
|
||||
// 1. because we know the parent is path-published (we just published it)
|
||||
// 2. because it would not work as nothing's been written out to the db until the uow completes
|
||||
result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId);
|
||||
results.Add(result);
|
||||
if (result.Success) continue;
|
||||
|
||||
// abort branch
|
||||
exclude.Add(d.Id);
|
||||
}
|
||||
|
||||
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(document, TreeChangeTypes.RefreshBranch).ToEventArgs());
|
||||
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(publishedDocuments, false, false), "Published");
|
||||
@@ -1512,25 +1458,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
private void DeleteLocked(IScope scope, IContent content)
|
||||
{
|
||||
// then recursively delete descendants, bottom-up
|
||||
// just repository.Delete + an event
|
||||
var stack = new Stack<IContent>();
|
||||
stack.Push(content);
|
||||
var level = 1;
|
||||
while (stack.Count > 0)
|
||||
void DoDelete(IContent c)
|
||||
{
|
||||
var c = stack.Peek();
|
||||
IContent[] cc;
|
||||
if (c.Level == level)
|
||||
while ((cc = c.Children(this).ToArray()).Length > 0)
|
||||
{
|
||||
foreach (var ci in cc)
|
||||
stack.Push(ci);
|
||||
c = cc[cc.Length - 1];
|
||||
}
|
||||
c = stack.Pop();
|
||||
level = c.Level;
|
||||
|
||||
_documentRepository.Delete(c);
|
||||
var args = new DeleteEventArgs<IContent>(c, false); // raise event & get flagged files
|
||||
scope.Events.Dispatch(Deleted, this, args, nameof(Deleted));
|
||||
@@ -1539,6 +1468,18 @@ namespace Umbraco.Core.Services.Implement
|
||||
_mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files
|
||||
(file, e) => Logger.Error<ContentService>(e, "An error occurred while deleting file attached to nodes: {File}", file));
|
||||
}
|
||||
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
//get descendants - ordered from deepest to shallowest
|
||||
var descendants = GetPagedDescendants(content.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
|
||||
foreach (var c in descendants)
|
||||
DoDelete(c);
|
||||
}
|
||||
DoDelete(content);
|
||||
}
|
||||
|
||||
//TODO:
|
||||
@@ -1748,8 +1689,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
moves.Add(Tuple.Create(content, content.Path)); // capture original path
|
||||
|
||||
// get before moving, in case uow is immediate
|
||||
var descendants = GetDescendants(content);
|
||||
//need to store the original path to lookup descendants based on it below
|
||||
var originalPath = content.Path;
|
||||
|
||||
// these will be updated by the repo because we changed parentId
|
||||
//content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
|
||||
@@ -1762,22 +1703,28 @@ namespace Umbraco.Core.Services.Implement
|
||||
//paths[content.Id] = content.Path;
|
||||
paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : "-1") : parent.Path) + "," + content.Id;
|
||||
|
||||
foreach (var descendant in descendants)
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while(page * pageSize < total)
|
||||
{
|
||||
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
|
||||
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
|
||||
|
||||
// update path and level since we do not update parentId
|
||||
if (paths.ContainsKey(descendant.ParentId) == false)
|
||||
Console.WriteLine("oops on " + descendant.ParentId + " for " + content.Path + " " + parent?.Path);
|
||||
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
|
||||
Console.WriteLine("path " + descendant.Id + " = " + paths[descendant.Id]);
|
||||
descendant.Level += levelDelta;
|
||||
PerformMoveContentLocked(descendant, userId, trash);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
|
||||
{
|
||||
//fixme no casting
|
||||
if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value;
|
||||
content.WriterId = userId;
|
||||
_documentRepository.Save(content);
|
||||
@@ -1906,29 +1853,36 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
if (recursive) // process descendants
|
||||
{
|
||||
foreach (var descendant in GetDescendants(content))
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while(page * pageSize < total)
|
||||
{
|
||||
// if parent has not been copied, skip, else gets its copy id
|
||||
if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue;
|
||||
var descendants = GetPagedDescendants(content.Id, page++, pageSize, out total);
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
// if parent has not been copied, skip, else gets its copy id
|
||||
if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue;
|
||||
|
||||
var descendantCopy = descendant.DeepCloneWithResetIdentities();
|
||||
descendantCopy.ParentId = parentId;
|
||||
var descendantCopy = descendant.DeepCloneWithResetIdentities();
|
||||
descendantCopy.ParentId = parentId;
|
||||
|
||||
if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs<IContent>(descendant, descendantCopy, parentId)))
|
||||
continue;
|
||||
if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs<IContent>(descendant, descendantCopy, parentId)))
|
||||
continue;
|
||||
|
||||
// a copy is not published (but not really unpublishing either)
|
||||
// update the create author and last edit author
|
||||
if (descendantCopy.Published)
|
||||
((Content) descendantCopy).Published = false;
|
||||
descendantCopy.CreatorId = userId;
|
||||
descendantCopy.WriterId = userId;
|
||||
// a copy is not published (but not really unpublishing either)
|
||||
// update the create author and last edit author
|
||||
if (descendantCopy.Published)
|
||||
((Content)descendantCopy).Published = false;
|
||||
descendantCopy.CreatorId = userId;
|
||||
descendantCopy.WriterId = userId;
|
||||
|
||||
// save and flush (see above)
|
||||
_documentRepository.Save(descendantCopy);
|
||||
// save and flush (see above)
|
||||
_documentRepository.Save(descendantCopy);
|
||||
|
||||
copies.Add(Tuple.Create(descendant, descendantCopy));
|
||||
idmap[descendant.Id] = descendantCopy.Id;
|
||||
copies.Add(Tuple.Create(descendant, descendantCopy));
|
||||
idmap[descendant.Id] = descendantCopy.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2006,17 +1960,19 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="items"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="raiseEvents"></param>
|
||||
/// <returns>True if sorting succeeded, otherwise False</returns>
|
||||
public bool Sort(IEnumerable<IContent> items, int userId = 0, bool raiseEvents = true)
|
||||
/// <returns>Result indicating what action was taken when handling the command.</returns>
|
||||
public OperationResult Sort(IEnumerable<IContent> items, int userId = 0, bool raiseEvents = true)
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
var itemsA = items.ToArray();
|
||||
if (itemsA.Length == 0) return true;
|
||||
if (itemsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs);
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var ret = Sort(scope, itemsA, userId, raiseEvents);
|
||||
var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents);
|
||||
scope.Complete();
|
||||
return ret;
|
||||
}
|
||||
@@ -2033,28 +1989,38 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="ids"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="raiseEvents"></param>
|
||||
/// <returns>True if sorting succeeded, otherwise False</returns>
|
||||
public bool Sort(IEnumerable<int> ids, int userId = 0, bool raiseEvents = true)
|
||||
/// <returns>Result indicating what action was taken when handling the command.</returns>
|
||||
public OperationResult Sort(IEnumerable<int> ids, int userId = 0, bool raiseEvents = true)
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
var idsA = ids.ToArray();
|
||||
if (idsA.Length == 0) return true;
|
||||
if (idsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs);
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
var itemsA = GetByIds(idsA).ToArray();
|
||||
|
||||
var ret = Sort(scope, itemsA, userId, raiseEvents);
|
||||
var ret = Sort(scope, itemsA, userId, evtMsgs, raiseEvents);
|
||||
scope.Complete();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Sort(IScope scope, IContent[] itemsA, int userId, bool raiseEvents)
|
||||
private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages evtMsgs, bool raiseEvents)
|
||||
{
|
||||
var saveEventArgs = new SaveEventArgs<IContent>(itemsA);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
|
||||
return false;
|
||||
if (raiseEvents)
|
||||
{
|
||||
//raise cancelable sorting event
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Sorting)))
|
||||
return OperationResult.Cancel(evtMsgs);
|
||||
|
||||
//raise saving event (this one cannot be canceled)
|
||||
saveEventArgs.CanCancel = false;
|
||||
scope.Events.Dispatch(Saving, this, saveEventArgs, nameof(Saving));
|
||||
}
|
||||
|
||||
var published = new List<IContent>();
|
||||
var saved = new List<IContent>();
|
||||
@@ -2086,8 +2052,9 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
if (raiseEvents)
|
||||
{
|
||||
saveEventArgs.CanCancel = false;
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved");
|
||||
//first saved, then sorted
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs, nameof(Saved));
|
||||
scope.Events.Dispatch(Sorted, this, saveEventArgs, nameof(Sorted));
|
||||
}
|
||||
|
||||
scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange<IContent>(x, TreeChangeTypes.RefreshNode)).ToEventArgs());
|
||||
@@ -2096,7 +2063,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
scope.Events.Dispatch(Published, this, new PublishEventArgs<IContent>(published, false, false), "Published");
|
||||
|
||||
Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
|
||||
return true;
|
||||
return OperationResult.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -2171,6 +2138,16 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IContentService, DeleteRevisionsEventArgs> DeletedVersions;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Sorting
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Sorting;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs after Sorting
|
||||
/// </summary>
|
||||
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Sorted;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before Save
|
||||
/// </summary>
|
||||
|
||||
@@ -364,18 +364,39 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by the Id of the <see cref="IMediaType"/>
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the <see cref="IMediaType"/></param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetMediaOfMediaType(int id)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMedia> GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords, IQuery<IMedia> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("sortOrder");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
var query = Query<IMedia>().Where(x => x.ContentTypeId == id);
|
||||
return _mediaRepository.Get(query);
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
return _mediaRepository.GetPage(
|
||||
Query<IMedia>().Where(x => x.ContentTypeId == contentTypeId),
|
||||
pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMedia> GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery<IMedia> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("sortOrder");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
return _mediaRepository.GetPage(
|
||||
Query<IMedia>().Where(x => contentTypeIds.Contains(x.ContentTypeId)),
|
||||
pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,149 +481,36 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetChildren(int id)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
var query = Query<IMedia>().Where(x => x.ParentId == id);
|
||||
return _mediaRepository.Get(query).OrderBy(x => x.SortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
||||
/// <param name="pageIndex">Page index (zero based)</param>
|
||||
/// <param name="pageSize">Page size</param>
|
||||
/// <param name="totalChildren">Total records query would return without paging</param>
|
||||
/// <param name="orderBy">Field to order by</param>
|
||||
/// <param name="orderDirection">Direction to order by</param>
|
||||
/// <param name="filter">Search text filter</param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "")
|
||||
{
|
||||
var filterQuery = filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: Query<IMedia>().Where(x => x.Name.Contains(filter));
|
||||
|
||||
return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
||||
/// <param name="pageIndex">Page index (zero based)</param>
|
||||
/// <param name="pageSize">Page size</param>
|
||||
/// <param name="totalChildren">Total records query would return without paging</param>
|
||||
/// <param name="orderBy">Field to order by</param>
|
||||
/// <param name="orderDirection">Direction to order by</param>
|
||||
/// <param name="orderBySystemField">Flag to indicate when ordering by system field</param>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery<IMedia> filter)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMedia> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IMedia> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("sortOrder");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
|
||||
var query = Query<IMedia>().Where(x => x.ParentId == id);
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
||||
/// <param name="pageIndex">Page number</param>
|
||||
/// <param name="pageSize">Page size</param>
|
||||
/// <param name="totalChildren">Total records query would return without paging</param>
|
||||
/// <param name="orderBy">Field to order by</param>
|
||||
/// <param name="orderDirection">Direction to order by</param>
|
||||
/// <param name="orderBySystemField">Flag to indicate when ordering by system field</param>
|
||||
/// <param name="filter">Search text filter</param>
|
||||
/// <param name="contentTypeFilter">A list of content type Ids to filter the list by</param>
|
||||
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter, int[] contentTypeFilter)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMedia> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IMedia> filter = null, Ordering ordering = null)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("Path");
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
|
||||
var query = Query<IMedia>();
|
||||
// always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method)
|
||||
|
||||
query.Where(x => x.ParentId == id);
|
||||
|
||||
if (contentTypeFilter != null && contentTypeFilter.Length > 0)
|
||||
{
|
||||
query.Where(x => contentTypeFilter.Contains(x.ContentTypeId));
|
||||
}
|
||||
|
||||
var filterQuery = filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: Query<IMedia>().Where(x => x.Name.Contains(filter));
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filterQuery, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
||||
/// <param name="pageIndex">Page number</param>
|
||||
/// <param name="pageSize">Page size</param>
|
||||
/// <param name="totalChildren">Total records query would return without paging</param>
|
||||
/// <param name="orderBy">Field to order by</param>
|
||||
/// <param name="orderDirection">Direction to order by</param>
|
||||
/// <param name="filter">Search text filter</param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "")
|
||||
{
|
||||
var filterQuery = filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: Query<IMedia>().Where(x => x.Name.Contains(filter));
|
||||
|
||||
return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IMedia"/> objects by Parent Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
||||
/// <param name="pageIndex">Page number</param>
|
||||
/// <param name="pageSize">Page size</param>
|
||||
/// <param name="totalChildren">Total records query would return without paging</param>
|
||||
/// <param name="orderBy">Field to order by</param>
|
||||
/// <param name="orderDirection">Direction to order by</param>
|
||||
/// <param name="orderBySystemField">Flag to indicate when ordering by system field</param>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery<IMedia> filter)
|
||||
{
|
||||
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
|
||||
var query = Query<IMedia>();
|
||||
|
||||
//if the id is System Root, then just get all
|
||||
if (id != Constants.System.Root)
|
||||
{
|
||||
@@ -612,47 +520,24 @@ namespace Umbraco.Core.Services.Implement
|
||||
totalChildren = 0;
|
||||
return Enumerable.Empty<IMedia>();
|
||||
}
|
||||
query.Where(x => x.Path.SqlStartsWith(mediaPath[0].Path + ",", TextColumnType.NVarchar));
|
||||
return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField));
|
||||
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets descendants of a <see cref="IMedia"/> object by its Id
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the Parent to retrieve descendants from</param>
|
||||
/// <returns>An Enumerable flat list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetDescendants(int id)
|
||||
private IEnumerable<IMedia> GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
|
||||
IQuery<IMedia> filter, Ordering ordering)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
var media = GetById(id);
|
||||
if (media == null)
|
||||
return Enumerable.Empty<IMedia>();
|
||||
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 pathMatch = media.Path + ",";
|
||||
var query = Query<IMedia>().Where(x => x.Id != media.Id && x.Path.StartsWith(pathMatch));
|
||||
return _mediaRepository.Get(query);
|
||||
}
|
||||
}
|
||||
var query = Query<IMedia>();
|
||||
if (!mediaPath.IsNullOrWhiteSpace())
|
||||
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
|
||||
|
||||
/// <summary>
|
||||
/// Gets descendants of a <see cref="IMedia"/> object by its Id
|
||||
/// </summary>
|
||||
/// <param name="media">The Parent <see cref="IMedia"/> object to retrieve descendants from</param>
|
||||
/// <returns>An Enumerable flat list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetDescendants(IMedia media)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
var pathMatch = media.Path + ",";
|
||||
var query = Query<IMedia>().Where(x => x.Id != media.Id && x.Path.StartsWith(pathMatch));
|
||||
return _mediaRepository.Get(query);
|
||||
}
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -694,17 +579,18 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of an <see cref="IMedia"/> objects, which resides in the Recycle Bin
|
||||
/// </summary>
|
||||
/// <returns>An Enumerable list of <see cref="IMedia"/> objects</returns>
|
||||
public IEnumerable<IMedia> GetMediaInRecycleBin()
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMedia> GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out long totalRecords,
|
||||
IQuery<IMedia> filter = null, Ordering ordering = null)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
if (ordering == null)
|
||||
ordering = Ordering.By("Path");
|
||||
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
var query = Query<IMedia>().Where(x => x.Path.StartsWith(Constants.System.RecycleBinMediaPathPrefix));
|
||||
return _mediaRepository.Get(query);
|
||||
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,25 +751,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
private void DeleteLocked(IScope scope, IMedia media)
|
||||
{
|
||||
// then recursively delete descendants, bottom-up
|
||||
// just repository.Delete + an event
|
||||
var stack = new Stack<IMedia>();
|
||||
stack.Push(media);
|
||||
var level = 1;
|
||||
while (stack.Count > 0)
|
||||
void DoDelete(IMedia c)
|
||||
{
|
||||
var c = stack.Peek();
|
||||
IMedia[] cc;
|
||||
if (c.Level == level)
|
||||
while ((cc = c.Children(this).ToArray()).Length > 0)
|
||||
{
|
||||
foreach (var ci in cc)
|
||||
stack.Push(ci);
|
||||
c = cc[cc.Length - 1];
|
||||
}
|
||||
c = stack.Pop();
|
||||
level = c.Level;
|
||||
|
||||
_mediaRepository.Delete(c);
|
||||
var args = new DeleteEventArgs<IMedia>(c, false); // raise event & get flagged files
|
||||
scope.Events.Dispatch(Deleted, this, args);
|
||||
@@ -891,6 +760,18 @@ namespace Umbraco.Core.Services.Implement
|
||||
_mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files
|
||||
(file, e) => Logger.Error<MediaService>(e, "An error occurred while deleting file attached to nodes: {File}", file));
|
||||
}
|
||||
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while(page * pageSize < total)
|
||||
{
|
||||
//get descendants - ordered from deepest to shallowest
|
||||
var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending));
|
||||
foreach (var c in descendants)
|
||||
DoDelete(c);
|
||||
}
|
||||
DoDelete(media);
|
||||
}
|
||||
|
||||
//TODO:
|
||||
@@ -1100,8 +981,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
moves.Add(Tuple.Create(media, media.Path)); // capture original path
|
||||
|
||||
// get before moving, in case uow is immediate
|
||||
var descendants = GetDescendants(media);
|
||||
//need to store the original path to lookup descendants based on it below
|
||||
var originalPath = media.Path;
|
||||
|
||||
// these will be updated by the repo because we changed parentId
|
||||
//media.Path = (parent == null ? "-1" : parent.Path) + "," + media.Id;
|
||||
@@ -1114,14 +995,21 @@ namespace Umbraco.Core.Services.Implement
|
||||
//paths[media.Id] = media.Path;
|
||||
paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : "-1") : parent.Path) + "," + media.Id;
|
||||
|
||||
foreach (var descendant in descendants)
|
||||
const int pageSize = 500;
|
||||
var page = 0;
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
|
||||
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
moves.Add(Tuple.Create(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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,12 +393,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
// fixme get rid of string filter?
|
||||
|
||||
public IEnumerable<IMember> GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "")
|
||||
public IEnumerable<IMember> GetAll(long pageIndex, int pageSize, out long totalRecords,
|
||||
string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "")
|
||||
{
|
||||
return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter);
|
||||
}
|
||||
|
||||
public IEnumerable<IMember> GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter)
|
||||
public IEnumerable<IMember> GetAll(long pageIndex, int pageSize, out long totalRecords,
|
||||
string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
|
||||
@@ -6,8 +6,8 @@ using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
@@ -24,91 +24,25 @@ namespace Umbraco.Core.Services.Implement
|
||||
private readonly IScopeProvider _uowProvider;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly INotificationsRepository _notificationsRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IContentSection _contentSection;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NotificationService(IScopeProvider provider, IUserService userService, IContentService contentService, ILogger logger,
|
||||
INotificationsRepository notificationsRepository, IGlobalSettings globalSettings)
|
||||
public NotificationService(IScopeProvider provider, IUserService userService, IContentService contentService, ILocalizationService localizationService,
|
||||
ILogger logger, INotificationsRepository notificationsRepository, IGlobalSettings globalSettings, IContentSection contentSection)
|
||||
{
|
||||
_notificationsRepository = notificationsRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_contentSection = contentSection;
|
||||
_uowProvider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
|
||||
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
|
||||
_localizationService = localizationService;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the notifications for the specified user regarding the specified node and action.
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="operatingUser"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="actionName"></param>
|
||||
/// <param name="http"></param>
|
||||
/// <param name="createSubject"></param>
|
||||
/// <param name="createBody"></param>
|
||||
/// <remarks>
|
||||
/// Currently this will only work for Content entities!
|
||||
/// </remarks>
|
||||
public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http,
|
||||
Func<IUser, string[], string> createSubject,
|
||||
Func<IUser, string[], string> createBody)
|
||||
{
|
||||
if (entity is IContent == false)
|
||||
throw new NotSupportedException();
|
||||
|
||||
var content = (IContent) entity;
|
||||
|
||||
// lazily get previous version
|
||||
IContentBase prevVersion = null;
|
||||
|
||||
// do not load *all* users in memory at once
|
||||
// do not load notifications *per user* (N+1 select)
|
||||
// cannot load users & notifications in 1 query (combination btw User2AppDto and User2NodeNotifyDto)
|
||||
// => get batches of users, get all their notifications in 1 query
|
||||
// re. users:
|
||||
// users being (dis)approved = not an issue, filtered in memory not in SQL
|
||||
// users being modified or created = not an issue, ordering by ID, as long as we don't *insert* low IDs
|
||||
// users being deleted = not an issue for GetNextUsers
|
||||
var id = Constants.Security.SuperUserId;
|
||||
var nodeIds = content.Path.Split(',').Select(int.Parse).ToArray();
|
||||
const int pagesz = 400; // load batches of 400 users
|
||||
do
|
||||
{
|
||||
// users are returned ordered by id, notifications are returned ordered by user id
|
||||
var users = ((UserService) _userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
|
||||
var notifications = GetUsersNotifications(users.Select(x => x.Id), action, nodeIds, Constants.ObjectTypes.Document).ToList();
|
||||
if (notifications.Count == 0) break;
|
||||
|
||||
var i = 0;
|
||||
foreach (var user in users)
|
||||
{
|
||||
// continue if there's no notification for this user
|
||||
if (notifications[i].UserId != user.Id) continue; // next user
|
||||
|
||||
// lazy load prev version
|
||||
if (prevVersion == null)
|
||||
{
|
||||
prevVersion = GetPreviousVersion(entity.Id);
|
||||
}
|
||||
|
||||
// queue notification
|
||||
var req = CreateNotificationRequest(operatingUser, user, content, prevVersion, actionName, http, createSubject, createBody);
|
||||
Enqueue(req);
|
||||
|
||||
// skip other notifications for this user
|
||||
while (i < notifications.Count && notifications[i++].UserId == user.Id) ;
|
||||
if (i >= notifications.Count) break; // break if no more notifications
|
||||
}
|
||||
|
||||
// load more users if any
|
||||
id = users.Count == pagesz ? users.Last().Id + 1 : -1;
|
||||
|
||||
} while (id > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previous version to the latest version of the content item if there is one
|
||||
/// </summary>
|
||||
@@ -131,20 +65,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="operatingUser"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="actionName"></param>
|
||||
/// <param name="http"></param>
|
||||
/// <param name="siteUri"></param>
|
||||
/// <param name="createSubject"></param>
|
||||
/// <param name="createBody"></param>
|
||||
/// <remarks>
|
||||
/// Currently this will only work for Content entities!
|
||||
/// </remarks>
|
||||
public void SendNotifications(IUser operatingUser, IEnumerable<IUmbracoEntity> entities, string action, string actionName, HttpContextBase http,
|
||||
Func<IUser, string[], string> createSubject,
|
||||
Func<IUser, string[], string> createBody)
|
||||
public void SendNotifications(IUser operatingUser, IEnumerable<IContent> entities, string action, string actionName, Uri siteUri,
|
||||
Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
|
||||
Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
|
||||
{
|
||||
if (entities is IEnumerable<IContent> == false)
|
||||
throw new NotSupportedException();
|
||||
|
||||
var entitiesL = entities as List<IContent> ?? entities.Cast<IContent>().ToList();
|
||||
var entitiesL = entities.ToList();
|
||||
|
||||
//exit if there are no entities
|
||||
if (entitiesL.Count == 0) return;
|
||||
@@ -156,7 +84,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
var prevVersionDictionary = new Dictionary<int, IContentBase>();
|
||||
|
||||
// see notes above
|
||||
var id = 0;
|
||||
var id = Constants.Security.SuperUserId;
|
||||
const int pagesz = 400; // load batches of 400 users
|
||||
do
|
||||
{
|
||||
@@ -185,7 +113,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
// queue notification
|
||||
var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, http, createSubject, createBody);
|
||||
var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, siteUri, createSubject, createBody);
|
||||
Enqueue(req);
|
||||
}
|
||||
|
||||
@@ -350,118 +278,141 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="content"></param>
|
||||
/// <param name="oldDoc"></param>
|
||||
/// <param name="actionName">The action readable name - currently an action is just a single letter, this is the name associated with the letter </param>
|
||||
/// <param name="http"></param>
|
||||
/// <param name="siteUri"></param>
|
||||
/// <param name="createSubject">Callback to create the mail subject</param>
|
||||
/// <param name="createBody">Callback to create the mail body</param>
|
||||
private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContentBase content, IContentBase oldDoc,
|
||||
string actionName, HttpContextBase http,
|
||||
Func<IUser, string[], string> createSubject,
|
||||
Func<IUser, string[], string> createBody)
|
||||
private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContent content, IContentBase oldDoc,
|
||||
string actionName,
|
||||
Uri siteUri,
|
||||
Func<(IUser user, NotificationEmailSubjectParams subject), string> createSubject,
|
||||
Func<(IUser user, NotificationEmailBodyParams body, bool isHtml), string> createBody)
|
||||
{
|
||||
if (performingUser == null) throw new ArgumentNullException("performingUser");
|
||||
if (mailingUser == null) throw new ArgumentNullException("mailingUser");
|
||||
if (content == null) throw new ArgumentNullException("content");
|
||||
if (http == null) throw new ArgumentNullException("http");
|
||||
if (siteUri == null) throw new ArgumentNullException("siteUri");
|
||||
if (createSubject == null) throw new ArgumentNullException("createSubject");
|
||||
if (createBody == null) throw new ArgumentNullException("createBody");
|
||||
|
||||
// build summary
|
||||
var summary = new StringBuilder();
|
||||
var props = content.Properties.ToArray();
|
||||
foreach (var p in props)
|
||||
|
||||
if (content.ContentType.VariesByNothing())
|
||||
{
|
||||
//fixme doesn't take into account variants
|
||||
|
||||
var newText = p.GetValue() != null ? p.GetValue().ToString() : "";
|
||||
var oldText = newText;
|
||||
|
||||
// check if something was changed and display the changes otherwise display the fields
|
||||
if (oldDoc.Properties.Contains(p.PropertyType.Alias))
|
||||
if (!_contentSection.DisableHtmlEmail)
|
||||
{
|
||||
var oldProperty = oldDoc.Properties[p.PropertyType.Alias];
|
||||
oldText = oldProperty.GetValue() != null ? oldProperty.GetValue().ToString() : "";
|
||||
//create the html summary for invariant content
|
||||
|
||||
// replace html with char equivalent
|
||||
ReplaceHtmlSymbols(ref oldText);
|
||||
ReplaceHtmlSymbols(ref newText);
|
||||
//list all of the property values like we used to
|
||||
summary.Append("<table style=\"width: 100 %; \">");
|
||||
foreach (var p in content.Properties)
|
||||
{
|
||||
//fixme doesn't take into account variants
|
||||
|
||||
var newText = p.GetValue() != null ? p.GetValue().ToString() : "";
|
||||
var oldText = newText;
|
||||
|
||||
// check if something was changed and display the changes otherwise display the fields
|
||||
if (oldDoc.Properties.Contains(p.PropertyType.Alias))
|
||||
{
|
||||
var oldProperty = oldDoc.Properties[p.PropertyType.Alias];
|
||||
oldText = oldProperty.GetValue() != null ? oldProperty.GetValue().ToString() : "";
|
||||
|
||||
// replace html with char equivalent
|
||||
ReplaceHtmlSymbols(ref oldText);
|
||||
ReplaceHtmlSymbols(ref newText);
|
||||
}
|
||||
|
||||
//show the values
|
||||
summary.Append("<tr>");
|
||||
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;border-bottom: 1px solid #CCC'>");
|
||||
summary.Append(p.PropertyType.Name);
|
||||
summary.Append("</th>");
|
||||
summary.Append("<td style='text-align: left; vertical-align: top;border-bottom: 1px solid #CCC'>");
|
||||
summary.Append(newText);
|
||||
summary.Append("</td>");
|
||||
summary.Append("</tr>");
|
||||
}
|
||||
summary.Append("</table>");
|
||||
}
|
||||
|
||||
}
|
||||
else if (content.ContentType.VariesByCulture())
|
||||
{
|
||||
//it's variant, so detect what cultures have changed
|
||||
|
||||
|
||||
// make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary
|
||||
// TODO: We should probably allow more than just tinymce??
|
||||
if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.TinyMce)
|
||||
&& string.CompareOrdinal(oldText, newText) != 0)
|
||||
if (!_contentSection.DisableHtmlEmail)
|
||||
{
|
||||
summary.Append("<tr>");
|
||||
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> Note: </th>");
|
||||
summary.Append(
|
||||
"<td style='text-align: left; vertical-align: top;'> <span style='background-color:red;'>Red for deleted characters</span> <span style='background-color:yellow;'>Yellow for inserted characters</span></td>");
|
||||
summary.Append("</tr>");
|
||||
summary.Append("<tr>");
|
||||
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> New ");
|
||||
summary.Append(p.PropertyType.Name);
|
||||
summary.Append("</th>");
|
||||
summary.Append("<td style='text-align: left; vertical-align: top;'>");
|
||||
summary.Append(ReplaceLinks(CompareText(oldText, newText, true, false, "<span style='background-color:yellow;'>", string.Empty), http.Request));
|
||||
summary.Append("</td>");
|
||||
summary.Append("</tr>");
|
||||
summary.Append("<tr>");
|
||||
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'> Old ");
|
||||
summary.Append(p.PropertyType.Name);
|
||||
summary.Append("</th>");
|
||||
summary.Append("<td style='text-align: left; vertical-align: top;'>");
|
||||
summary.Append(ReplaceLinks(CompareText(newText, oldText, true, false, "<span style='background-color:red;'>", string.Empty), http.Request));
|
||||
summary.Append("</td>");
|
||||
summary.Append("</tr>");
|
||||
//Create the html based summary (ul of culture names)
|
||||
|
||||
var culturesChanged = content.CultureInfos.Where(x => x.Value.WasDirty())
|
||||
.Select(x => x.Key)
|
||||
.Select(_localizationService.GetLanguageByIsoCode)
|
||||
.WhereNotNull()
|
||||
.Select(x => x.CultureName);
|
||||
summary.Append("<ul>");
|
||||
foreach (var culture in culturesChanged)
|
||||
{
|
||||
summary.Append("<li>");
|
||||
summary.Append(culture);
|
||||
summary.Append("</li>");
|
||||
}
|
||||
summary.Append("</ul>");
|
||||
}
|
||||
else
|
||||
{
|
||||
summary.Append("<tr>");
|
||||
summary.Append("<th style='text-align: left; vertical-align: top; width: 25%;'>");
|
||||
summary.Append(p.PropertyType.Name);
|
||||
summary.Append("</th>");
|
||||
summary.Append("<td style='text-align: left; vertical-align: top;'>");
|
||||
summary.Append(newText);
|
||||
summary.Append("</td>");
|
||||
summary.Append("</tr>");
|
||||
//Create the text based summary (csv of culture names)
|
||||
|
||||
var culturesChanged = string.Join(", ", content.CultureInfos.Where(x => x.Value.WasDirty())
|
||||
.Select(x => x.Key)
|
||||
.Select(_localizationService.GetLanguageByIsoCode)
|
||||
.WhereNotNull()
|
||||
.Select(x => x.CultureName));
|
||||
|
||||
summary.Append("'");
|
||||
summary.Append(culturesChanged);
|
||||
summary.Append("'");
|
||||
}
|
||||
summary.Append(
|
||||
"<tr><td colspan=\"2\" style=\"border-bottom: 1px solid #CCC; font-size: 2px;\"> </td></tr>");
|
||||
}
|
||||
else
|
||||
{
|
||||
//not supported yet...
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
string protocol = _globalSettings.UseHttps ? "https" : "http";
|
||||
var protocol = _globalSettings.UseHttps ? "https" : "http";
|
||||
|
||||
var subjectVars = new NotificationEmailSubjectParams(
|
||||
string.Concat(siteUri.Authority, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
|
||||
actionName,
|
||||
content.Name);
|
||||
|
||||
string[] subjectVars = {
|
||||
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
|
||||
actionName,
|
||||
content.Name
|
||||
};
|
||||
string[] bodyVars = {
|
||||
mailingUser.Name,
|
||||
actionName,
|
||||
content.Name,
|
||||
performingUser.Name,
|
||||
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
|
||||
content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(),
|
||||
string.Format("{2}://{0}/{1}",
|
||||
string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port),
|
||||
//TODO: RE-enable this so we can have a nice url
|
||||
/*umbraco.library.NiceUrl(documentObject.Id))*/
|
||||
string.Concat(content.Id, ".aspx"),
|
||||
protocol)
|
||||
|
||||
};
|
||||
var bodyVars = new NotificationEmailBodyParams(
|
||||
mailingUser.Name,
|
||||
actionName,
|
||||
content.Name,
|
||||
content.Id.ToString(CultureInfo.InvariantCulture),
|
||||
string.Format("{2}://{0}/{1}",
|
||||
string.Concat(siteUri.Authority),
|
||||
//TODO: RE-enable this so we can have a nice url
|
||||
/*umbraco.library.NiceUrl(documentObject.Id))*/
|
||||
string.Concat(content.Id, ".aspx"),
|
||||
protocol),
|
||||
performingUser.Name,
|
||||
string.Concat(siteUri.Authority, IOHelper.ResolveUrl(SystemDirectories.Umbraco)),
|
||||
summary.ToString());
|
||||
|
||||
// create the mail message
|
||||
var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email);
|
||||
var mail = new MailMessage(_contentSection.NotificationEmailAddress, mailingUser.Email);
|
||||
|
||||
// populate the message
|
||||
mail.Subject = createSubject(mailingUser, subjectVars);
|
||||
if (UmbracoConfig.For.UmbracoSettings().Content.DisableHtmlEmail)
|
||||
|
||||
|
||||
mail.Subject = createSubject((mailingUser, subjectVars));
|
||||
if (_contentSection.DisableHtmlEmail)
|
||||
{
|
||||
mail.IsBodyHtml = false;
|
||||
mail.Body = createBody(mailingUser, bodyVars);
|
||||
mail.Body = createBody((user: mailingUser, body: bodyVars, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -470,14 +421,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
string.Concat(@"<html><head>
|
||||
</head>
|
||||
<body style='font-family: Trebuchet MS, arial, sans-serif; font-color: black;'>
|
||||
", createBody(mailingUser, bodyVars));
|
||||
", createBody((user: mailingUser, body: bodyVars, true)));
|
||||
}
|
||||
|
||||
// nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here
|
||||
// adding the server name to make sure we don't replace external links
|
||||
if (_globalSettings.UseHttps && string.IsNullOrEmpty(mail.Body) == false)
|
||||
{
|
||||
string serverName = http.Request.ServerVariables["SERVER_NAME"];
|
||||
string serverName = siteUri.Host;
|
||||
mail.Body = mail.Body.Replace(
|
||||
string.Format("http://{0}", serverName),
|
||||
string.Format("https://{0}", serverName));
|
||||
@@ -486,12 +437,10 @@ namespace Umbraco.Core.Services.Implement
|
||||
return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email);
|
||||
}
|
||||
|
||||
private string ReplaceLinks(string text, HttpRequestBase request)
|
||||
private string ReplaceLinks(string text, Uri siteUri)
|
||||
{
|
||||
var sb = new StringBuilder(_globalSettings.UseHttps ? "https://" : "http://");
|
||||
sb.Append(request.ServerVariables["SERVER_NAME"]);
|
||||
sb.Append(":");
|
||||
sb.Append(request.Url.Port);
|
||||
sb.Append(siteUri.Authority);
|
||||
sb.Append("/");
|
||||
var domain = sb.ToString();
|
||||
text = text.Replace("href=\"/", "href=\"" + domain);
|
||||
@@ -505,6 +454,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="oldString">The old string.</param>
|
||||
private static void ReplaceHtmlSymbols(ref string oldString)
|
||||
{
|
||||
if (oldString.IsNullOrWhiteSpace()) return;
|
||||
oldString = oldString.Replace(" ", " ");
|
||||
oldString = oldString.Replace("’", "'");
|
||||
oldString = oldString.Replace("&", "&");
|
||||
@@ -512,69 +462,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
oldString = oldString.Replace("”", "”");
|
||||
oldString = oldString.Replace(""", "\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the text.
|
||||
/// </summary>
|
||||
/// <param name="oldText">The old text.</param>
|
||||
/// <param name="newText">The new text.</param>
|
||||
/// <param name="displayInsertedText">if set to <c>true</c> [display inserted text].</param>
|
||||
/// <param name="displayDeletedText">if set to <c>true</c> [display deleted text].</param>
|
||||
/// <param name="insertedStyle">The inserted style.</param>
|
||||
/// <param name="deletedStyle">The deleted style.</param>
|
||||
/// <returns></returns>
|
||||
private static string CompareText(string oldText, string newText, bool displayInsertedText,
|
||||
bool displayDeletedText, string insertedStyle, string deletedStyle)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var diffs = Diff.DiffText1(oldText, newText);
|
||||
|
||||
int pos = 0;
|
||||
for (var n = 0; n < diffs.Length; n++)
|
||||
{
|
||||
var it = diffs[n];
|
||||
|
||||
// write unchanged chars
|
||||
while ((pos < it.StartB) && (pos < newText.Length))
|
||||
{
|
||||
sb.Append(newText[pos]);
|
||||
pos++;
|
||||
} // while
|
||||
|
||||
// write deleted chars
|
||||
if (displayDeletedText && it.DeletedA > 0)
|
||||
{
|
||||
sb.Append(deletedStyle);
|
||||
for (var m = 0; m < it.DeletedA; m++)
|
||||
{
|
||||
sb.Append(oldText[it.StartA + m]);
|
||||
} // for
|
||||
sb.Append("</span>");
|
||||
}
|
||||
|
||||
// write inserted chars
|
||||
if (displayInsertedText && pos < it.StartB + it.InsertedB)
|
||||
{
|
||||
sb.Append(insertedStyle);
|
||||
while (pos < it.StartB + it.InsertedB)
|
||||
{
|
||||
sb.Append(newText[pos]);
|
||||
pos++;
|
||||
} // while
|
||||
sb.Append("</span>");
|
||||
} // if
|
||||
} // while
|
||||
|
||||
// write rest of unchanged chars
|
||||
while (pos < newText.Length)
|
||||
{
|
||||
sb.Append(newText[pos]);
|
||||
pos++;
|
||||
} // while
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
// manage notifications
|
||||
// ideally, would need to use IBackgroundTasks - but they are not part of Core!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user