Compare commits

...

8 Commits

Author SHA1 Message Date
Benedek Farkas
d6b177c563 Adding notification when deleting a menu items 2025-10-03 21:42:41 +02:00
Benedek Farkas
14d19b9ccc MainMenuService: Fixing that unpublished menu items could not be deleted 2025-10-03 21:42:21 +02:00
Benedek Farkas
2b410d808e Merge branch '1.10.x' into issue/8830 2025-10-03 21:07:11 +02:00
Benedek Farkas
971a97874c Preventing Indexing AdminController and Commands from operating with unsafe index names (#8845)
Some checks failed
Compile / Compile .NET solution (push) Has been cancelled
Compile / Compile Client-side Assets (push) Has been cancelled
SpecFlow Tests / Define Strategy Matrix (push) Has been cancelled
SpecFlow Tests / SpecFlow Tests (push) Has been cancelled
2025-10-02 19:18:19 +02:00
Benedek Farkas
8851f622a3 Adding release package workflow (#8846)
Some checks failed
Compile / Compile .NET solution (push) Has been cancelled
Compile / Compile Client-side Assets (push) Has been cancelled
SpecFlow Tests / Define Strategy Matrix (push) Has been cancelled
SpecFlow Tests / SpecFlow Tests (push) Has been cancelled
(cherry picked from commit 4268b28d95)
2025-09-29 21:10:53 +02:00
Benedek Farkas
e22be0b1ce Navigation: Fixing that fields attached to a Menu Item should also be updated when creating the item 2025-09-26 19:16:54 +02:00
Benedek Farkas
8a6e89d1af Navigation: Adding support to the AdminController for the Delete button rendered by the Contents feature 2025-09-26 19:15:13 +02:00
Benedek Farkas
5e44b013cd Navigation: Fixing that saving a menu item should not force creating a draft version 2025-09-24 23:02:11 +02:00
10 changed files with 226 additions and 68 deletions

View File

@@ -0,0 +1,27 @@
name: Build Crowdin Translation Packages
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
jobs:
build-crowdin-translation-packages:
runs-on: ubuntu-24.04
steps:
- name: Orchard CMS
uses: andrii-bodnar/crowdin-request-action@ee7a2af9564d8934b5b4a8427185aaaffee0165e # v0.3.0
with:
route: POST /projects/{projectId}/translations/builds
projectId: 46524
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
- name: Orchard CMS Gallery
uses: andrii-bodnar/crowdin-request-action@ee7a2af9564d8934b5b4a8427185aaaffee0165e # v0.3.0
with:
route: POST /projects/{projectId}/translations/builds
projectId: 63766
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}

View File

@@ -17,13 +17,13 @@ jobs:
runs-on: windows-2025
steps:
- name: Clone Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore NuGet Packages
run: nuget restore src/Orchard.sln
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
- name: Compile
run: msbuild Orchard.proj /m /v:minimal /t:Compile /p:TreatWarningsAsErrors=true -WarnAsError /p:MvcBuildViews=true
@@ -69,10 +69,10 @@ jobs:
runs-on: windows-2025
steps:
- name: Clone Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup NodeJS
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 7

39
.github/workflows/release-package.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Release Package
# Builds the release package and uploads it as an artifact.
on:
workflow_dispatch:
jobs:
release-package:
name: Release Package
runs-on: Windows-2025
defaults:
run:
shell: cmd
steps:
- name: Clone Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore NuGet Packages
run: nuget restore src/Orchard.sln
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
- name: Build Precompiled Application
run: msbuild Orchard.proj /m /v:minimal /t:Precompiled
- name: Generate Release Package Name
id: package-name
shell: pwsh
run: |
$packageName = "Orchard-$('${{ github.ref_name }}'.Replace('/', '_'))-${{ github.sha }}"
"package-name=$packageName" >> $Env:GITHUB_OUTPUT
- name: Upload Release Package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.package-name.outputs.package-name }}
path: .\build\Precompiled
if-no-files-found: error

