Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OrchardCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentLocalization/OrchardCore.ContentLocalization.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentPreview/OrchardCore.ContentPreview.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Contents.VersionPruning/OrchardCore.Contents.VersionPruning.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentTypes/OrchardCore.ContentTypes.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.CustomSettings/OrchardCore.CustomSettings.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using OrchardCore.Contents.VersionPruning.Drivers;
using OrchardCore.Navigation;

namespace OrchardCore.Contents.VersionPruning;

public sealed class AdminMenu : AdminNavigationProvider
{
private static readonly RouteValueDictionary _routeValues = new()
{
{ "area", "OrchardCore.Settings" },
{ "groupId", ContentVersionPruningSettingsDisplayDriver.GroupId },
};

internal readonly IStringLocalizer S;

public AdminMenu(IStringLocalizer<AdminMenu> stringLocalizer)
{
S = stringLocalizer;
}

protected override ValueTask BuildAsync(NavigationBuilder builder)
{
if (NavigationHelper.UseLegacyFormat())
{
builder
.Add(S["Configuration"], configuration => configuration
.Add(S["Settings"], settings => settings
.Add(S["Content Version Pruning"], S["Content Version Pruning"], pruning => pruning
.Action("Index", "Admin", _routeValues)
.Permission(ContentVersionPruningPermissions.ManageContentVersionPruningSettings)
.LocalNav()
)
)
);

return ValueTask.CompletedTask;
}

builder
.Add(S["Settings"], settings => settings
.Add(S["Content Version Pruning"], S["Content Version Pruning"].PrefixPosition(), pruning => pruning
.Action("Index", "Admin", _routeValues)
.Permission(ContentVersionPruningPermissions.ManageContentVersionPruningSettings)
.LocalNav()
)
);

return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using OrchardCore.Security.Permissions;

namespace OrchardCore.Contents.VersionPruning;

public static class ContentVersionPruningPermissions
{
public static readonly Permission ManageContentVersionPruningSettings = new(
"ManageContentVersionPruningSettings",
"Manage Content Version Pruning settings");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using OrchardCore.Contents.VersionPruning.Drivers;
using OrchardCore.Contents.VersionPruning.Models;
using OrchardCore.Contents.VersionPruning.Services;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Entities;
using OrchardCore.Modules;
using OrchardCore.Settings;

namespace OrchardCore.Contents.VersionPruning.Controllers;

public sealed class AdminController : Controller
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentVersionPruningService _pruningService;
private readonly ISiteService _siteService;
private readonly IClock _clock;
private readonly INotifier _notifier;

internal readonly IHtmlLocalizer H;

public AdminController(
IAuthorizationService authorizationService,
IContentVersionPruningService pruningService,
ISiteService siteService,
IClock clock,
INotifier notifier,
IHtmlLocalizer<AdminController> htmlLocalizer)
{
_authorizationService = authorizationService;
_pruningService = pruningService;
_siteService = siteService;
_clock = clock;
_notifier = notifier;
H = htmlLocalizer;
}

[HttpPost]
public async Task<IActionResult> Prune()
{
if (!await _authorizationService.AuthorizeAsync(User, ContentVersionPruningPermissions.ManageContentVersionPruningSettings))
{
return Forbid();
}

var settings = await _siteService.GetSettingsAsync<ContentVersionPruningSettings>();

var pruned = await _pruningService.PruneVersionsAsync(settings);

var container = await _siteService.LoadSiteSettingsAsync();
container.Alter<ContentVersionPruningSettings>(nameof(ContentVersionPruningSettings), settings =>
{
settings.LastRunUtc = _clock.UtcNow;
});

await _siteService.UpdateSiteSettingsAsync(container);

await _notifier.SuccessAsync(H["Content version pruning completed. {0} version(s) deleted.", pruned]);

return RedirectToAction("Index", "Admin", new
{
area = "OrchardCore.Settings",
groupId = ContentVersionPruningSettingsDisplayDriver.GroupId,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using OrchardCore.Contents.VersionPruning.Models;
using OrchardCore.Contents.VersionPruning.ViewModels;
using OrchardCore.DisplayManagement.Entities;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Settings;

namespace OrchardCore.Contents.VersionPruning.Drivers;

public sealed class ContentVersionPruningSettingsDisplayDriver : SiteDisplayDriver<ContentVersionPruningSettings>
{
public const string GroupId = "ContentVersionPruningSettings";

private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuthorizationService _authorizationService;

public ContentVersionPruningSettingsDisplayDriver(
IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
}

protected override string SettingsGroupId => GroupId;

public override async Task<IDisplayResult> EditAsync(ISite site, ContentVersionPruningSettings settings, BuildEditorContext context)
{
if (!await _authorizationService.AuthorizeAsync(
_httpContextAccessor.HttpContext?.User,
ContentVersionPruningPermissions.ManageContentVersionPruningSettings))
{
return null;
}

return Initialize<ContentVersionPruningSettingsViewModel>("ContentVersionPruningSettings_Edit", model =>
{
model.RetentionDays = settings.RetentionDays;
model.VersionsToKeep = settings.VersionsToKeep;
model.Disabled = settings.Disabled;
model.ContentTypes = settings.ContentTypes;
model.LastRunUtc = settings.LastRunUtc;
}).Location("Content:5")
.OnGroup(GroupId);
}

public override async Task<IDisplayResult> UpdateAsync(ISite site, ContentVersionPruningSettings settings, UpdateEditorContext context)
{
if (!await _authorizationService.AuthorizeAsync(
_httpContextAccessor.HttpContext?.User,
ContentVersionPruningPermissions.ManageContentVersionPruningSettings))
{
return null;
}

var model = new ContentVersionPruningSettingsViewModel();
await context.Updater.TryUpdateModelAsync(model, Prefix);

settings.RetentionDays = model.RetentionDays;
settings.VersionsToKeep = model.VersionsToKeep;
settings.Disabled = model.Disabled;
settings.ContentTypes = model.ContentTypes ?? [];

return await EditAsync(site, settings, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Name = "Content Version Pruning",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion,
Description = "Provides a background task to prune old content item versions.",
Dependencies = ["OrchardCore.Contents"],
Category = "Content Management"
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace OrchardCore.Contents.VersionPruning.Models;

public class ContentVersionPruningSettings
{
/// <summary>
/// The number of days after which non-latest, non-published content item versions are deleted.
/// </summary>
public int RetentionDays { get; set; } = 30;

/// <summary>
/// The number of the most-recent archived (non-latest, non-published) versions to retain
/// per content item, regardless of age.
/// </summary>
public int VersionsToKeep { get; set; } = 1;

/// <summary>
/// The content types to prune. When empty, all content types are pruned.
/// </summary>
public string[] ContentTypes { get; set; } = [];

/// <summary>
/// Whether the pruning background task is disabled.
/// </summary>
public bool Disabled { get; set; }

/// <summary>
/// The last time the pruning task was run.
/// </summary>
public DateTime? LastRunUtc { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<!-- NuGet properties-->
<Title>OrchardCore Contents Version Pruning</Title>
<Description>$(OCCMSDescription)

Provides a background task to prune old content item versions.</Description>
<PackageTags>$(PackageTags) OrchardCoreCMS ContentManagement</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Admin.Abstractions\OrchardCore.Admin.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement\OrchardCore.ContentManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.YesSql\OrchardCore.Data.YesSql.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Navigation.Core\OrchardCore.Navigation.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Settings.Core\OrchardCore.Settings.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using OrchardCore.Security.Permissions;

namespace OrchardCore.Contents.VersionPruning;

public sealed class Permissions : IPermissionProvider
{
private readonly IEnumerable<Permission> _allPermissions =
[
ContentVersionPruningPermissions.ManageContentVersionPruningSettings,
];

public Task<IEnumerable<Permission>> GetPermissionsAsync()
=> Task.FromResult(_allPermissions);

public IEnumerable<PermissionStereotype> GetDefaultStereotypes() =>
[
new PermissionStereotype
{
Name = OrchardCoreConstants.Roles.Administrator,
Permissions = _allPermissions,
},
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using OrchardCore.ContentManagement;

namespace OrchardCore.Contents.VersionPruning.Services;

public static class ContentVersionPruningSelector
{
public static List<ContentItem> SelectForDeletion(IEnumerable<ContentItem> candidates, int minVersionsToKeep)
{
var result = new List<ContentItem>();

foreach (var group in candidates.GroupBy(x => x.ContentItemId))
{
// Sort newest-first so Skip() protects the most-recent N versions.
// Null ModifiedUtc is treated as oldest: Nullable.Compare places null before
// any non-null value, so descending order puts null-dated versions last.
var ordered = group
.Where(x =>
!x.Latest &&
!x.Published)
.OrderByDescending(x => x.ModifiedUtc, Comparer<DateTime?>.Create(Nullable.Compare))
.Skip(minVersionsToKeep);

result.AddRange(ordered);
}

return result;
}
}
Loading
Loading