diff --git a/.tools/phpstan/baseline/missingType.iterableValue.php b/.tools/phpstan/baseline/missingType.iterableValue.php
index 5d2cc9b819..f87e478d7a 100644
--- a/.tools/phpstan/baseline/missingType.iterableValue.php
+++ b/.tools/phpstan/baseline/missingType.iterableValue.php
@@ -33,16 +33,6 @@
'count' => 1,
'path' => __DIR__ . '/../../../addons/debug/lib/extensions/extension_debug.php',
];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Backend\\Controller::pageAddProperties() has parameter $properties with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Backend/Controller.php',
-];
-$ignoreErrors[] = [
- 'rawMessage' => 'Method Redaxo\\Core\\Backend\\Controller::pageCreate() has parameter $page with no value type specified in iterable type array.',
- 'count' => 1,
- 'path' => __DIR__ . '/../../../src/Backend/Controller.php',
-];
$ignoreErrors[] = [
'rawMessage' => 'Method Redaxo\\Core\\Content\\ArticleHandler::addArticle() has parameter $data with no value type specified in iterable type array.',
'count' => 1,
diff --git a/.tools/psalm/baseline.xml b/.tools/psalm/baseline.xml
index dfc62ffdb0..2e95b81af8 100644
--- a/.tools/psalm/baseline.xml
+++ b/.tools/psalm/baseline.xml
@@ -1491,41 +1491,9 @@
-
- getProperty('page')]]>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/debug/package.yml b/addons/debug/package.yml
deleted file mode 100644
index 5b93349678..0000000000
--- a/addons/debug/package.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-page:
- title: translate:debug
- perm: admin
- live_mode: false
- block: system
- pjax: false
- icon: rex-icon rex-icon-heartbeat
- linkAttr: { target: _blank }
diff --git a/addons/debug/src/DebugAddon.php b/addons/debug/src/DebugAddon.php
index 3d9952ad5d..df363a03ca 100644
--- a/addons/debug/src/DebugAddon.php
+++ b/addons/debug/src/DebugAddon.php
@@ -5,6 +5,9 @@
use Override;
use Redaxo\Core\Addon\Addon;
use Redaxo\Core\Addon\LoadOrder;
+use Redaxo\Core\Backend\MainPage;
+use Redaxo\Core\Core;
+use Redaxo\Core\Translation\I18n;
final class DebugAddon extends Addon
{
@@ -16,6 +19,19 @@ public function boot(): void
$this->includeFile('boot.php');
}
+ #[Override]
+ public function getPages(): iterable
+ {
+ if (Core::isLiveMode()) {
+ return;
+ }
+
+ yield new MainPage('system', $this->name, I18n::msg('debug'))
+ ->setRequiredPermissions('admin')
+ ->setIcon('rex-icon rex-icon-heartbeat')
+ ->setLinkAttr('target', '_blank');
+ }
+
#[Override]
public function install(): void
{
diff --git a/schemas/package.json b/schemas/package.json
index 558380f7f7..8f883e909d 100644
--- a/schemas/package.json
+++ b/schemas/package.json
@@ -4,204 +4,10 @@
"title": "JSON schema for REDAXO package.yml",
"type": "object",
"properties": {
- "page": {
- "description": "Main page",
- "$ref": "#/definitions/main-page"
- },
- "pages": {
- "description": "Additional pages",
- "type": "object",
- "patternProperties": {
- "^[^/]*$": {
- "$ref": "#/definitions/main-page"
- }
- },
- "additionalProperties": {
- "$ref": "#/definitions/page"
- }
- },
"default_config": {
"description": "Default values for Redaxo\\Core\\Config",
"type": "object"
}
},
- "additionalProperties": true,
- "definitions": {
- "page-base": {
- "type": "object",
- "required": ["title"],
- "properties": {
- "title": {
- "description": "Page title",
- "type": "string"
- },
- "hidden": {
- "description": "Whether the page is hidden",
- "type": "boolean",
- "default": true
- },
- "hasLayout": {
- "description": "Whether the page has layout",
- "type": "boolean",
- "default": false
- },
- "hasNavigation": {
- "description": "Whether the page has the navigation",
- "type": "boolean",
- "default": false
- },
- "popup": {
- "description": "Whether the page is a popup",
- "oneOf": [
- {
- "type": "boolean",
- "default": true
- },
- {
- "description": "onclick attribute",
- "type": "string"
- }
- ]
- },
- "pjax": {
- "description": "Whether the page uses pjax",
- "type": "boolean",
- "default": true
- },
- "perm": {
- "description": "Page permission",
- "type": "string",
- "default": "admin"
- },
- "live_mode": {
- "description": "Live mode",
- "type": "boolean",
- "default": true
- },
- "icon": {
- "description": "Icon class name(s)",
- "type": "string"
- },
- "href": {
- "description": "href attribute",
- "oneOf": [
- {
- "type": "string"
- },
- {
- "description": "href params",
- "type": "object"
- }
- ]
- },
- "itemAttr": {
- "description": "Attributes for the list item",
- "type": "object"
- },
- "itemClass": {
- "description": "class attributes for the list item",
- "type": "string"
- },
- "linkAttr": {
- "description": "Attributes for the link",
- "type": "object"
- },
- "linkClass": {
- "description": "class attributes for the link",
- "type": "string"
- },
- "path": {
- "description": "Path to the main page file",
- "type": "string",
- "pattern": "\\.php$"
- },
- "subPath": {
- "description": "Path to the subpage file",
- "type": "string",
- "pattern": "\\.(php|md)$"
- },
- "subpages": {
- "description": "Subpages",
- "patternProperties": {
- "^[^/]+$": {
- "$ref": "#/definitions/page"
- }
- },
- "additionalProperties": false
- }
- }
- },
- "page": {
- "allOf": [
- {
- "$ref": "#/definitions/page-base"
- }
- ],
- "properties": {
- "title": true,
- "hidden": true,
- "hasLayout": true,
- "hasNavigation": true,
- "popup": true,
- "pjax": true,
- "perm": true,
- "icon": true,
- "href": true,
- "itemAttr": true,
- "itemClass": true,
- "linkAttr": true,
- "linkClass": true,
- "path": true,
- "subPath": true,
- "subpages": true
- },
- "additionalProperties": false
- },
- "main-page": {
- "allOf": [
- {
- "$ref": "#/definitions/page-base"
- }
- ],
- "properties": {
- "title": true,
- "hidden": true,
- "hasLayout": true,
- "hasNavigation": true,
- "popup": true,
- "pjax": true,
- "perm": true,
- "icon": true,
- "href": true,
- "itemAttr": true,
- "itemClass": true,
- "linkAttr": true,
- "linkClass": true,
- "path": true,
- "subPath": true,
- "subpages": true,
- "main": {
- "description": "Whether it is a main page",
- "type": "boolean",
- "default": true
- },
- "block": {
- "description": "Block name",
- "anyOf": [
- {
- "enum": ["system", "addons"]
- },
- {
- "type": "string"
- }
- ]
- },
- "prio": {
- "description": "Page prio",
- "type": "integer"
- }
- },
- "additionalProperties": false
- }
- }
+ "additionalProperties": true
}
diff --git a/src/Addon/Addon.php b/src/Addon/Addon.php
index 8ec3f19c92..bb356d8ec0 100644
--- a/src/Addon/Addon.php
+++ b/src/Addon/Addon.php
@@ -5,6 +5,7 @@
use Composer\InstalledVersions;
use OutOfBoundsException;
use Redaxo\Core\Addon\ExtensionPoint\AddonCacheDeleted;
+use Redaxo\Core\Backend\Page;
use Redaxo\Core\Config;
use Redaxo\Core\Core;
use Redaxo\Core\Exception\RuntimeException;
@@ -445,6 +446,22 @@ final public function enlist(): void
/** Boot hook — runs on every request after all addons are enlisted. Override to register listeners etc. */
public function boot(): void {}
+ /**
+ * Backend page hook — override to register the addon's backend pages.
+ *
+ * Runs only in the backend, after the core pages and all earlier-loading addons have been registered, so
+ * Controller::getPageObject() can be used to attach subpages to existing core or addon pages. Top-level pages
+ * are typically MainPage instances (shown in the navigation); a plain Page can be used for hidden entry points
+ * that are reachable by key/URL but should not appear in the navigation. Any page without an explicit path falls
+ * back to the convention `pages/.php` (or `pages/index.php` for the main page whose key equals the addon name).
+ *
+ * @return iterable
+ */
+ public function getPages(): iterable
+ {
+ return [];
+ }
+
/**
* Install hook — runs on install/reinstall. Override for schema/data setup. Must be idempotent.
*
diff --git a/src/Backend/Controller.php b/src/Backend/Controller.php
index 9e21ffd78e..09c2241d4f 100644
--- a/src/Backend/Controller.php
+++ b/src/Backend/Controller.php
@@ -18,12 +18,9 @@
use Redaxo\Core\Util\Type;
use Redaxo\Core\View\Fragment;
-use function call_user_func;
use function count;
use function ini_get;
use function is_array;
-use function is_callable;
-use function is_string;
use function sprintf;
use const DIRECTORY_SEPARATOR;
@@ -358,71 +355,30 @@ public static function appendLoggedInPages(): void
public static function appendPackagePages(): void
{
- $insertPages = [];
$addons = Core::isSafeMode() ? Addon::getSetupAddons() : Addon::getAvailableAddons();
foreach ($addons as $addon) {
- $mainPage = self::pageCreate($addon->getProperty('page'), $addon, true);
-
- if (is_array($pages = $addon->getProperty('pages'))) {
- foreach ($pages as $key => $page) {
- if (str_contains($key, '/')) {
- $insertPages[$key] = [$addon, $page];
- } else {
- self::pageCreate($page, $addon, false, $mainPage, $key, true);
- }
- }
- }
- }
- foreach ($insertPages as $key => $packagePage) {
- [$package, $page] = $packagePage;
- $key = explode('/', $key);
- if (!isset(self::$pages[$key[0]])) {
- continue;
- }
- $parentPage = self::$pages[$key[0]];
- for ($i = 1, $count = count($key) - 1; $i < $count && $parentPage; ++$i) {
- $parentPage = $parentPage->getSubpage($key[$i]);
- }
- if ($parentPage) {
- self::pageCreate($page, $package, false, $parentPage, $key[$i], strtr($parentPage->getFullKey(), '/', '.') . '.' . $key[$i] . '.');
+ foreach ($addon->getPages() as $page) {
+ self::registerAddonPage($page, $addon);
}
}
}
- private static function pageCreate(Page|array|null $page, Addon $package, bool $createMainPage, ?Page $parentPage = null, ?string $pageKey = null, bool|string $prefix = false): ?Page
+ /**
+ * Registers a top-level addon page and applies the path convention to it and its subpages.
+ *
+ * A page without an explicit path falls back to `pages/.php`, or `pages/index.php` for the main page
+ * whose key equals the addon name.
+ */
+ private static function registerAddonPage(Page $page, Addon $addon): void
{
- if (is_array($page) && isset($page['title']) && (false !== ($page['live_mode'] ?? null) || !Core::isLiveMode())) {
- $pageArray = $page;
- $pageKey = $pageKey ?: $package->name;
- if ($createMainPage || isset($pageArray['main']) && $pageArray['main']) {
- $page = new MainPage('addons', $pageKey, $pageArray['title']);
- } else {
- $page = new Page($pageKey, $pageArray['title']);
- }
- self::pageAddProperties($page, $pageArray, $package);
- }
+ $prefix = $page->getKey() === $addon->name ? '' : $page->getKey() . '.';
- if ($page instanceof Page) {
- if (!is_string($prefix)) {
- $prefix = $prefix ? $page->getKey() . '.' : '';
- }
- if ($page instanceof MainPage) {
- if (!$page->hasPath()) {
- $page->setPath($package->getPath('pages/' . ($prefix ?: 'index.') . 'php'));
- }
- self::$pages[$page->getKey()] = $page;
- } else {
- if (!$page->hasSubPath()) {
- $page->setSubPath($package->getPath('pages/' . ($prefix ?: 'index.') . 'php'));
- }
- if ($parentPage) {
- $parentPage->addSubpage($page);
- }
- }
- self::pageSetSubPaths($page, $package, $prefix);
- return $page;
+ if (!$page->hasPath()) {
+ $page->setPath($addon->getPath('pages/' . ($prefix ?: 'index.') . 'php'));
}
- return null;
+ self::$pages[$page->getKey()] = $page;
+
+ self::pageSetSubPaths($page, $addon, $prefix);
}
private static function pageSetSubPaths(Page $page, Addon $package, string $prefix = ''): void
@@ -435,56 +391,6 @@ private static function pageSetSubPaths(Page $page, Addon $package, string $pref
}
}
- private static function pageAddProperties(Page $page, array $properties, Addon $package): void
- {
- foreach ($properties as $key => $value) {
- switch (strtolower($key)) {
- case 'subpages':
- if (is_array($value)) {
- foreach ($value as $pageKey => $subProperties) {
- if (isset($subProperties['title']) && (false !== ($subProperties['live_mode'] ?? null) || !Core::isLiveMode())) {
- $subpage = new Page($pageKey, $subProperties['title']);
- $page->addSubpage($subpage);
- self::pageAddProperties($subpage, $subProperties, $package);
- }
- }
- }
- break;
-
- case 'itemattr':
- case 'linkattr':
- $setter = [$page, 'set' . ucfirst($key)];
- foreach ($value as $k => $v) {
- call_user_func($setter, $k, $v);
- }
- break;
-
- case 'perm':
- $page->setRequiredPermissions($value);
- break;
-
- case 'path':
- case 'subpath':
- if (is_file($path = $package->getPath($value))) {
- $value = $path;
- }
- // no break
- default:
- $adder = [$page, 'add' . ucfirst($key)];
- if (is_callable($adder)) {
- foreach ((array) $value as $v) {
- call_user_func($adder, $v);
- }
- break;
- }
- $setter = [$page, 'set' . ucfirst($key)];
- if (is_callable($setter)) {
- call_user_func($setter, $value);
- }
- }
- }
- }
-
public static function checkPagePermissions(User $user): void
{
$check = static function (Page $page) use (&$check, $user): bool {