Skip to content

Instantly share code, notes, and snippets.

@j3rbr0wn
Created June 12, 2023 12:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save j3rbr0wn/29a197364b60c57d619b8070016566d2 to your computer and use it in GitHub Desktop.
Save j3rbr0wn/29a197364b60c57d619b8070016566d2 to your computer and use it in GitHub Desktop.
Content Cleaner Tool for Optimizely CMS 12. Admin plugin to allow finding content of a specific type and deleting instances.
@using EPiServer.Framework.Localization
@using EPiServer.Web.Mvc.Html
@using EPiServer.Shell.Web.Mvc.Html
@using EPiServer.Core
@using EPiServer.Web
@using EPiServer.Web.Mvc
@using EPiServer.Web.Routing
@using MyCms12Website.Models.ViewModels
@using MyCms12Website.Models.Blocks
@using MyCms12Website.Models.Pages
@using Microsoft.AspNetCore.Mvc.Razor
@using Microsoft.AspNetCore.Html
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
using EPiServer.Authorization;
using EPiServer.Shell.Navigation;
namespace MyCms12Website.Business.Plugins;
[MenuProvider]
public class AdminToolMenuProvider : IMenuProvider
{
public IEnumerable<MenuItem> GetMenuItems()
{
var contentCleaner = new UrlMenuItem("Content Cleaner", MenuPaths.Global + "/cms/admin/contentcleanertool", "/contentcleanertool/index")
{
IsAvailable = _ => true,
SortIndex = 70,
AuthorizationPolicy = CmsPolicyNames.CmsAdmin
};
return new List<MenuItem>(1)
{
contentCleaner
};
}
}
@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation
@using EPiServer.Editor
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model MyCms12Website.Business.Plugins.ContentCleanerTool.ContentCleanerToolViewModel
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Content Type Cleaner Tool</title>
<!-- Shell -->
@ClientResources.RenderResources("ShellCore")
<!-- LightTheme -->
@ClientResources.RenderResources("ShellCoreLightTheme")
</head>
<body class="epi-workspace">
@Html.AntiForgeryToken()
@Html.Raw(Html.CreatePlatformNavigationMenu())
<div @Html.Raw(Html.ApplyPlatformNavigation())>
<div class="epi-contentContainer epi-padding">
<div class="epi-contentArea">
<h1 class="EP-prefix">Content Type Cleaner Tool</h1>
<div>
<p id="message"></p>
</div>
<div>
<form action="/contentcleanertool/index/" method="post" id="contentTypeSelectionForm">
<h2>Content Type Selection</h2>
<div>
<label asp-for="ContentTypeId">Select content type:</label>
<select asp-for="ContentTypeId" asp-items="@Model.ContentItems">
</select>
<button type="submit" class="epi-cmsButton">List content</button>
</div>
</form>
</div>
@if (Model.ContentUsages?.Any() ?? false)
{
<div>
<h2>Content</h2>
<table>
<tr>
<th>Id</th>
<th>Location</th>
<th></th>
<th></th>
</tr>
@foreach (var item in Model.ContentUsages)
{
<tr>
<td>
@item.Content.ContentLink.ID
</td>
<td>
<a href="@item.ContentUrl" target="_blank">
@item.Breadcrumb
</a>
</td>
<td>
<a href="@PageEditing.GetEditUrl(item.Content.ContentLink)" target="_blank">
Edit
</a>
</td>
<td>
<button class="js-delete-button" data-contentref="@item.Content.ContentLink">
Delete
</button>
</td>
</tr>
}
</table>
</div>
}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js" crossorigin="anonymous"></script>
<script>
$(document).ready(function () {
$('.js-delete-button').click(function () {
let contentRef = $(this).data('contentref');
$.get('/contentcleanertool/delete/', { contentRef: contentRef })
.done(function (data) {
$( "#contentTypeSelectionForm" ).trigger( "submit" );
})
.fail(function(jqXHR, textStatus, errorThrown) {
$( "#message" ).text( `Delete failed - ${jqXHR.responseText}` );
});
});
});
</script>
</body>
</html>
using EPiServer.Security;
using EPiServer.Web.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Net;
using System.Text;
namespace MyCms12Website.Business.Plugins.ContentCleanerTool.Controllers;
[Authorize(Roles = "Administrators")]
[Route("[controller]")]
public class ContentCleanerToolController : Controller
{
private readonly IContentRepository _contentRepository;
private readonly IContentModelUsage _contentModelUsage;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly IUrlResolver _urlResolver;
public ContentCleanerToolController(IUrlResolver urlResolver, IContentTypeRepository contentTypeRepository, IContentModelUsage contentModelUsage, IContentRepository contentRepository)
{
_urlResolver = urlResolver;
_contentTypeRepository = contentTypeRepository;
_contentModelUsage = contentModelUsage;
_contentRepository = contentRepository;
}
[Route("[action]")]
[HttpGet]
public IActionResult Index()
{
var model = new ContentCleanerToolViewModel()
{
ContentItems = GetContentTypes()
};
return View("/Business/Plugins/ContentCleanerTool/Views/ContentCleanerTool/Index.cshtml", model);
}
[Route("[action]")]
[HttpPost]
public IActionResult Index(ContentCleanerToolViewModel model)
{
model.ContentItems = GetContentTypes();
if (ModelState.IsValid)
{
model.ContentUsages = GetContentTypeUsages(model.ContentTypeId);
}
return View("/Business/Plugins/ContentCleanerTool/Views/ContentCleanerTool/Index.cshtml", model);
}
[Route("[action]")]
[HttpGet]
public IActionResult Delete(string contentRef)
{
try
{
if (ModelState.IsValid)
{
DeleteItem(contentRef);
}
return new OkResult();
}
catch (Exception ex)
{
return new ContentResult
{
StatusCode = (int)HttpStatusCode.InternalServerError,
ContentType = "application/json",
Content = $"Error - {ex.Message}"
};
}
}
private void DeleteItem(string contentRef)
{
_contentRepository.Delete(ContentReference.Parse(contentRef), forceDelete: false, access: AccessLevel.NoAccess);
}
private List<ContentUsageBreadcrumb<ContentUsage>> GetContentTypeUsages(int selectedTypeID)
{
var contentType = _contentTypeRepository.Load(selectedTypeID);
return _contentModelUsage.ListContentOfContentType(contentType)
.Select(x => new ContentUsageBreadcrumb<ContentUsage>(x)
{
Breadcrumb = BreadCrumb(x.ContentLink),
ContentUrl = _urlResolver.GetUrl(x.ContentLink)
})
.OrderBy(x => x.Name)
.ToList();
}
private IEnumerable<SelectListItem> GetContentTypes()
{
var uploads = _contentTypeRepository.List().OrderBy(p => p.Name);
return uploads.Select(x => new SelectListItem
{
Value = x.ID.ToString(),
Text = x.Name
}).ToList();
}
public string BreadCrumb(ContentReference cReference)
{
if (cReference == null)
return string.Empty;
var ancestor = _contentRepository.GetAncestors(cReference).ToList();
var content = _contentRepository.Get<IContent>(cReference);
var path = new StringBuilder();
for (var x = ancestor.Count - 1; x >= 0; x--)
{
path.Append($"\\{ancestor[x].Name}");
}
return $"{path}\\{content.Name}".Trim("\\".ToCharArray());
}
}
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MyCms12Website.Business.Plugins.ContentCleanerTool;
public class ContentCleanerToolViewModel
{
public IEnumerable<SelectListItem> ContentItems { get; set; }
public IEnumerable<ContentUsageBreadcrumb<ContentUsage>> ContentUsages { get; set; } = new List<ContentUsageBreadcrumb<ContentUsage>>();
public int ContentTypeId { get; set; }
}
public class ContentUsageBreadcrumb<TContent> : ContentUsage
{
public ContentUsageBreadcrumb(TContent content)
{
Content = content;
}
public TContent Content { get; }
public string ContentUrl { get; set; }
public string Breadcrumb { get; set; }
}
@PNergard
Copy link

Nice one getting this to CMS 12.

@Brian-Jaxon
Copy link

Nice Tool. Can you explain the need for the ModelState.IsValid check in the POST Method of the controller? For me it only returns content items if I comment out that check and I'm curious about it's intented purpose.

@j3rbr0wn
Copy link
Author

j3rbr0wn commented Jan 31, 2024

@Brian-Jaxon if ModelState.IsValid always returns false this could be the reason https://stackoverflow.com/a/72689810

@GeekInTheNorth
Copy link

@j3rbr0wn You should stick all this in a Razor Class Library and put it onto the Optimizely Nuget Feed :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment