Skip to content

Instantly share code, notes, and snippets.

@martinrayenglish
Last active September 28, 2020 12:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save martinrayenglish/fef1bce5796e2bc33ed1a81dac6b92d4 to your computer and use it in GitHub Desktop.
Save martinrayenglish/fef1bce5796e2bc33ed1a81dac6b92d4 to your computer and use it in GitHub Desktop.
Improved Version of Sitecore Broken Links Removal tool
<%@ 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>
&nbsp;
<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>
&nbsp;
<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" />
&nbsp;
</fieldset>
</form>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment