Skip to content

Instantly share code, notes, and snippets.

@AThraen
Last active June 3, 2022 12:36
Show Gist options
  • Select an option

  • Save AThraen/9cde9a64c365e8dfb03163c0e3171bfb to your computer and use it in GitHub Desktop.

Select an option

Save AThraen/9cde9a64c365e8dfb03163c0e3171bfb to your computer and use it in GitHub Desktop.
ContentReportV2
using EPiServer;
using EPiServer.Approvals;
using EPiServer.Approvals.ContentApprovals;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Personalization.VisitorGroups;
using EPiServer.Web;
using EPiServer.Web.Mvc.Html;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
namespace CodeArt.Episerver.ContentReport
{
public class ContentAnalyzer
{
private readonly IContentRepository _repo;
private readonly IContentVersionRepository _vrepo;
private readonly ISiteDefinitionRepository _sites;
private readonly IContentTypeRepository _types;
private readonly ISiteDefinitionResolver _resolver;
private readonly IApprovalDefinitionRepository _apdefrepo;
private readonly IVisitorGroupRepository _vgrepo;
private readonly IContentSoftLinkRepository _slrepo;
private readonly UrlHelper _url;
private readonly int _trashid;
public List<ContentDetails> ContentDetails{ get; set; }
public List<VisitorGroupUse> VisitorGroupUses { get; set; }
public List<VisitorGroupDetails> VisitorGroupDetails { get; set; }
protected Dictionary<ContentReference, List<ContentReference>> usedBy { get; set; }
public ContentAnalyzer(IContentRepository repo, IContentVersionRepository vrepo, ISiteDefinitionRepository sites, IContentTypeRepository types, ISiteDefinitionResolver resolver, IApprovalDefinitionRepository apdefrepo, IVisitorGroupRepository vgrepo, IContentSoftLinkRepository slrepo)
{
_repo = repo;
_vrepo = vrepo;
_sites = sites;
_types = types;
_resolver = resolver;
_apdefrepo = apdefrepo;
_vgrepo = vgrepo;
_slrepo = slrepo;
_url = new UrlHelper();
_trashid = _types.Load("SysRecycleBin").ID;
}
public ContentDetails AnalyzeContent(ContentReference contentRef, string lang=null)
{
var mastercnt = (lang == null) ? _repo.Get<IContent>(contentRef) : _repo.Get<IContent>(contentRef, new CultureInfo(lang));
if (mastercnt == null)
{
return null;
}
if (mastercnt.ContentTypeID == _trashid) return null; //Skip if it's the recycle bin
//Get ancestors
var ancestors = _repo.GetAncestors(contentRef).Select(a => a.Name).Reverse().ToList();
//Get versions
var versions = (lang == null) ? _vrepo.List(contentRef) : _vrepo.List(contentRef, lang);
//Which version should we use?
var draft = (lang == null) ? versions.Where(cv => cv.IsMasterLanguageBranch).OrderByDescending(cv => cv.MasterVersionID).First() : versions.Where(cv => cv.LanguageBranch == lang).OrderByDescending(cv => cv.MasterVersionID).First();
var draftcnt = (lang == null) ? _repo.Get<IContent>(draft.ContentLink) : _repo.Get<IContent>(draft.ContentLink, new CultureInfo(lang));
var refs = _repo.GetReferencesToContent(contentRef, false); //Does this work?
var site = _resolver.GetByContent(contentRef, true);
var apdef = _apdefrepo.ResolveAsync(contentRef).Result;
var revs = apdef?.Definition?.Steps?.SelectMany(st => st.Reviewers).Select(r => r.Name).ToArray();
string reviewers = (revs == null || apdef.Definition.IsEnabled == false) ? "(No Approval Flow)" : string.Join(",", revs);
ContentDetails rt = new ContentDetails()
{
ContentId = contentRef.ToReferenceWithoutVersion().ToString(),
Language = (mastercnt is ILocalizable) ? (mastercnt as ILocalizable).Language.TwoLetterISOLanguageName : "",
ContentType = _types.Load(mastercnt.ContentTypeID).Name,
MainType = (mastercnt is PageData) ? "Page" : (mastercnt is MediaData) ? "Media" : (mastercnt is BlockData) ? "Block" : "Other",
Url = (mastercnt is PageData || mastercnt is MediaData) ? _url.ContentUrl(contentRef) : string.Empty,
SiteName = site?.Name,
Breadcrumb = string.Join(" > ", ancestors.ToArray()),
Name = draftcnt.Name,
DraftStatus = draft.Status.ToString(),
CreatedBy = (draftcnt as IChangeTrackable)?.CreatedBy,
CreatedDate = (draftcnt as IChangeTrackable)?.Created,
ChangedBy = (draftcnt as IChangeTrackable)?.ChangedBy,
ChangedDate = (draftcnt as IChangeTrackable)?.Changed,
SavedBy = draft.SavedBy,
SavedDate = draft.Saved,
PublishedDate = (mastercnt is IVersionable) ? (mastercnt as IVersionable).StartPublish : null,
StopPublishDate = (mastercnt is IVersionable) ? (mastercnt as IVersionable).StopPublish : null,
Languages = (mastercnt is ILocalizable)? string.Join(", ", (mastercnt as ILocalizable).ExistingLanguages.Select(ci => ci.TwoLetterISOLanguageName).ToArray()) : string.Empty,
UsesCount = refs.Select(r => r.ReferencedID.ToReferenceWithoutVersion().ToString()).Distinct().Count(),
Uses = string.Join(", ", refs.Select(r => r.ReferencedID.ToReferenceWithoutVersion().ToString()).Distinct().ToArray()),
Versions = versions.Count(),
Reviewers = reviewers
};
return rt;
}
protected IEnumerable<VisitorGroupUse> AnalyzeVisitorGroups(IContent c)
{
foreach (var p in c.Property)
{
if (p.Value == null) continue;
if (p.PropertyValueType == typeof(ContentArea))
{
var ca = p.Value as ContentArea;
if (ca == null) continue;
foreach (var f in ca.Items.Where(l => l.AllowedRoles != null && l.AllowedRoles.Any()))
{
//Match! This page uses the visitor groups in l.AllowedRoles. Record.
foreach (var r in f.AllowedRoles)
{
VisitorGroupUse vgu = new VisitorGroupUse();
vgu.VisitorGroup = r;
vgu.Content = c.ContentLink;
vgu.PropertyName = p.Name;
vgu.ContentName = (c as IContent).Name;
vgu.ContentType = (c is PageData) ? "Page" : (c is BlockData) ? "Block" : "Other";
yield return vgu;
}
}
}
else if (p.PropertyValueType == typeof(XhtmlString))
{
var ca = p.Value as XhtmlString;
if (ca == null) continue;
foreach (var f in ca.Fragments.Where(fr => fr is EPiServer.Core.Html.StringParsing.PersonalizedContentFragment))
{
var j = f as EPiServer.Core.Html.StringParsing.PersonalizedContentFragment;
var roles = j.GetRoles();
foreach (var r in roles)
{
VisitorGroupUse vgu = new VisitorGroupUse();
vgu.VisitorGroup = r;
vgu.Content = c.ContentLink;
vgu.PropertyName = p.Name;
vgu.ContentName = (c as IContent).Name;
vgu.ContentType = (c is PageData) ? "Page" : (c is BlockData) ? "Block" : "Other";
yield return vgu;
}
}
}
}
}
public void AnalyzeAllVisitorGroups(Action<string> StatusChanged, Func<bool> ShouldStop)
{
VisitorGroupDetails = new List<VisitorGroupDetails>();
var visitorgrouplist = _vgrepo.List().ToList();
foreach (var vg in visitorgrouplist)
{
StatusChanged($"Analyzing visitor group {vg.Name}");
foreach(var crt in vg.Criteria)
{
var vgd = new VisitorGroupDetails()
{
Name = vg.Name,
Notes = vg.Notes,
Operator = vg.CriteriaOperator.ToString(),
IsSecurity = vg.IsSecurityRole,
EnableStatistics = vg.EnableStatistics,
PointsThreshold = vg.PointsThreshold,
CriteriaType = crt.TypeName,
CriteriaModel = JsonConvert.SerializeObject(crt.Model,Formatting.Indented),
CriteriaPoints = crt.Points,
CriteriaRequired = crt.Required,
DefinitionName = crt.Definition.DisplayName
};
VisitorGroupDetails.Add(vgd);
}
if (ShouldStop()) break;
}
}
public void AnalyzeAllContent(Action<string> StatusChanged, Func<bool> ShouldStop)
{
ContentDetails = new List<ContentDetails>();
VisitorGroupUses = new List<VisitorGroupUse>();
Queue<ContentReference> todo = new Queue<ContentReference>();
Dictionary<ContentReference, bool> done = new Dictionary<ContentReference, bool>();
usedBy = new Dictionary<ContentReference, List<ContentReference>>();
todo.Enqueue(ContentReference.GlobalBlockFolder);
todo.Enqueue(ContentReference.SiteBlockFolder);
_sites.List().ToList().ForEach(sd => {
todo.Enqueue(sd.ContentAssetsRoot);
todo.Enqueue(sd.GlobalAssetsRoot);
todo.Enqueue(sd.SiteAssetsRoot);
todo.Enqueue(sd.StartPage);
});
while (todo.Count > 0)
{
var cr = todo.Dequeue();
if (done.ContainsKey(cr)) continue;
done.Add(cr, true);
var cnt = _repo.Get<IContent>(cr);
if (cnt == null) continue;
if (cnt is ILocalizable && (cnt as ILocalizable).ExistingLanguages.Count() > 1)
{
var langs = (cnt as ILocalizable).ExistingLanguages.Select(ci => ci.TwoLetterISOLanguageName);
foreach (var l in langs.Where(ll => ll != (cnt as ILocalizable).Language.TwoLetterISOLanguageName))
{
//Analyze other language versions of content
var langcntdetails = AnalyzeContent(cr, l);
if (langcntdetails != null) ContentDetails.Add(langcntdetails);
}
}
var cntdetails = AnalyzeContent(cr);
if (cntdetails != null)
{
ContentDetails.Add(cntdetails);
var refs = _repo.GetReferencesToContent(cr, false);
usedBy.Add(cr.ToReferenceWithoutVersion(), _slrepo.Load(cr.ToReferenceWithoutVersion(), true).Select(sl => sl.OwnerContentLink.ToReferenceWithoutVersion()).ToList());
VisitorGroupUses.AddRange(AnalyzeVisitorGroups(cnt));
}
var children = _repo.GetChildren<IContent>(cr);
children.ToList().ForEach(c => todo.Enqueue(c.ContentLink));
if (ShouldStop()) return;
StatusChanged($"Analyzing Content. In progress, done with {ContentDetails.Count} content items, {VisitorGroupUses.Count} usages of visitorgroups, {todo.Count} content items waiting in queue.");
}
//Update Visitor Group Uses
var visitorgrouplist = _vgrepo.List().ToList();
var vgcount = VisitorGroupUses.Count;
for(int i = 0; i < vgcount; i++)
{
var vgu = VisitorGroupUses[i];
var vg = visitorgrouplist.Where(g => g.Id.ToString() == vgu.VisitorGroup).FirstOrDefault();
vgu.VisitorGroup = vg.Name;
if (vgu.ContentType == "Page")
{
vgu.PageId = vgu.Content.ToReferenceWithoutVersion().ToString();
vgu.PageName = vgu.ContentName;
} else
{
var pages = new List<PageData>();
var lst = usedBy[vgu.Content];
while (lst.Any())
{
var itm = _repo.Get<IContent>(lst.First());
lst.RemoveAt(0);
if (itm is PageData) pages.Add((PageData)itm);
else if (usedBy.ContainsKey(itm.ContentLink.ToReferenceWithoutVersion()))
{
lst.AddRange(usedBy[itm.ContentLink.ToReferenceWithoutVersion()]);
}
}
VisitorGroupUses.AddRange(pages.Select(p => new VisitorGroupUse()
{
VisitorGroup = vgu.VisitorGroup,
Content = vgu.Content,
PropertyName = vgu.PropertyName,
ContentName = vgu.ContentName,
ContentType = vgu.ContentType,
PageId = p.ContentLink.ToReferenceWithoutVersion().ToString(),
PageName = p.Name
}));
}
}
// List visitor groups that personalize each content item
ContentDetails.ForEach(cd =>
{
cd.VisitorGroupsUsed = String.Join(",", VisitorGroupUses.Where(vgu => vgu.PageId == cd.ContentId || vgu.Content.ToReferenceWithoutVersion().ToString() == cd.ContentId).Select(vgu => vgu.VisitorGroup).Distinct().OrderBy(vgu => vgu));
});
}
}
public class ContentDetails
{
[ReportColumn(Title = "Content ID", Order = 10)]
public string ContentId { get; set; }
[ReportColumn(Title = "Language", Order = 15)]
public string Language { get; set; }
[ReportColumn(Title = "Content Type", Order = 20)]
public string ContentType { get; set; }
[ReportColumn(Title = "Main Type", Order = 30)]
public string MainType { get; set; }
[ReportColumn(Title = "Url", Order = 40)]
public string Url { get; set; }
[ReportColumn(Title = "Site Name", Order = 50)]
public string SiteName { get; set; }
[ReportColumn(Title = "Breadcrumb", Order = 60)]
public string Breadcrumb { get; set; }
[ReportColumn(Title = "Name", Order = 70)]
public string Name { get; set; }
[ReportColumn(Title = "DraftStatus", Order = 80)]
public string DraftStatus { get; set; }
[ReportColumn(Title = "Created By", Order = 90)]
public string CreatedBy { get; set; }
[ReportColumn(Title = "Created", Order = 100, FormatString = "yyyy-MM-dd HH:mm")]
public DateTime? CreatedDate { get; set; }
[ReportColumn(Title = "Changed By", Order = 110)]
public string ChangedBy { get; set; }
[ReportColumn(Title = "Changed", Order = 120, FormatString = "yyyy-MM-dd HH:mm")]
public DateTime? ChangedDate { get; set; }
[ReportColumn(Title = "Saved By", Order = 130)]
public string SavedBy { get; set; }
[ReportColumn(Title = "Saved", Order = 140, FormatString = "yyyy-MM-dd HH:mm")]
public DateTime? SavedDate { get; set; }
[ReportColumn(Title = "Published", Order = 150, FormatString = "yyyy-MM-dd HH:mm")]
public DateTime? PublishedDate { get; set; }
[ReportColumn(Title = "Published until", Order = 160, FormatString = "yyyy-MM-dd HH:mm", DefaultValue ="")]
public DateTime? StopPublishDate{ get; set; }
[ReportColumn(Title = "Master Language", Order = 170)]
public string MasterLanguage { get; set; }
[ReportColumn(Title = "Languages", Order = 180)]
public string Languages { get; set; }
[ReportColumn(Title = "How many places is it used", Order = 190)]
public int UsesCount { get; set; }
[ReportColumn(Title = "Where is it used", Order = 200)]
public string Uses { get; set; }
[ReportColumn(Title = "How many versions", Order = 210)]
public int Versions { get; set; }
[ReportColumn(Title = "List of reviewers", Order = 220)]
public string Reviewers { get; set; }
[ReportColumn(Title = "Visitor Groups used", Order = 230)]
public string VisitorGroupsUsed { get; set; }
}
public class VisitorGroupDetails
{
[ReportColumn(Title = "Visitor Group Name", Order = 10)]
public string Name { get; set; }
[ReportColumn(Title = "Visitor Group Notes", Order = 20)]
public string Notes { get; set; }
[ReportColumn(Title = "Criteria Operator", Order = 30)]
public string Operator { get; set; }
[ReportColumn(Title = "Is Security Role", Order = 40)]
public bool IsSecurity { get; set; }
[ReportColumn(Title = "Enable statistics", Order = 50)]
public bool EnableStatistics { get; set; }
[ReportColumn(Title = "Points Threshold", Order = 60)]
public int PointsThreshold { get; set; }
[ReportColumn(Title = "Criteria Name", Order = 70)]
public string DefinitionName { get; set; }
[ReportColumn(Title = "Criteria Type", Order = 80)]
public string CriteriaType { get; set; }
[ReportColumn(Title = "Criteria Model", Order = 90)]
public string CriteriaModel { get; set; }
[ReportColumn(Title = "Criteria is Required", Order = 100)]
public bool CriteriaRequired { get; set; }
[ReportColumn(Title = "Criteria Points", Order = 110)]
public int CriteriaPoints { get; set; }
}
//TODO: Content Type Use
//TODO: Language Use
public class VisitorGroupUse
{
[ReportColumn(Title = "Page Id", Order = 10)]
public string PageId { get; set; }
[ReportColumn(Title = "Page Name", Order = 20)]
public string PageName { get; set; }
[ReportColumn(Title = "Visitor Group", Order = 30)]
public string VisitorGroup { get; set; }
[ReportColumn(Title = "Property Name", Order = 40)]
public string PropertyName { get; set; }
[ReportColumn(Title = "Content Name", Order = 60)]
public string ContentName { get; set; }
[ReportColumn(Title = "Content Type", Order = 70)]
public string ContentType { get; set; }
[ReportColumn(Title = "Content Id", Order = 80)]
public ContentReference Content { get; set; }
}
/// <summary>
/// Contains details on how this should be used in the report. Including the column title, order, formatstring
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
public class ReportColumnAttribute : Attribute {
public string Title { get; set; }
public int Order { get; set; }
public string FormatString { get; set; }
public string DefaultValue { get; set; }
}
}
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Framework.Blobs;
using EPiServer.ServiceLocation;
using OfficeOpenXml;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CodeArt.Episerver.ContentReport
{
//Requires EPPlusFree, Nuget command: Install-Package EPPlusFree
public class ContentReport : IDisposable
{
private ExcelPackage package;
private Injected<ContentMediaResolver> _mediaResolver;
private Injected<IContentRepository> _repo;
private Injected<IContentTypeRepository> _types;
private Injected<IBlobFactory> _blobFactory;
public ContentReport()
{
package = new ExcelPackage();
}
public void AddListWorksheet<G>(string SheetName, IEnumerable<G> Items)
{
ExcelWorksheet ws = package.Workbook.Worksheets.Add(SheetName);
var type = typeof(G);
var properties=type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty).Where(pi => pi.GetCustomAttributes(typeof(ReportColumnAttribute), true).Any()).ToList();
var attributes = properties.Select(pi => new { property = pi, attribute = pi.GetCustomAttributes(typeof(ReportColumnAttribute), true).First() as ReportColumnAttribute }).OrderBy(rc => rc.attribute.Order).ToList();
//Write header
int column = 1;
foreach(var attr in attributes)
{
ws.Cells[1, column].Value = attr.attribute.Title ?? attr.property.Name;
ws.Cells[1, column].Style.Font.Bold = true;
column++;
}
int row = 2;
foreach(G itm in Items)
{
column = 1;
foreach (var attr in attributes)
{
//ws.Cells[row, column].Value= (attr.attribute.FormatString!=null)? attr.property.GetValue(itm).ToString()
object v = attr.property.GetValue(itm);
string cell = string.Empty;
if (v == null)
{
cell = attr.attribute.DefaultValue ?? string.Empty;
}
else if (v is DateTime?)
{
cell = ((DateTime?)v).Value.ToString(attr.attribute.FormatString ?? "yyyy-MM-dd HH:mm");
}
else
{
cell = v.ToString();
}
ws.Cells[row, column].Value = cell;
column++;
}
row++;
}
for (int i = 1; i < column; i++)
{
ws.Column(i).AutoFit();
}
}
public void Save(string Filename, ContentReference Parent)
{
Type t = _mediaResolver.Service.GetFirstMatching(Path.GetExtension(Filename));
var contentType=_types.Service.Load(t);
var reportcnt = _repo.Service.GetChildren<MediaData>(Parent).Where(md => md.Name == Filename && md.ContentTypeID == contentType.ID).FirstOrDefault();
if (reportcnt != null)
{
reportcnt = (MediaData)reportcnt.CreateWritableClone();
}
else
{
reportcnt = _repo.Service.GetDefault<MediaData>(Parent, contentType.ID);
reportcnt.Name = Filename;
}
var blob =_blobFactory.Service.CreateBlob(reportcnt.BinaryDataContainer, ".xlsx");
using (var s = blob.OpenWrite())
{
BinaryWriter w = new BinaryWriter(s);
w.Write(package.GetAsByteArray());
w.Flush();
}
reportcnt.BinaryData = blob;
_repo.Service.Save(reportcnt, EPiServer.DataAccess.SaveAction.Publish, EPiServer.Security.AccessLevel.NoAccess);
}
public void Dispose()
{
package.Dispose();
}
}
}
using EPiServer;
using EPiServer.Core;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using EPiServer.ServiceLocation;
using System;
using System.Linq;
namespace CodeArt.Episerver.ContentReport
{
[ScheduledPlugIn(DisplayName = "Content Report Job")]
public class ContentReportJob : ScheduledJobBase
{
private bool _stopSignaled;
private Injected<IContentRepository> _repo;
public ContentReportJob()
{
IsStoppable = true;
}
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
_stopSignaled = true;
}
/// <summary>
/// Called when a scheduled job executes
/// </summary>
/// <returns>A status message to be stored in the database log and visible from admin mode</returns>
public override string Execute()
{
OnStatusChanged("Starting to build report");
ContentAnalyzer contentAnalyzer = ServiceLocator.Current.GetInstance<ContentAnalyzer>();
//Ensure output folder
ContentReference outputRef = ContentReference.EmptyReference;
ContentFolder outputfolder = _repo.Service.GetChildren<ContentFolder>(ContentReference.GlobalBlockFolder).Where(cf => cf.Name == "Internal Reports").FirstOrDefault();
if (outputfolder == null)
{
var tmp = _repo.Service.GetDefault<ContentFolder>(ContentReference.GlobalBlockFolder);
tmp.Name = "Internal Reports";
//TODO: Access rights, only admin access!!
outputRef = _repo.Service.Save(tmp, EPiServer.DataAccess.SaveAction.Publish, EPiServer.Security.AccessLevel.NoAccess);
}
else outputRef = outputfolder.ContentLink;
//Analyze visitor groups
contentAnalyzer.AnalyzeAllVisitorGroups(OnStatusChanged, () => _stopSignaled);
//Analyze content
contentAnalyzer.AnalyzeAllContent(OnStatusChanged, () => _stopSignaled);
string repname = "Report " + DateTime.Now.ToString("yyyy-MM-dd HH:mm") + ".xlsx";
using (var report = new ContentReport())
{
report.AddListWorksheet<ContentDetails>("Content Details", contentAnalyzer.ContentDetails.OrderBy(ob => ob.CreatedDate));
report.AddListWorksheet<VisitorGroupDetails>("Visitor Group Details", contentAnalyzer.VisitorGroupDetails.OrderBy(ob => ob.Name));
report.AddListWorksheet<VisitorGroupUse>("Visitor Group Usage", contentAnalyzer.VisitorGroupUses.OrderBy(ob => ob.VisitorGroup));
report.Save(repname, outputRef);
}
return $"Job done. Analyzed {contentAnalyzer.ContentDetails.Count} content items and {contentAnalyzer.VisitorGroupDetails.Count} Visitor group details. Created report {repname}.";
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment