Compare commits

...

3 Commits

Author SHA1 Message Date
Benedek Farkas
8f3b3d451c Adding content type filter to the Recycle Bin 2026-01-07 19:35:39 +01:00
Milán Keszthelyi
165505b1db #8862: Preventing the SQL Server Service Broker feature from being enabled when not using SQL Server (Lombiq Technologies: ORCH-309) (#8875)
Some checks failed
Build Crowdin Translation Packages / build-crowdin-translation-packages (push) Has been cancelled
Compile / Compile .NET solution (push) Has been cancelled
Compile / Compile Client-side Assets (push) Has been cancelled
SpecFlow Tests / SpecFlow Tests (push) Has been cancelled
2025-12-09 00:05:04 +01:00
Benedek Farkas
d9d071115c ResourceManager skips rendering a resource without an URL (#8876) 2025-12-04 21:54:36 +01:00
8 changed files with 262 additions and 52 deletions

View File

@@ -4,7 +4,6 @@ using System.Linq;
using System.Web.Mvc;
using Orchard.AuditTrail.Helpers;
using Orchard.AuditTrail.Services;
using Orchard.AuditTrail.Services.Models;
using Orchard.AuditTrail.ViewModels;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;
@@ -28,7 +27,12 @@ namespace Orchard.AuditTrail.Controllers
private readonly IOrchardServices _services;
private readonly IRecycleBin _recycleBin;
public RecycleBinController(IAuthorizer authorizer, IContentManager contentManager, INotifier notifier, IOrchardServices services, IRecycleBin recycleBin)
public RecycleBinController(
IAuthorizer authorizer,
IContentManager contentManager,
INotifier notifier,
IOrchardServices services,
IRecycleBin recycleBin)
{
_authorizer = authorizer;
_contentManager = contentManager;
@@ -42,12 +46,14 @@ namespace Orchard.AuditTrail.Controllers
public Localizer T { get; set; }
public ILogger Logger { get; set; }
public ActionResult Index(PagerParameters pagerParameters, AuditTrailOrderBy? orderBy = null)
public ActionResult Index(PagerParameters pagerParameters, string contentTypeName = null)
{
if (!_authorizer.Authorize(Permissions.ViewAuditTrail))
{
return new HttpUnauthorizedResult();
}
var viewModel = SetupViewModel(new RecycleBinViewModel(), pagerParameters);
var viewModel = SetupViewModel(new RecycleBinViewModel(), pagerParameters, contentTypeName);
return View(viewModel);
}
@@ -55,8 +61,11 @@ namespace Orchard.AuditTrail.Controllers
public ActionResult Restore(int id, string returnUrl)
{
var contentItem = _contentManager.Get(id, VersionOptions.AllVersions);
if (!_authorizer.Authorize(Core.Contents.Permissions.PublishContent, contentItem))
{
return new HttpUnauthorizedResult();
}
var restoredContentItem = _recycleBin.Restore(contentItem);
var restoredContentItemTitle = _contentManager.GetItemMetadata(restoredContentItem).DisplayText;
@@ -104,14 +113,15 @@ namespace Orchard.AuditTrail.Controllers
return RedirectToAction("Index");
}
private RecycleBinViewModel SetupViewModel(RecycleBinViewModel viewModel, PagerParameters pagerParameters)
private RecycleBinViewModel SetupViewModel(RecycleBinViewModel viewModel, PagerParameters pagerParameters, string contentTypeName = null)
{
var pager = new Pager(_services.WorkContext.CurrentSite, pagerParameters);
var removedContentItems = _recycleBin.List(pager.Page, pager.PageSize);
var pagershape = _services.New.Pager(pager).TotalItemCount(removedContentItems.TotalItemCount);
var removedContentItems = _recycleBin.List(pager.Page, pager.PageSize, contentTypeName);
var pagerShape = _services.New.Pager(pager).TotalItemCount(removedContentItems.TotalItemCount);
viewModel.FilterContentType = contentTypeName;
viewModel.ContentItems = removedContentItems;
viewModel.Pager = pagershape;
viewModel.Pager = pagerShape;
return viewModel;
}
@@ -160,6 +170,5 @@ namespace Orchard.AuditTrail.Controllers
}
}
}
}
}

View File

@@ -9,26 +9,27 @@ namespace Orchard.AuditTrail.Services
/// <summary>
/// Returns all removed content items.
/// </summary>
IPageOfItems<ContentItem> List(int page, int pageSize);
IPageOfItems<ContentItem> List(int page, int pageSize, string contentTypeName = null);
/// <summary>
/// Returns all removed content items.
/// </summary>
IPageOfItems<T> List<T>(int page, int pageSize) where T : class, IContent;
IPageOfItems<T> List<T>(int page, int pageSize, string contentTypeName = null) where T : class, IContent;
/// <summary>
/// Returns the specified list of content items from the recycle bin.
/// </summary>
IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null);
IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null);
/// <summary>
/// Returns the specified list of content items from the recycle bin.
/// </summary>
IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null) where T : class, IContent;
IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null)
where T : class, IContent;
/// <summary>
/// Restores the specified content item.
/// </summary>
ContentItem Restore(ContentItem contentItem);
}
}
}

View File

@@ -4,7 +4,9 @@ using System.Linq;
using NHibernate;
using Orchard.Collections;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.Data;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
namespace Orchard.AuditTrail.Services
@@ -14,21 +16,29 @@ namespace Orchard.AuditTrail.Services
{
private readonly ITransactionManager _transactionManager;
private readonly IContentManager _contentManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly ShellSettings _shellSettings;
public RecycleBin(ITransactionManager transactionManager, IContentManager contentManager)
public RecycleBin(
ITransactionManager transactionManager,
IContentManager contentManager,
IContentDefinitionManager contentDefinitionManager,
ShellSettings shellSettings)
{
_transactionManager = transactionManager;
_contentManager = contentManager;
_contentDefinitionManager = contentDefinitionManager;
_shellSettings = shellSettings;
}
public IPageOfItems<ContentItem> List(int page, int pageSize)
public IPageOfItems<ContentItem> List(int page, int pageSize, string contentTypeName = null)
{
return List<ContentItem>(page, pageSize);
return List<ContentItem>(page, pageSize, contentTypeName);
}
public IPageOfItems<T> List<T>(int page, int pageSize) where T : class, IContent
public IPageOfItems<T> List<T>(int page, int pageSize, string contentTypeName = null) where T : class, IContent
{
var query = GetDeletedVersionsQuery();
var query = GetDeletedVersionsQuery(null, contentTypeName);
var totalCount = query.List().Count;
query.SetFirstResult((page - 1) * pageSize);
@@ -44,14 +54,13 @@ namespace Orchard.AuditTrail.Services
};
}
public IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null)
{
return GetMany<ContentItem>(contentItemIds, hints);
}
public IEnumerable<ContentItem> GetMany(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null) =>
GetMany<ContentItem>(contentItemIds, hints, contentTypeName);
public IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null) where T : class, IContent
public IEnumerable<T> GetMany<T>(IEnumerable<int> contentItemIds, QueryHints hints = null, string contentTypeName = null)
where T : class, IContent
{
var query = GetDeletedVersionsQuery(contentItemIds);
var query = GetDeletedVersionsQuery(contentItemIds, contentTypeName);
return LoadContentItems<T>(query, hints);
}
@@ -60,10 +69,10 @@ namespace Orchard.AuditTrail.Services
var versions = contentItem.Record.Versions.OrderBy(x => x.Number).ToArray();
var lastVersion = versions.Last();
if (lastVersion.Latest || lastVersion.Published)
throw new InvalidOperationException(string.Format("Cannot restore content item with ID {0} ftom the recycle bin, since that item is not deleted", contentItem.Id));
return _contentManager.Restore(contentItem, VersionOptions.Restore(lastVersion.Number, publish: false));
return lastVersion.Latest || lastVersion.Published
? throw new InvalidOperationException(
string.Format("Cannot restore content item with ID {0} from the recycle bin, since that item is not deleted.", contentItem.Id))
: _contentManager.Restore(contentItem, VersionOptions.Restore(lastVersion.Number, publish: false));
}
private IEnumerable<T> LoadContentItems<T>(IQuery query, QueryHints hints = null) where T : class, IContent
@@ -73,22 +82,53 @@ namespace Orchard.AuditTrail.Services
return _contentManager.GetManyByVersionId<T>(versionIds, hints ?? QueryHints.Empty);
}
private IQuery GetDeletedVersionsQuery(IEnumerable<int> contentItemIds = null)
private IQuery GetDeletedVersionsQuery(IEnumerable<int> contentItemIds = null, string contentTypeName = null)
{
var session = _transactionManager.GetSession();
// Select only the highest versions where both Published and Latest are false.
var select =
"select max(ContentItemVersionRecord.Id), ContentItemVersionRecord.ContentItemRecord.Id, max(ContentItemVersionRecord.Number) " +
"from Orchard.ContentManagement.Records.ContentItemVersionRecord ContentItemVersionRecord ";
var select = @"
select
max(contentItemVersionRecord.Id),
max(contentItemVersionRecord.Number),
contentItemVersionRecord.ContentItemRecord.Id
from
Orchard.ContentManagement.Records.ContentItemVersionRecord contentItemVersionRecord
left join
Orchard.Core.Common.Models.CommonPartVersionRecord commonPartVersionRecord
on
contentItemVersionRecord.Id = commonPartVersionRecord.Id";
var filter = contentItemIds != null ? "where ContentItemVersionRecord.ContentItemRecord.Id in (:ids) " : default(string);
var filter = contentItemIds == null
? default
: "\nwhere contentItemVersionRecord.ContentItemRecord.Id in (:ids)";
var group =
"group by ContentItemVersionRecord.ContentItemRecord.Id " +
"having max(cast(Latest as Int32)) = 0 and max(cast(Published as Int32)) = 0 ";
// ContentTypeName is safe to use in a query directly, because it's a technical name without special characters.
if (contentTypeName != null
&& _contentDefinitionManager.ListTypeDefinitions().Any(typeDefinition => typeDefinition.Name == contentTypeName))
{
filter += $@"
{(filter == default ? "where" : "and")}
contentItemVersionRecord.ContentItemRecord.ContentType.Name = '{contentTypeName}'";
}
var hql = string.Concat(select, filter, group);
var group = @"
group by
contentItemVersionRecord.ContentItemRecord.Id
having
max(cast(Latest as Int32)) = 0
and max(cast(Published as Int32)) = 0";
var order = "";
// SQL CE doesn't support ordering by an aggregate value.
if (!string.Equals(_shellSettings.DataProvider, "SqlCe", StringComparison.OrdinalIgnoreCase))
{
order = @"
order by
max(commonPartVersionRecord.ModifiedUtc) desc";
}
var hql = string.Concat(select, filter, group, order);
var query = session.CreateQuery(hql);
if (contentItemIds != null)
@@ -99,4 +139,4 @@ namespace Orchard.AuditTrail.Services
return query;
}
}
}
}