59
.github/workflows/specflow.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: SpecFlow Tests
# Compiles the solution and runs unit tests, as well the SpecFlow tests on the main development branches.
on:
workflow_dispatch:
push:
branches:
- 1.10.x
- dev
schedule:
- cron: '0 0 * * 1' # Every Monday midnight.
jobs:
define-matrix:
name: Define Strategy Matrix
runs-on: ubuntu-24.04
outputs:
branches: ${{ steps.branches.outputs.branches }}
steps:
- name: Define Branches
id: branches
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo 'branches=["1.10.x", "dev"]' >> "$GITHUB_OUTPUT"
else
echo 'branches=["${{ github.ref_name }}"]' >> "$GITHUB_OUTPUT"
fi
compile:
name: SpecFlow Tests
needs: define-matrix
runs-on: windows-2025
strategy:
matrix:
branch: ${{ fromJSON(needs.define-matrix.outputs.branches) }}
fail-fast: false
defaults:
run:
shell: cmd
steps:
- name: Clone Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ matrix.branch }}
- name: Restore NuGet Packages
run: nuget restore src/Orchard.sln
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0
- name: Compile
run: msbuild Orchard.proj /m /v:minimal /t:Compile /p:MvcBuildViews=true /p:TreatWarningsAsErrors=true -WarnAsError
- name: Test
run: msbuild Orchard.proj /m /v:minimal /t:Test
- name: Spec
run: msbuild Orchard.proj /m /v:minimal /t:Spec

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Handlers;
@@ -14,6 +13,7 @@ using Orchard.Data;
using Orchard.Exceptions;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Mvc;
using Orchard.Mvc.Extensions;
using Orchard.Mvc.Html;
using Orchard.Security;
@@ -131,11 +131,15 @@ namespace Orchard.Core.Navigation.Controllers {
return RedirectToAction("Index", new { menuId });
}
[HttpPost, ActionName("Edit")]
[FormValueRequired("submit.Delete")]
public ActionResult EditDeletePOST(int id) => Delete(id);
[HttpPost]
public ActionResult Delete(int id) {
MenuPart menuPart = _menuService.Get(id);
int? menuId = null;
if (!_authorizer.Authorize(
Permissions.ManageMenus,
menuPart == null ? null : _menuService.GetMenu(menuPart.Menu.Id),
@@ -152,7 +156,7 @@ namespace Orchard.Core.Navigation.Controllers {
.ToList();
foreach (var menuItem in menuItems.Concat(new[] { menuPart })) {
// if the menu item is a concrete content item, don't delete it, just unreference the menu
// if the menu item is a concrete content item, don't delete it, just remove the menu reference
if (!menuPart.ContentItem.TypeDefinition.Settings.ContainsKey("Stereotype")
|| menuPart.ContentItem.TypeDefinition.Settings["Stereotype"] != "MenuItem") {
menuPart.Menu = null;
@@ -162,6 +166,11 @@ namespace Orchard.Core.Navigation.Controllers {
}
}
_notifier.Information(T.Plural(
"The menu item '{1}' has been deleted.",
"The menu item '{1}' and its children have been deleted.",
menuItems.Count() + 1,
menuPart.MenuText));
}
return RedirectToAction("Index", new { menuId });
@@ -172,7 +181,8 @@ namespace Orchard.Core.Navigation.Controllers {
return new HttpUnauthorizedResult();
// create a new temporary menu item
var menuPart = _contentManager.New<MenuPart>(id);
var contentItem = _contentManager.New(id);
var menuPart = contentItem.As<MenuPart>();
if (menuPart == null)
return HttpNotFound();
@@ -187,7 +197,7 @@ namespace Orchard.Core.Navigation.Controllers {
// filter the content items for this specific menu
menuPart.MenuPosition = Position.GetNext(_navigationManager.BuildMenu(menu));
menuPart.Menu = menu;
var model = _contentManager.BuildEditor(menuPart);
var model = _contentManager.BuildEditor(contentItem);
return View(model);
}
@@ -206,23 +216,32 @@ namespace Orchard.Core.Navigation.Controllers {
public ActionResult CreateMenuItemPost(string id, int menuId, string returnUrl) {
if (!_authorizer.Authorize(Permissions.ManageMenus, _menuService.GetMenu(menuId), T("Couldn't manage the menu")))
return new HttpUnauthorizedResult();
var menuPart = _contentManager.New<MenuPart>(id);
var contentItem = _contentManager.New(id);
var menuPart = contentItem.As<MenuPart>();
if (menuPart == null)
return HttpNotFound();
// load the menu
var menu = _contentManager.Get(menuId);
if (menu == null)
return HttpNotFound();
_contentManager.Create(contentItem);
menuPart.Menu = menu;
var model = _contentManager.UpdateEditor(menuPart, this);
menuPart.MenuPosition = Position.GetNext(_navigationManager.BuildMenu(menu));
_contentManager.Create(menuPart);
var model = _contentManager.UpdateEditor(contentItem, this);
if (!ModelState.IsValid) {
_transactionManager.Cancel();
return View(model);
}
_notifier.Information(T("Your {0} has been added.", menuPart.TypeDefinition.DisplayName));
_notifier.Information(T("Your {0} has been added.", contentItem.TypeDefinition.DisplayName));
return this.RedirectLocal(returnUrl, () => RedirectToAction("Index"));
}
@@ -308,7 +327,8 @@ namespace Orchard.Core.Navigation.Controllers {
}
private ActionResult EditPOST(int id, string returnUrl, Action<ContentItem> conditionallyPublish) {
var menuPart = _contentManager.GetDraftRequired<MenuPart>(id);
var contentItem = _contentManager.GetLatest(id);
var menuPart = contentItem.As<MenuPart>();
if (menuPart == null)
return HttpNotFound();
@@ -316,8 +336,6 @@ namespace Orchard.Core.Navigation.Controllers {
if (!_authorizer.Authorize(Permissions.ManageMenus, menuPart.Menu, T("Couldn't manage the menu")))
return new HttpUnauthorizedResult();
var contentItem = menuPart.ContentItem;
string previousRoute = null;
if (contentItem.Has<IAliasAspect>()
&& !string.IsNullOrWhiteSpace(returnUrl)

View File

@@ -25,7 +25,7 @@ namespace Orchard.Core.Navigation.Services {
}
public IContent GetMenu(string menuName) {
if(string.IsNullOrWhiteSpace(menuName)) {
if (string.IsNullOrWhiteSpace(menuName)) {
return null;
}
@@ -37,19 +37,19 @@ namespace Orchard.Core.Navigation.Services {
}
public IContent GetMenu(int menuId) {
return _contentManager.Get(menuId, VersionOptions.Published);
return _contentManager.Get(menuId, VersionOptions.Published);
}
public MenuPart Get(int menuPartId) {
return _contentManager.Get<MenuPart>(menuPartId);
return _contentManager.Get<MenuPart>(menuPartId, VersionOptions.Latest);
}
public IContent Create(string name) {
if(string.IsNullOrWhiteSpace(name)) {
if (string.IsNullOrWhiteSpace(name)) {
throw new ArgumentNullException(name);
}
var menu = _contentManager.Create("Menu");
menu.As<TitlePart>().Title = name;

View File

@@ -4,7 +4,7 @@ using Orchard.Commands;
using Orchard.ContentManagement;
using Orchard.Indexing.Services;
using Orchard.Tasks.Indexing;
using Orchard.Utility.Extensions;
using static Orchard.Indexing.Helpers.IndexingHelpers;
namespace Orchard.Indexing.Commands {
public class IndexingCommands : DefaultOrchardCommandHandler {
@@ -38,27 +38,22 @@ namespace Orchard.Indexing.Commands {
return;
}
if (string.IsNullOrWhiteSpace(index)) {
if (!IsValidIndexName(index)) {
Context.Output.WriteLine(T("Invalid index name."));
return;
}
if (index.ToSafeName() != index) {
Context.Output.WriteLine(T("Invalid index name."));
var indexProvider = _indexManager.GetSearchIndexProvider();
if (indexProvider == null) {
Context.Output.WriteLine(T("No indexing service was found. Please enable a module like Lucene."));
}
else {
var indexProvider = _indexManager.GetSearchIndexProvider();
if(indexProvider == null) {
Context.Output.WriteLine(T("No indexing service was found. Please enable a module like Lucene."));
if (indexProvider.Exists(index)) {
Context.Output.WriteLine(T("The specified index already exists."));
}
else {
if (indexProvider.Exists(index)) {
Context.Output.WriteLine(T("The specified index already exists."));
}
else {
_indexManager.GetSearchIndexProvider().CreateIndex(index);
Context.Output.WriteLine(T("New index has been created successfully."));
}
_indexManager.GetSearchIndexProvider().CreateIndex(index);
Context.Output.WriteLine(T("New index has been created successfully."));
}
}
}
@@ -66,7 +61,7 @@ namespace Orchard.Indexing.Commands {
[CommandName("index update")]
[CommandHelp("index update <index>\r\n\t" + "Updates the specified index")]
public void Update(string index) {
if (string.IsNullOrWhiteSpace(index)) {
if (!IsValidIndexName(index)) {
Context.Output.WriteLine(T("Invalid index name."));
return;
}
@@ -78,7 +73,7 @@ namespace Orchard.Indexing.Commands {
[CommandName("index rebuild")]
[CommandHelp("index rebuild <index> \r\n\t" + "Rebuilds the specified index")]
public void Rebuild(string index) {
if (string.IsNullOrWhiteSpace(index)) {
if (!IsValidIndexName(index)) {
Context.Output.WriteLine(T("Invalid index name."));
return;
}
@@ -91,24 +86,24 @@ namespace Orchard.Indexing.Commands {
[CommandHelp("index query <index> /Query:<query>\r\n\t" + "Searches the specified <query> terms in the specified index")]
[OrchardSwitches("Query")]
public void Search(string index) {
if (string.IsNullOrWhiteSpace(index)) {
if (!IsValidIndexName(index)) {
Context.Output.WriteLine(T("Invalid index name."));
return;
}
if ( !_indexManager.HasIndexProvider() ) {
if (!_indexManager.HasIndexProvider()) {
Context.Output.WriteLine(T("No index available"));
return;
}
var searchBuilder = _indexManager.GetSearchIndexProvider().CreateSearchBuilder(index);
var results = searchBuilder.Parse( new [] {"body", "title"}, Query).Search();
var results = searchBuilder.Parse(new[] { "body", "title" }, Query).Search();
Context.Output.WriteLine("{0} result{1}\r\n-----------------\r\n", results.Count(), results.Count() > 0 ? "s" : "");
Context.Output.WriteLine("┌──────────────────────────────────────────────────────────────┬────────┐");
Context.Output.WriteLine("│ {0} │ {1,6} │", "Title" + new string(' ', 60 - "Title".Length), "Score");
Context.Output.WriteLine("├──────────────────────────────────────────────────────────────┼────────┤");
foreach ( var searchHit in results ) {
foreach (var searchHit in results) {
var contentItem = _contentManager.Get(searchHit.ContentItemId);
var metadata = _contentManager.GetItemMetadata(contentItem);
var title = String.IsNullOrWhiteSpace(metadata.DisplayText) ? "- no title -" : metadata.DisplayText;
@@ -126,12 +121,12 @@ namespace Orchard.Indexing.Commands {
[CommandHelp("index stats <index>\r\n\t" + "Displays some statistics about the search index")]
[OrchardSwitches("IndexName")]
public void Stats(string index) {
if (string.IsNullOrWhiteSpace(index)) {
if (!IsValidIndexName(index)) {
Context.Output.WriteLine(T("Invalid index name."));
return;
}
if ( !_indexManager.HasIndexProvider() ) {
if (!_indexManager.HasIndexProvider()) {
Context.Output.WriteLine(T("No index available"));
return;
}
@@ -140,11 +135,10 @@ namespace Orchard.Indexing.Commands {
}
[CommandName("index refresh")]
[CommandHelp("index refresh /ContentItem:<content item id> \r\n\t" + "Refreshes the index for the specifed <content item id>")]
[CommandHelp("index refresh /ContentItem:<content item id> \r\n\t" + "Refreshes the index for the specified <content item id>")]
[OrchardSwitches("ContentItem")]
public void Refresh() {
int contentItemId;
if ( !int.TryParse(ContentItem, out contentItemId) ) {
if (!int.TryParse(ContentItem, out int contentItemId)) {
Context.Output.WriteLine(T("Invalid content item id. Not an integer."));
return;
}
@@ -152,19 +146,18 @@ namespace Orchard.Indexing.Commands {
var contentItem = _contentManager.Get(contentItemId);
_indexingTaskManager.CreateUpdateIndexTask(contentItem);
Context.Output.WriteLine(T("Content Item marked for reindexing"));
Context.Output.WriteLine(T("Content Item marked for re-indexing"));
}
[CommandName("index delete")]
[CommandHelp("index delete /ContentItem:<content item id>\r\n\t" + "Deletes the specifed <content item id> from the index")]
[CommandHelp("index delete /ContentItem:<content item id>\r\n\t" + "Deletes the specified <content item id> from the index")]
[OrchardSwitches("ContentItem")]
public void Delete() {
int contentItemId;
if(!int.TryParse(ContentItem, out contentItemId)) {
if (!int.TryParse(ContentItem, out int contentItemId)) {
Context.Output.WriteLine(T("Invalid content item id. Not an integer."));
return;
}
var contentItem = _contentManager.Get(contentItemId);
_indexingTaskManager.CreateDeleteIndexTask(contentItem);

View File

@@ -2,12 +2,12 @@
using System.Linq;
using System.Web.Mvc;
using Orchard.Indexing.Services;
using Orchard.Indexing.ViewModels;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Security;
using Orchard.Indexing.ViewModels;
using Orchard.UI.Notify;
using Orchard.Utility.Extensions;
using static Orchard.Indexing.Helpers.IndexingHelpers;
namespace Orchard.Indexing.Controllers {
public class AdminController : Controller {
@@ -15,7 +15,7 @@ namespace Orchard.Indexing.Controllers {
private readonly IIndexManager _indexManager;
public AdminController(
IIndexingService indexingService,
IIndexingService indexingService,
IOrchardServices services,
IIndexManager indexManager) {
_indexingService = indexingService;
@@ -34,23 +34,21 @@ namespace Orchard.Indexing.Controllers {
IndexEntries = Enumerable.Empty<IndexEntry>(),
IndexProvider = _indexManager.GetSearchIndexProvider()
};
if (_indexManager.HasIndexProvider()) {
viewModel.IndexEntries = _indexManager.GetSearchIndexProvider().List().Select(x => {
try {
return _indexingService.GetIndexEntry(x);
}
catch(Exception e) {
catch (Exception e) {
Logger.Error(e, "Index couldn't be read: " + x);
return new IndexEntry {
return new IndexEntry {
IndexName = x,
IndexingStatus = IndexingStatus.Unavailable
};
}
});
}
// Services.Notifier.Information(T("The index might be corrupted. If you can't recover click on Rebuild."));
return View(viewModel);
}
@@ -59,7 +57,7 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
return View("Create", String.Empty);
return View("Create");
}
[HttpPost, ActionName("Create")]
@@ -68,7 +66,7 @@ namespace Orchard.Indexing.Controllers {
return new HttpUnauthorizedResult();
var provider = _indexManager.GetSearchIndexProvider();
if (String.IsNullOrWhiteSpace(id) || id.ToSafeName() != id) {
if (!IsValidIndexName(id)) {
Services.Notifier.Error(T("Invalid index name."));
return View("Create", id);
}
@@ -82,9 +80,9 @@ namespace Orchard.Indexing.Controllers {
provider.CreateIndex(id);
Services.Notifier.Information(T("Index named {0} created successfully", id));
}
catch(Exception e) {
catch (Exception e) {
Services.Notifier.Error(T("An error occurred while creating the index: {0}", id));
Logger.Error("An error occurred while creatign the index " + id, e);
Logger.Error("An error occurred while creating the index " + id, e);
return View("Create", id);
}
@@ -96,7 +94,12 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.UpdateIndex(id);
if (IsValidIndexName(id)) {
_indexingService.UpdateIndex(id);
}
else {
Services.Notifier.Error(T("Invalid index name."));
}
return RedirectToAction("Index");
}
@@ -106,7 +109,12 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.RebuildIndex(id);
if (IsValidIndexName(id)) {
_indexingService.RebuildIndex(id);
}
else {
Services.Notifier.Error(T("Invalid index name."));
}
return RedirectToAction("Index");
}
@@ -116,7 +124,12 @@ namespace Orchard.Indexing.Controllers {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage the search index.")))
return new HttpUnauthorizedResult();
_indexingService.DeleteIndex(id);
if (IsValidIndexName(id)) {
_indexingService.DeleteIndex(id);
}
else {
Services.Notifier.Error(T("Invalid index name."));
}
return RedirectToAction("Index");
}

View File

@@ -0,0 +1,8 @@
using Orchard.Utility.Extensions;
namespace Orchard.Indexing.Helpers {
public static class IndexingHelpers {
public static bool IsValidIndexName(string name) =>
!string.IsNullOrWhiteSpace(name) && name.ToSafeName() == name;
}
}

View File

@@ -95,6 +95,7 @@
<Compile Include="AdminMenu.cs" />
<Compile Include="Commands\IndexingCommands.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Helpers\IndexingHelpers.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Handlers\CreateIndexingTaskHandler.cs" />
<Compile Include="Handlers\InfosetFieldIndexingHandler.cs" />
@@ -180,4 +181,4 @@
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
</Project>