Last active
September 28, 2020 12:41
-
-
Save martinrayenglish/fef1bce5796e2bc33ed1a81dac6b92d4 to your computer and use it in GitHub Desktop.
Improved Version of Sitecore Broken Links Removal tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<%@ Page Language="c#" EnableEventValidation="false" AutoEventWireup="true" EnableViewState="false" %> | |
<%@ Import Namespace="System.Data" %> | |
<%@ Import Namespace="Sitecore.Links" %> | |
<%@ Import Namespace="Sitecore.SecurityModel" %> | |
<%@ Import Namespace="Sitecore.StringExtensions" %> | |
<%@ Import Namespace="Sitecore.Collections" %> | |
<%@ Import Namespace="Sitecore.Configuration" %> | |
<%@ Import Namespace="Sitecore.Data" %> | |
<%@ Import Namespace="Sitecore.Data.DataProviders.Sql" %> | |
<%@ Import Namespace="Sitecore.Data.Fields" %> | |
<%@ Import Namespace="Sitecore.Data.Items" %> | |
<%@ Import Namespace="Sitecore.Data.SqlServer" %> | |
<%@ Import Namespace="Sitecore.Diagnostics" %> | |
<%@ Import Namespace="Sitecore.Diagnostics.PerformanceCounters" %> | |
<%@ Import Namespace="Sitecore.Globalization" %> | |
<script runat="server"> | |
private void OutputResult(string result) | |
{ | |
Response.ClearContent(); | |
Response.ContentType = "text/html"; | |
Response.Write(result); | |
} | |
protected void btnCleanupLinks_Click(object sender, EventArgs e) | |
{ | |
var brokenLinksHelper = new BrokenLinksEraserHelper(ddlDatabase.SelectedValue, txtStartPath.Text); | |
var result = brokenLinksHelper.FixBrokenLinksInDatabase(); | |
OutputResult(result.ToString()); | |
} | |
public class BrokenLinksEraserHelper | |
{ | |
private ItemLink[] _linksToFix = {}; | |
private readonly LockSet _locks = new LockSet(); | |
private SqlDataApi DataApi { get; set; } | |
private Database TargetDatabase { get; set; } | |
public string StartPathId { get; set; } | |
public enum LogType | |
{ | |
Info, | |
Error | |
}; | |
StringBuilder _result = new StringBuilder(); | |
public ItemLink[] ItemLinksToFix | |
{ | |
get | |
{ | |
if (_linksToFix.Length == 0) | |
{ | |
_linksToFix = GetLinksToFix(); | |
} | |
return _linksToFix; | |
} | |
} | |
public BrokenLinksEraserHelper(string targetDatabaseName, string startPathId) | |
{ | |
TargetDatabase = Factory.GetDatabase(targetDatabaseName); | |
StartPathId = startPathId; | |
DataApi = new SqlServerDataApi(ConfigurationManager.ConnectionStrings["core"].ConnectionString); | |
} | |
public StringBuilder FixBrokenLinksInDatabase() | |
{ | |
foreach (var itemLink in ItemLinksToFix) | |
{ | |
var sourceItem = itemLink.GetSourceItem(); | |
if (sourceItem == null) | |
{ | |
continue; | |
} | |
//Sitecore Remove Broken Links tool is missing this check and will throw an exception when itemLink.SourceFieldID is null | |
if (!Sitecore.Data.ID.IsNullOrEmpty(itemLink.SourceFieldID)) | |
{ | |
var field = FieldTypeManager.GetField(sourceItem.Fields[itemLink.SourceFieldID]); | |
using (new SecurityDisabler()) | |
using (new EditContext(sourceItem)) | |
{ | |
try | |
{ | |
field.RemoveLink(itemLink); | |
var message = "Removed broken link - Database: {0}, Item: {1}, Field: {2}, Target item database: {3}, Target item path: {4}".FormatWith(itemLink.SourceDatabaseName, sourceItem.Paths.FullPath, sourceItem.Fields[itemLink.SourceFieldID], itemLink.TargetDatabaseName, itemLink.TargetPath); | |
LogLinkActivity(LogType.Info, message, _result); | |
} | |
catch (Exception error) | |
{ | |
sourceItem.Editing.CancelEdit(); | |
var message = $"Source Item Update Field Error {sourceItem.ID}. Exception: {error.InnerException}"; | |
LogLinkActivity(LogType.Error, message, _result); | |
} | |
finally | |
{ | |
sourceItem.Editing.EndEdit(); | |
} | |
} | |
} | |
else | |
{ | |
//Remove templates based on branches that no longer exist | |
if (!sourceItem.BranchId.IsNull && TargetDatabase.GetItem(sourceItem.BranchId) == null) | |
{ | |
RemoveLegacyBranch(sourceItem.ID); | |
var message = $"Source Item ID: {sourceItem.ID} Removed Invalid Branch ID: {sourceItem.BranchId}"; | |
LogLinkActivity(LogType.Info, message, _result); | |
} | |
//Iterate through all field values, and remove all field values with invalid item references | |
RemoveAllInvalidFieldReferences(sourceItem); | |
} | |
} | |
Sitecore.Caching.CacheManager.ClearAllCaches(); | |
return _result; | |
} | |
private void RemoveAllInvalidFieldReferences(Item sourceItem) | |
{ | |
using (new SecurityDisabler()) | |
using (new EditContext(sourceItem)) | |
{ | |
sourceItem.Fields.ReadAll(); | |
foreach (Field field in sourceItem.Fields) | |
{ | |
sourceItem.Editing.BeginEdit(); | |
try | |
{ | |
ID targetItemId; | |
if (Sitecore.Data.ID.TryParse(field.Value, out targetItemId)) | |
{ | |
if (TargetDatabase.GetItem(targetItemId) == null) | |
{ | |
field.Value = string.Empty; | |
} | |
} | |
} | |
catch (Exception error) | |
{ | |
sourceItem.Editing.CancelEdit(); | |
var message = $"Source Item Update Field Error {sourceItem.ID}. Exception: {error.InnerException}"; | |
LogLinkActivity(LogType.Error, message, _result); | |
} | |
finally | |
{ | |
sourceItem.Editing.EndEdit(); | |
} | |
} | |
} | |
} | |
protected bool IsSourceItemContentOrMedia(Database database, ItemLink itemLink) | |
{ | |
Assert.ArgumentNotNull(database, nameof(database)); | |
Assert.ArgumentNotNull(itemLink, nameof(itemLink)); | |
var obj = database.GetItem(itemLink.SourceItemID); | |
return obj != null && (obj.Paths.IsContentItem || obj.Paths.IsMediaItem); | |
} | |
public virtual ItemLink[] ExcludeSystemItemLinks(Database database, IReadOnlyCollection<ItemLink> brokenLinks) | |
{ | |
Assert.ArgumentNotNull(brokenLinks, nameof(brokenLinks)); | |
return brokenLinks.Where(link => IsSourceItemContentOrMedia(database, link)).ToArray(); | |
} | |
public void RemoveLegacyBranch(ID itemId) | |
{ | |
Assert.ArgumentNotNull(itemId, nameof(itemId)); | |
DataApi.Execute("UPDATE Items SET {0}MasterId{1} = {2}EmptyGuid{3} WHERE {0}ID{1} = {2}ItemID{3}", "EmptyGuid", Guid.Empty, "ItemID", itemId.ToGuid()); | |
} | |
private ItemLink[] GetLinksToFix() | |
{ | |
var items = RebuildTargetItemLinks(); | |
var brokenLinks = GetBrokenLinks(TargetDatabase, items); | |
var itemLinkArray = ExcludeSystemItemLinks(TargetDatabase, brokenLinks); | |
var message = $"Rebuilt Target Item Links Of {items.Count()} Items"; | |
LogLinkActivity(LogType.Info, message, _result); | |
return (from itemInDefaultLanguage in items from itemLanguage in itemInDefaultLanguage.Languages select itemInDefaultLanguage.Database.GetItem(itemInDefaultLanguage.ID, itemLanguage) into item where item.Versions.Count > 0 from itemLink in itemLinkArray.Where(s => s.SourceItemID.Equals(item.ID)) select itemLink).ToArray(); | |
} | |
private IEnumerable<Item> RebuildTargetItemLinks() | |
{ | |
var items = TargetDatabase.SelectItems($"fast:{StartPathId}//*").ToList(); | |
//Get top level item | |
items.Add(TargetDatabase.GetItem(StartPathId)); | |
foreach (var itemInDefaultLanguage in items) | |
{ | |
var allLinks = itemInDefaultLanguage.Links.GetAllLinks(true); | |
UpdateLinks(itemInDefaultLanguage, allLinks); | |
} | |
return items; | |
} | |
protected void UpdateLinks(Item item, ItemLink[] links) | |
{ | |
Assert.ArgumentNotNull(item, nameof(item)); | |
Assert.ArgumentNotNull(links, nameof(links)); | |
lock (_locks.GetLock(item.ID)) | |
Factory.GetRetryer().ExecuteNoResult((Action) (() => | |
{ | |
using (var transaction = DataApi.CreateTransaction()) | |
{ | |
RemoveLinks(item); | |
foreach (var link in links) | |
{ | |
if (link.SourceItemID.IsNull) | |
{ | |
continue; | |
} | |
AddLink(item, link); | |
DataCount.LinksDataUpdated.Increment(1L); | |
} | |
transaction.Complete(); | |
} | |
})); | |
} | |
protected virtual void RemoveLinks(Item item) | |
{ | |
Assert.ArgumentNotNull(item, nameof(item)); | |
DataApi.Execute("DELETE\r\n FROM {0}Links{1}\r\n WHERE {0}SourceItemID{1} = {2}itemID{3}\r\n AND {0}SourceDatabase{1} = {2}database{3}", "itemID", item.ID.ToGuid(), "database", GetString(item.Database.Name, 150)); | |
} | |
protected void AddLink(Item item, ItemLink link) | |
{ | |
Assert.ArgumentNotNull(item, nameof(item)); | |
Assert.ArgumentNotNull(link, nameof(link)); | |
DataApi.Execute("INSERT INTO {0}Links{1}({0}SourceDatabase{1}, {0}SourceItemID{1}, {0}SourceLanguage{1}, {0}SourceVersion{1}, {0}SourceFieldID{1}, {0}TargetDatabase{1}, {0}TargetItemID{1}, {0}TargetLanguage{1}, {0}TargetVersion{1}, {0}TargetPath{1} ) VALUES( {2}database{3}, {2}itemID{3}, {2}sourceLanguage{3}, {2}sourceVersion{3}, {2}fieldID{3}, {2}targetDatabase{3}, {2}targetID{3}, {2}targetLanguage{3}, {2}targetVersion{3}, {2}targetPath{3} )", "itemID", item.ID.ToGuid(), "database", GetString(item.Database.Name, 150), "fieldID", link.SourceFieldID.ToGuid(), "sourceLanguage", GetString(link.SourceItemLanguage.ToString(), 50), "sourceVersion", link.SourceItemVersion.Number, "targetDatabase", GetString(link.TargetDatabaseName, 150), "targetID", link.TargetItemID.ToGuid(), "targetLanguage", GetString(link.TargetItemLanguage.ToString(), 50), "targetVersion", link.TargetItemVersion.Number, "targetPath", link.TargetPath); | |
} | |
protected virtual string GetString(string value, int maxLength) | |
{ | |
return value.Length <= maxLength ? value : value.Substring(0, maxLength); | |
} | |
private ItemLink[] GetBrokenLinks(Database database, IEnumerable<Item> items) | |
{ | |
Assert.ArgumentNotNull((object) database, nameof(database)); | |
var links = new List<ItemLink>(); | |
using (var reader = DataApi.CreateReader("SELECT {0}SourceItemID{1}, {0}SourceLanguage{1}, {0}SourceVersion{1}, {0}SourceFieldID{1}, {0}TargetDatabase{1}, {0}TargetItemID{1}, {0}TargetLanguage{1}, {0}TargetVersion{1}, {0}TargetPath{1}\r\n FROM {0}Links{1}\r\n WHERE {0}SourceDatabase{1} = {2}database{3}\r\n ORDER BY {0}SourceItemID{1}, {0}SourceFieldID{1}", nameof(database), GetString(database.Name, 150))) | |
{ | |
DataCount.LinksDataRead.Increment(); | |
AddBrokenLinks(reader.InnerReader, links, database, items); | |
return links.ToArray(); | |
} | |
} | |
private void AddBrokenLinks(IDataReader reader, List<ItemLink> links, Database database, IEnumerable<Item> items) | |
{ | |
using (new SecurityDisabler()) | |
{ | |
while (reader.Read()) | |
{ | |
var sourceItemId = Sitecore.Data.ID.Parse(reader.GetGuid(0)); | |
if (!items.Any(i => i.ID.Equals(sourceItemId))) | |
{ | |
continue; | |
} | |
var sourceItemLanguage = Language.Parse(reader.GetString(1)); | |
var sourceItemVersion = Sitecore.Data.Version.Parse(reader.GetInt32(2)); | |
var sourceFieldId = Sitecore.Data.ID.Parse(reader.GetGuid(3)); | |
var databaseName = reader.GetString(4); | |
var id = Sitecore.Data.ID.Parse(reader.GetGuid(5)); | |
var language = Language.Parse(reader.GetString(6)); | |
var version = Sitecore.Data.Version.Parse(reader.GetInt32(7)); | |
var str = reader.GetString(8); | |
var database1 = Factory.GetDatabase(databaseName); | |
if (!ItemExists(id, str, language, version, database1)) | |
{ | |
links.Add(new ItemLink(database.Name, sourceItemId, sourceItemLanguage, sourceItemVersion, sourceFieldId, database.Name, id, language, version, str)); | |
} | |
var job = Sitecore.Context.Job; | |
if (job != null && job.Category == "GetBrokenLinks") | |
{ | |
++job.Status.Processed; | |
} | |
DataCount.LinksDataRead.Increment(1L); | |
DataCount.DataPhysicalReads.Increment(1L); | |
} | |
} | |
} | |
protected virtual bool ItemExists(ID itemId, string itemPath, Language itemLanguage, Sitecore.Data.Version itemVersion, Database database) | |
{ | |
Item targetItem = (Item) null; | |
if (!itemId.IsNull) | |
{ | |
targetItem = database.GetItem(itemId); | |
if (targetItem == null) | |
{ | |
return false; | |
} | |
} | |
else if (!string.IsNullOrEmpty(itemPath)) | |
{ | |
targetItem = ItemUtil.GetItemFromPartialPath(itemPath, database); | |
} | |
if (targetItem == null) | |
{ | |
return false; | |
} | |
if (itemLanguage == Language.Invariant) | |
{ | |
return true; | |
} | |
Item versionedItem = targetItem.Database.GetItem(targetItem.ID, itemLanguage); | |
if (versionedItem == null) | |
{ | |
return false; | |
} | |
if (itemVersion == Sitecore.Data.Version.Latest) | |
{ | |
return versionedItem.Versions.Count > 0; | |
} | |
foreach (Sitecore.Data.Version versionNumber in versionedItem.Versions.GetVersionNumbers()) | |
{ | |
if (versionNumber.Number == itemVersion.Number) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
private void LogLinkActivity(LogType type, string message, StringBuilder result) | |
{ | |
message = $"[Broken Links Eraser] {message}"; | |
switch (type) | |
{ | |
case LogType.Info: | |
{ | |
Log.Info(message, this); | |
result.AppendLine($"<p>{message}</p>"); | |
break; | |
} | |
case LogType.Error: | |
{ | |
Log.Error(message, this); | |
result.AppendLine($"<p style='color:red'>{message}</p>"); | |
break; | |
} | |
default: | |
goto case LogType.Info; | |
} | |
} | |
public int BrokenLinksCount() | |
{ | |
return ItemLinksToFix.Length; | |
} | |
} | |
</script> | |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > | |
<html> | |
<head> | |
<title>Broken Links Eraser</title> | |
</head> | |
<body style="font-size: 14px"> | |
<form id="form1" runat="server"> | |
<fieldset style="width: 50%"> | |
<legend>Settings</legend> | |
<asp:Label ID="lblDatabase" runat="server" Text="Database"></asp:Label> | |
| |
<asp:DropDownList ID="ddlDatabase" runat="server"> | |
<asp:ListItem>master</asp:ListItem> | |
<asp:ListItem>web</asp:ListItem> | |
<asp:ListItem>core</asp:ListItem> | |
</asp:DropDownList> | |
<br /> | |
<br /> | |
<asp:Label ID="lblStartPath" runat="server" Text="Start Path (Eg. /sitecore/content/Site/Home"></asp:Label> | |
| |
<asp:TextBox ID="txtStartPath" runat="server" Width="242px"></asp:TextBox> | |
<br /> | |
<br /> | |
<asp:Button ID="btnCleanup" runat="server" OnClick="btnCleanupLinks_Click" Text="Erase Broken Links" /> | |
| |
</fieldset> | |
</form> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment