From 6fdeabd5b5a651dba5d35edc16c3a23e9dcc5fc0 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Thu, 18 Jan 2024 13:44:20 +0000 Subject: [PATCH] MDL-80566 user: convert callback 'bulk_user_actions' to hook --- .../local/hooks/extend_bulk_user_actions.php | 41 ++++++ admin/tool/mfa/db/hooks.php | 33 +++++ admin/tool/mfa/lib.php | 17 --- admin/user/user_bulk_forms.php | 63 ++++++--- lib/upgrade.txt | 1 + .../classes/hook/extend_bulk_user_actions.php | 121 ++++++++++++++++++ 6 files changed, 242 insertions(+), 34 deletions(-) create mode 100644 admin/tool/mfa/classes/local/hooks/extend_bulk_user_actions.php create mode 100644 admin/tool/mfa/db/hooks.php create mode 100644 user/classes/hook/extend_bulk_user_actions.php diff --git a/admin/tool/mfa/classes/local/hooks/extend_bulk_user_actions.php b/admin/tool/mfa/classes/local/hooks/extend_bulk_user_actions.php new file mode 100644 index 00000000000..c4ff226219c --- /dev/null +++ b/admin/tool/mfa/classes/local/hooks/extend_bulk_user_actions.php @@ -0,0 +1,41 @@ +. + +namespace tool_mfa\local\hooks; + +/** + * Extend user bulk actions menu + * + * @package tool_mfa + * @copyright 2024 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class extend_bulk_user_actions { + + /** + * Add action to reset MFA factors + * + * @param \core_user\hook\extend_bulk_user_actions $hook + */ + public static function callback(\core_user\hook\extend_bulk_user_actions $hook): void { + if (has_capability('moodle/site:config', \context_system::instance())) { + $hook->add_action('tool_mfa_reset_factors', new \action_link( + new \moodle_url('/admin/tool/mfa/reset_factor.php'), + get_string('resetfactor', 'tool_mfa') + )); + } + } +} diff --git a/admin/tool/mfa/db/hooks.php b/admin/tool/mfa/db/hooks.php new file mode 100644 index 00000000000..ae8576cddb0 --- /dev/null +++ b/admin/tool/mfa/db/hooks.php @@ -0,0 +1,33 @@ +. + +/** + * Hook callbacks for Multi-factor authentication + * + * @package tool_mfa + * @copyright 2024 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$callbacks = [ + [ + 'hook' => core_user\hook\extend_bulk_user_actions::class, + 'callback' => 'tool_mfa\local\hooks\extend_bulk_user_actions::callback', + 'priority' => 0, + ], +]; diff --git a/admin/tool/mfa/lib.php b/admin/tool/mfa/lib.php index 0dbce19945b..5343b1990bf 100644 --- a/admin/tool/mfa/lib.php +++ b/admin/tool/mfa/lib.php @@ -106,23 +106,6 @@ function tool_mfa_after_config(): void { } } -/** - * Any plugin typically an admin tool can add new bulk user actions - * - * @return array - */ -function tool_mfa_bulk_user_actions(): array { - if (!has_capability('moodle/site:config', context_system::instance())) { - return []; - } - return [ - 'tool_mfa_reset_factors' => new action_link( - new moodle_url('/admin/tool/mfa/reset_factor.php'), - get_string('resetfactor', 'tool_mfa') - ), - ]; -} - /** * Serves any files for the guidance page. * diff --git a/admin/user/user_bulk_forms.php b/admin/user/user_bulk_forms.php index 8af94588a86..0b36c941d15 100644 --- a/admin/user/user_bulk_forms.php +++ b/admin/user/user_bulk_forms.php @@ -38,12 +38,33 @@ class user_bulk_action_form extends moodleform { /** @var bool */ protected $hasbulkactions = false; + /** @var array|null */ + protected $actions = null; + /** * Returns an array of action_link's of all bulk actions available for this user. * + * @param bool $flatlist whether to return a flat list (for easier searching) or a list with + * option groups that can be used to build a select element * @return array of action_link objects */ - public function get_actions(): array { + public function get_actions(bool $flatlist = true): array { + if ($this->actions === null) { + $this->actions = $this->build_actions(); + $this->hasbulkactions = !empty($this->actions); + } + if ($flatlist) { + return array_reduce($this->actions, fn($carry, $item) => $carry + $item, []); + } + return $this->actions; + } + + /** + * Builds the list of bulk user actions available for this user. + * + * @return array + */ + protected function build_actions(): array { global $CFG; @@ -89,28 +110,36 @@ class user_bulk_action_form extends moodleform { get_string('bulkadd', 'core_cohort')); } - // Any plugin can append actions to this list by implementing a callback - // _bulk_user_actions() which returns an array of action_link. - // Each new action's key should have a frankenstyle prefix to avoid clashes. - // See MDL-38511 for more details. - $moreactions = get_plugins_with_function('bulk_user_actions', 'lib.php'); + // Collect all bulk user actions. + $hook = new \core_user\hook\extend_bulk_user_actions(); + + // Add actions from core. + foreach ($actions as $identifier => $action) { + $hook->add_action($identifier, $action); + } + + // Add actions from the legacy callback 'bulk_user_actions'. + $moreactions = get_plugins_with_function('bulk_user_actions', 'lib.php', true, true); foreach ($moreactions as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { - $actions += $pluginfunction(); + $pluginactions = $pluginfunction(); + foreach ($pluginactions as $identifier => $action) { + $hook->add_action($identifier, $action); + } } } + // Any plugin can append user bulk actions to this list by implementing a hook callback. + \core\hook\manager::get_instance()->dispatch($hook); + // This method may be called from 'Bulk actions' and 'Browse user list' pages. Some actions // may be irrelevant in one of the contexts and they can be excluded by specifying the // 'excludeactions' customdata. $excludeactions = $this->_customdata['excludeactions'] ?? []; foreach ($excludeactions as $excludeaction) { - unset($actions[$excludeaction]); + $hook->add_action($excludeaction, null); } - - $this->hasbulkactions = !empty($actions); - return $actions; - + return $hook->get_actions(); } /** @@ -131,13 +160,13 @@ class user_bulk_action_form extends moodleform { $mform->addElement('hidden', 'userids'); $mform->setType('userids', PARAM_SEQUENCE); - $actions = [0 => get_string('choose') . '...']; - $bulkactions = $this->get_actions(); - foreach ($bulkactions as $key => $action) { - $actions[$key] = $action->text; + $actions = ['' => [0 => get_string('choose') . '...']]; + $bulkactions = $this->get_actions(false); + foreach ($bulkactions as $category => $categoryactions) { + $actions[$category] = array_map(fn($action) => $action->text, $categoryactions); } $objs = array(); - $objs[] = $selectel = $mform->createElement('select', 'action', get_string('userbulk', 'admin'), $actions); + $objs[] = $selectel = $mform->createElement('selectgroups', 'action', get_string('userbulk', 'admin'), $actions); $selectel->setHiddenLabel(true); if (empty($this->_customdata['hidesubmit'])) { $objs[] =& $mform->createElement('submit', 'doaction', get_string('go')); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 1c94bf2a7db..748413408ea 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -38,6 +38,7 @@ information provided here is intended especially for developers. * There is a new DML method $DB->get_fieldset. For some reason, this did not exist even though get_fieldset_select etc. did. * The following callbacks have been migrated to hooks: - before_standard_html_head() -> core\hook\output\before_standard_head_html_generation + - bulk_user_actions() -> core_user\hook\extend_bulk_user_actions * Deprecated PARAM_ types with the exception of PARAM_CLEAN now emit a deprecation exception. These were all deprecated in Moodle 2.0. * A new \core\attribute\deprecated attribute can be used to more clearly describe deprecated methods. * A new \core\deprecation class can be used to inspect for deprecated attributes: diff --git a/user/classes/hook/extend_bulk_user_actions.php b/user/classes/hook/extend_bulk_user_actions.php new file mode 100644 index 00000000000..0dad128334a --- /dev/null +++ b/user/classes/hook/extend_bulk_user_actions.php @@ -0,0 +1,121 @@ +. + +namespace core_user\hook; + +use action_link; +use core\hook\described_hook; +use core\hook\deprecated_callback_replacement; + +/** + * Class extend_bulk_user_actions + * + * @package core_user + * @copyright 2024 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class extend_bulk_user_actions implements described_hook, deprecated_callback_replacement { + + /** + * Describes the hook purpose. + * + * @return string + */ + public static function get_hook_description(): string { + return 'Extend bulk user actions menu'; + } + + /** + * List of tags that describe this hook. + * + * @return string[] + */ + public static function get_hook_tags(): array { + return ['user']; + } + + /** + * Returns list of lib.php plugin callbacks that were deprecated by the hook. + * + * @return array + */ + public static function get_deprecated_plugin_callbacks(): array { + return ['bulk_user_actions']; + } + + /** @var array $actions Stores all added user actions */ + private $actions = []; + + /** + * To be called by callback to add an action to the bulk user actions menu + * + * Callbacks with higher priority will be called first and actions they added will be displayed first. + * Callbacks with lower priority can override actions added by callbacks with higher priority. + * + * To prevent accidental overrides plugins should prefix the action identifier with the plugin name. + * + * @param string $identifier Unique key for the action, recommended to prefix with plugin name + * @param action_link|null $action an object containing the action URL and text, + * other properties are ignored. Can be set to null to remove an action added by somebody else. + * @param ?string $category Label for the option group in the action select dropdown + */ + public function add_action(string $identifier, ?action_link $action, ?string $category = null): void { + $category = $category ?? get_string('actions', 'moodle'); + + // If an action with the same identifier already exists in another option group, remove it. + $oldcategory = $this->find_action_category($identifier); + if ($oldcategory !== null && ($oldcategory !== $category || $action === null)) { + unset($this->actions[$oldcategory][$identifier]); + if (empty($this->actions[$oldcategory])) { + unset($this->actions[$oldcategory]); + } + } + + // Add the new action. + if ($action !== null) { + $this->actions += [$category => []]; + $this->actions[$category][$identifier] = $action; + } + } + + /** + * Returns all actions groupped by category + * + * @return action_link[][] + */ + public function get_actions(): array { + return $this->actions; + } + + /** + * Allows to locate an action by the identifier + * + * This method returns the option group label. The action itself can be found as: + * $category = $this->find_action_category($identifier); + * $action = $this->get_actions()[$category][$identifier]; + * + * @param string $identifier + * @return string|null + */ + public function find_action_category(string $identifier): ?string { + foreach ($this->actions as $category => $actions) { + if (array_key_exists($identifier, $actions)) { + return $category; + } + } + return null; + } +}