Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
159 changes: 159 additions & 0 deletions module/VuFind/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,165 @@
// This section contains all VuFind-specific settings (i.e. configurations
// unrelated to specific Laminas components).
'vufind' => [
// This section contains all action specific configuration that gets applied to actions before they're executed.
//
// The configuration is an array of associative arrays of configuration entries. Each entry is identified by its
// key so that any specific entry can be overridden in other modules.
//
// Note: Each module should use a module specific prefix in their own entries to avoid any unintentional clashes
// between modules. A good practice is to prefix each config entry key with lowercase module name followed by
// an underscore and the actual identifier (e.g. 'vufindadmin_admin').
//
// Valid keys for each configuration entry:
// - actionIds An array of action identifiers or prefixes the configuration applies to
// (format: category/action in lowercase)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the documentation also include an example of the prefix format?

// - accessPermission Set access permission (string|false|null, see AccessPermissionInterface)
// - accessDeniedBehavior Set behavior when access is denied (string|null, see AccessPermissionInterface)
// - backendId Set search backend identifier (string)
// - defaultTab Set default tab (string|null)
// - fallbackDefaultTab Set fallback default tab (string; empty string to use Site/defaultRecordTab from
// config)
// - poweredBy Set "Powered by" displayed in page footer
'action_config' => [
// EDS:
'vufind_eds_record' => [
'actionIds' => [
'edsrecord',
[
'type' => 'prefix',
'prefix' => 'edsrecord/',
],
],
'accessPermission' => 'access.EDSModule',
'backendId' => 'EDS',
'fallbackDefaultTab' => 'Description',
],

// EIT:
'vufind_eit_record' => [
'actionIds' => [
'eitrecord',
[
'type' => 'prefix',
'prefix' => 'eitrecord/',
],
],
'accessPermission' => 'access.EITModule',
'backendId' => 'EIT',
'fallbackDefaultTab' => 'Description',
],

// EPF:
'vufind_epf_record' => [
'actionIds' => [
'epfrecord',
[
'type' => 'prefix',
'prefix' => 'epfrecord/',
],
],
'accessPermission' => 'access.EPFModule',
'backendId' => 'EPF',
],

// Record, Collection (Default backend):
'vufind_record' => [
'actionIds' => [
'collection',
[
'type' => 'prefix',
'prefix' => 'collection/',
],
'missingrecord',
'missingrecord/home',
'record',
[
'type' => 'prefix',
'prefix' => 'record/',
],
],
'backendId' => DEFAULT_SEARCH_BACKEND,
'fallbackDefaultTab' => '',
],

// Primo:
'vufind_primo_record' => [
'actionIds' => [
'primorecord',
[
'type' => 'prefix',
'prefix' => 'primorecord/',
],
],
'accessPermission' => 'access.PrimoModule',
'backendId' => 'Primo',
'fallbackDefaultTab' => 'Description',
],

// ProquestFSG:
'vufind_proquestfsg_record' => [
'actionIds' => [
'proquestfsgrecord',
[
'type' => 'prefix',
'prefix' => 'proquestfsgrecord/',
],
],
'backendId' => 'ProQuestFSG',
'checkEnabled' => true,
],

// Search2Record, Search2Collection:
'vufind_search2_record' => [
'actionIds' => [
'search2collection',
[
'type' => 'prefix',
'prefix' => 'search2collection/',
],
'search2record',
[
'type' => 'prefix',
'prefix' => 'search2record/',
],
],
'backendId' => 'Search2',
'fallbackDefaultTab' => 'Description',
],

// Summon:
'vufind_summon_record' => [
'actionIds' => [
'summonrecord',
[
'type' => 'prefix',
'prefix' => 'summonrecord/',
],
],
'backendId' => 'Summon',
'fallbackDefaultTab' => 'Description',
'poweredBy' => 'Powered by Summon™ from Serials Solutions, a division of ProQuest.',
],

// WorldCat2 and legacy WorldCat actions:
'vufind_worldcat2_record' => [
'actionIds' => [
// Legacy WorldCat actions:
'worldcatrecord',
[
'type' => 'prefix',
'prefix' => 'worldcatrecord/',
],
// Current WorldCat2 actions:
'worldcat2record',
[
'type' => 'prefix',
'prefix' => 'worldcat2record/',
],
],
'backendId' => 'WorldCat2',
],
],
// The config reader is a special service manager for loading .ini files:
'config_reader' => [ /* see VuFind\Config\PluginManager for defaults */ ],
// This section contains service manager configurations for all VuFind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org Main Page
*/
interface AccessPermissionInterface
interface AccessPermissionInterface extends ActionConfigInterface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of kills the usefulness of the interface check in the beginning of applyActionConfig because AbstractAction implements AccessPermissionInterface so in essence all actions are now configurable. But this is unavoidable if we want it to be possible for permissions to be applied to any action or group of actions.

{
/**
* Get access permission.
Expand Down
190 changes: 190 additions & 0 deletions module/VuFind/src/VuFind/Action/ActionConfigManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

/**
* Action configuration manager.
*
* PHP version 8
*
* Copyright (C) The National Library of Finland 2026.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see
* <https://www.gnu.org/licenses/>.
*
* @category VuFind
* @package Action
* @author Ere Maijala <ere.maijala@helsinki.fi>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
*/

namespace VuFind\Action;

use Laminas\Router\RouteMatch;
use VuFind\Exception\ConfigException;
use VuFind\ServiceManager\Factory\Autowire;
use VuFind\View\GlobalsContainer;

use function is_string;

/**
* Action configuration manager.
*
* @category VuFind
* @package Action
* @author Ere Maijala <ere.maijala@helsinki.fi>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
*/
class ActionConfigManager
{
/**
* Constructor.
*
* @param GlobalsContainer $globalsContainer Global data container
* @param array $config VuFind configuration
* @param array $appConfig Application config
*/
public function __construct(
protected GlobalsContainer $globalsContainer,
#[Autowire(config: 'config')]
protected array $config,
#[Autowire(service: 'Config')]
protected array $appConfig,
) {
}

/**
* Apply configuration to the action.
*
* @param ActionInterface $action Action
* @param ?RouteMatch $routeMatch Route match
* @param ?string $actionIdentifier Action identifier to use (alternative to one determined from RouteMatch)
*
* @return void
*/
public function applyActionConfig(
ActionInterface $action,
?RouteMatch $routeMatch = null,
?string $actionIdentifier = null,
): void {
if ((!$routeMatch && !$actionIdentifier) || !($action instanceof ActionConfigInterface)) {
return;
}

if (!$actionIdentifier) {
// Try to use lowercase controller-action or just action if available, with route name as a fallback:
if ($actionName = $routeMatch->getParam('action')) {
if ($controllerName = $routeMatch->getParam('controller')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously we need to retain the controller value in routes for the moment, but I wonder if we should start thinking ahead to our eventual end goal. Do we want to call this variable $actionCategoryName instead of $controllerName? Is it worth adding an actionCategory route parameter that falls back to controller for legacy compatibility? (And if we're calling the groups something other than "action categories" I'm open to other names -- that's just what popped into my head first).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used $controllerName because it comes from the 'controller' param, but I can change that and add a comment. This part would be a legacy thing if we replace controller with action category. The end result depends on what we end up doing with the router. If we keep using Laminas Router, things stay pretty much the same, but e.g. with FastRoute there is only a name for the route (which, I think, should be category-action to maintain some order in the chaos).

$actionIdentifier = $controllerName . '/' . $actionName;
} else {
$actionIdentifier = $actionName;
}
} else {
$actionIdentifier = $routeMatch->getMatchedRouteName();
}
}
$actionIdentifier = strtolower($actionIdentifier);
foreach ($this->appConfig['vufind']['action_config'] ?? [] as $currentConfig) {
if ($this->actionIdentifierMatchesConfig($actionIdentifier, $currentConfig)) {
// Apply configuration:
foreach ($currentConfig as $key => $value) {
switch ($key) {
case 'actionIds':
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a comment explaining why we skip these?

case 'accessPermission':
case 'accessDeniedBehavior':
if (!($action instanceof AccessPermissionInterface)) {
throw new ConfigException(
$action::class . ' (action ' . $actionIdentifier . ')'
. " does not implement AccessPermissionInterface for $key configuration"
);
}
if ('accessDeniedBehavior' === $key) {
$action->setAccessDeniedBehavior($value);
} else {
$action->setAccessPermission($value);
}
break;
case 'backendId':
if (!($action instanceof BackendIdInterface)) {
throw new ConfigException(
$action::class . ' (action ' . $actionIdentifier . ')'
. " does not implement BackendIdInterface for $key configuration"
);
}
$action->setBackendId($value);
break;
case 'defaultTab':
case 'fallbackDefaultTab':
if (!($action instanceof DefaultTabInterface)) {
throw new ConfigException(
$action::class . ' (action ' . $actionIdentifier . ')'
. " does not implement DefaultTabInterface for $key configuration"
);
}
if ('fallbackDefaultTab' === $key) {
if ('' === $value) {
// Load default tab setting:
if (!($value = $this->config['Site']['defaultRecordTab'] ?? null)) {
break;
}
}
$action->setFallbackDefaultTab($value);
} else {
$action->setDefaultTab($value);
}
break;
case 'poweredBy':
$this->globalsContainer['poweredBy'] = $value;
break;
default:
throw new ConfigException(
$action::class . ' (action ' . $actionIdentifier . "): Invalid configuration key $key"
);
}
}
break;
}
}
}

/**
* Check if action identifier matches the given config.
*
* @param string $actionIdentifier Action identifier
* @param array $config Config entry
*
* @return bool
*/
protected function actionIdentifierMatchesConfig(string $actionIdentifier, array $config): bool
{
foreach ($config['actionIds'] as $actionId) {
if (is_string($actionId)) {
if ($actionIdentifier === $actionId) {
return true;
}
} else {
switch ($actionId['type']) {
case 'prefix':
if (str_starts_with($actionIdentifier, $actionId['prefix'])) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we strtolower the prefix value since we're already normalizing the action identifier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that no, we shouldn't. The docs say that they are all lowercase, and you should follow the docs. :) Otherwise it means that we'd have to strtolower strings that should already be in lowercase with every single request.

return true;
}
break;
default:
throw new ConfigException(('Invalid actionIds entry: ' . var_export($actionId, true)));
}
}
}
return false;
}
}
Loading