View File

@@ -10,9 +10,11 @@ namespace Orchard.AuditTrail.ViewModels
{
SelectedContentItems = new List<RemovedContentItemViewModel>(0);
}
public string FilterContentType { get; set; }
public RecycleBinCommand? RecycleBinCommand { get; set; }
public IList<RemovedContentItemViewModel> SelectedContentItems { get; set; }
public IPageOfItems<ContentItem> ContentItems { get; set; }
public dynamic Pager { get; set; }
}
}
}

View File

@@ -1,37 +1,95 @@
@using Orchard.AuditTrail.ViewModels
@using Orchard.AuditTrail.ViewModels
@using Orchard.ContentManagement
@using Orchard.Core.Common.Models
@using Orchard.Localization.Services
@using Orchard.ContentManagement.MetaData
@using Orchard.Core.Contents.Settings
@model RecycleBinViewModel
@{
Style.Include("audittrail-recycle-bin.css");
Script.Require("ShapesBase");
Script.Include(("audittrail-recyclebin.js"));
Layout.Title = T("Audit Trail");
var contentItems = Model.ContentItems;
var dateLocalizationServices = WorkContext.Resolve<IDateLocalizationServices>();
var listableContentTypeDefinitions = WorkContext.Resolve<IContentDefinitionManager>()
.ListTypeDefinitions()
.Where(definition => definition.Settings.GetModel<ContentTypeSettings>().Listable)
.ToList();
var selectedContentType = Request.QueryString["contentTypeName"];
var routeData = new RouteValueDictionary(ViewContext.RouteData.Values);
var queryString = ViewContext.HttpContext.Request.QueryString;
if (queryString != null)
{
foreach (string key in queryString.Keys)
{
if (key != null && !routeData.ContainsKey(key))
{
routeData[key] = queryString[key];
}
}
}
if (routeData.ContainsKey("id") && !HasText(routeData["id"]))
{
routeData.Remove("id");
}
}
<div id="recycle-bin">
@Html.ValidationSummary()
@using (Html.BeginFormAntiForgeryPost()) {
@using (Html.BeginForm("Index", "RecycleBin", FormMethod.Get))
{
<fieldset class="bulk-actions">
<label for="filterResults" class="bulk-filter">@T("Show")</label>
<select id="filterResults" name="contentTypeName">
@Html.SelectOption(Model.FilterContentType, "", T("any (show all)").Text)
@foreach (var contentTypeDefinition in listableContentTypeDefinitions)
{
@Html.SelectOption(Model.FilterContentType, contentTypeDefinition.Name, contentTypeDefinition.DisplayName)
}
</select>
<button type="submit">@T("Apply")</button>
</fieldset>
}
<br />
@using (Html.BeginFormAntiForgeryPost())
{
<fieldset class="bulk-actions">
<label>@T("Actions:")</label>
<select name="RecycleBinCommand">
<option></option>
<option value="@RecycleBinCommand.Restore" data-unsafe-action="@T("Are you sure you want to restore the selected items?")" @if(Model.RecycleBinCommand == RecycleBinCommand.Restore){<text>selected="selected"</text>}>@T("Restore")</option>
<option value="@RecycleBinCommand.Restore" data-unsafe-action="@T("Are you sure you want to restore the selected items?")" @if (Model.RecycleBinCommand == RecycleBinCommand.Restore) { <text> selected="selected" </text> }>
@T("Restore")
</option>
@**TODO: Decide wether or not to allow users to permanently delete items. Commented out for now.*@
@*<option value="@RecycleBinCommand.Destroy" data-unsafe-action="@T("WARNING: This will PERMANENTLY delete the selected content items, including related content part records, never to be seen again. Are you sure you want to do this?")" @if (Model.RecycleBinCommand == RecycleBinCommand.Destroy) { <text> selected="selected" </text> }>@T("Remove Permanently ☠")</option>*@
@*<option value="@RecycleBinCommand.Destroy" data-unsafe-action="@T("WARNING: This will PERMANENTLY delete the selected content items, including related content part records, never to be seen again. Are you sure you want to do this?")"
@if (Model.RecycleBinCommand == RecycleBinCommand.Destroy) { <text> selected="selected" </text> }>@T("Remove Permanently ☠")</option>*@
</select>
</fieldset>
<div class="bulk-actions">
<button type="submit" class="filter-apply-button" name="ExecuteActionButton" value="ExecuteActionButton">@T("Execute")</button>
</div>
<section class="recycle-bin-list-section">
@if (!contentItems.Any()) {
@if (!contentItems.Any())
{
<p class="info">@T("There are no records to display.")</p>
}
else {
else
{
<table class="items">
<thead>
<tr>
@@ -44,17 +102,18 @@
<tbody>
@{
var index = 0;
foreach (var contentItem in contentItems) {
foreach (var contentItem in contentItems)
{
var isSelected = Model.SelectedContentItems.Where(x => x.Id == contentItem.Id && x.Selected).Select(x => x.Id).Any();
var commonPart = contentItem.As<CommonPart>();
var removedText = commonPart != null ? dateLocalizationServices.ConvertToLocalizedString(commonPart.VersionModifiedUtc) : T("-").Text;
var contentDisplayTextHtmlString = Html.ItemDisplayText(contentItem);
var contentDisplayText = contentDisplayTextHtmlString != null ? contentDisplayTextHtmlString.ToString() : contentItem.ContentType;
var contentDisplayUrl = Url.Action("Detail", "Content", new {id = contentItem.Id, version = contentItem.Version, area = "Orchard.AuditTrail"});
var contentDisplayUrl = Url.Action("Detail", "Content", new { id = contentItem.Id, version = contentItem.Version, area = "Orchard.AuditTrail" });
<tr>
<td>
<input type="hidden" name="SelectedContentItems[@index].Id" value="@contentItem.Id" />
<input type="checkbox" name="SelectedContentItems[@index].Selected" value="true" @if(isSelected){<text>checked="checked"</text>} />
<input type="checkbox" name="SelectedContentItems[@index].Selected" value="true" @if (isSelected) { <text> checked="checked" </text> } />
</td>
<td class="content-column"><a href="@contentDisplayUrl">@contentDisplayText</a></td>
<td class="content-removed-column">@removedText</td>
@@ -67,7 +126,7 @@
@Html.ActionLink(T("View Audit Trail").Text, "Index", "Admin", new { content = contentItem.Id, area = "Orchard.AuditTrail" }, null)
</li>
<li class="action-link">
<a href="@Url.Action("Restore", "RecycleBin", new {id = contentItem.Id, area = "Orchard.AuditTrail"})" data-unsafe-url="@T("Are you sure you want to restore this item?")">@T("Restore")</a>
<a href="@Url.Action("Restore", "RecycleBin", new { id = contentItem.Id, area = "Orchard.AuditTrail" })" data-unsafe-url="@T("Are you sure you want to restore this item?")">@T("Restore")</a>
</li>
</ul>
</td>
@@ -83,4 +142,4 @@
@Display(Model.Pager)
</section>
}
</div>
</div>

View File

@@ -130,6 +130,7 @@
<Compile Include="Services\IHostNameProvider.cs" />
<Compile Include="Services\MessageBusNotificationProvider.cs" />
<Compile Include="Handler\MessageBusHandler.cs" />
<Compile Include="Services\SqlServerServiceBrokerFeatureGuard.cs" />
<Compile Include="SqlServerBrokerMigrations.cs" />
<Compile Include="Models\MessageRecord.cs" />
<Compile Include="Services\DefaultMessageBus.cs" />

View File

@@ -0,0 +1,96 @@
using System;
using System.Web.Mvc;
using Orchard.Environment;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions.Models;
using Orchard.Environment.Features;
using Orchard.Localization;
using Orchard.Mvc;
using Orchard.Mvc.Filters;
using Orchard.UI.Notify;
namespace Orchard.MessageBus.Services
{
/// <summary>
/// Prevents the SQL Server Service Broker feature from being enabled for tenants that are not using Microsoft SQL
/// Server as their database provider.
/// </summary>
/// <remarks>
/// The implementation is hackish but there seems to be no other way: if it would use <see cref="INotifier"/> it
/// wouldn't work since <see cref="NotifyFilter"/> that writes out notifications to TempData runs before feature
/// events are raised. Thus we have to manually add the messages to TempData, just as the filter would do.
/// </remarks>
public class SqlServerServiceBrokerFeatureGuard : FilterProvider, IFeatureEventHandler, IActionFilter
{
private const string FeatureId = "Orchard.MessageBus.SqlServerServiceBroker";
private const string TempDataKey = FeatureId + ".TempData";
private readonly ShellSettings _shellSettings;
private readonly IFeatureManager _featureManager;
private readonly IHttpContextAccessor _hca;
public Localizer T { get; set; }
public SqlServerServiceBrokerFeatureGuard(
ShellSettings shellSettings,
IFeatureManager featureManager,
IHttpContextAccessor hca)
{
_shellSettings = shellSettings;
_featureManager = featureManager;
_hca = hca;
T = NullLocalizer.Instance;
}
void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext) { }
void IActionFilter.OnActionExecuted(ActionExecutedContext filterContext)
{
var httpContext = _hca.Current();
if (httpContext == null) return;
httpContext.Items[TempDataKey] = filterContext.Controller.TempData;
}
public void Installing(Feature feature) { }
public void Installed(Feature feature) { }
public void Enabling(Feature feature) { }
public void Enabled(Feature feature)
{
if (feature.Descriptor.Id != FeatureId)
return;
if (!_shellSettings.DataProvider.Equals("SqlServer", StringComparison.OrdinalIgnoreCase))
{
_featureManager.DisableFeatures(new[] { FeatureId }, force: true);
AddNotification(
T("The SQL Server Service Broker cannot be enabled because it requires Microsoft SQL Server."),
NotifyType.Error);
}
}
public void Disabling(Feature feature) { }
public void Disabled(Feature feature) { }
public void Uninstalling(Feature feature) { }
public void Uninstalled(Feature feature) { }
private void AddNotification(LocalizedString message, NotifyType notifyType = NotifyType.Warning)
{
var tempDataDictionary = _hca.Current()?.Items[TempDataKey];
if (tempDataDictionary == null) return;
((TempDataDictionary)tempDataDictionary)["messages"] +=
$"{notifyType}:{message.Text}{System.Environment.NewLine}-{System.Environment.NewLine}";
}
}
}

View File

@@ -97,6 +97,8 @@ namespace Orchard.UI.Resources
public static void WriteResource(TextWriter writer, ResourceDefinition resource, string url, string condition, Dictionary<string, string> attributes)
{
if (string.IsNullOrEmpty(url)) return;
if (!string.IsNullOrEmpty(condition))
{
if (condition == NotIE)