Merge branch 'MDL-74954-master' of https://github.com/skodak/moodle
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_admin\table;
|
||||
|
||||
use core_plugin_manager;
|
||||
use flexible_table;
|
||||
use html_writer;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once("{$CFG->libdir}/tablelib.php");
|
||||
|
||||
/**
|
||||
* Plugin Management table.
|
||||
*
|
||||
* @package core_admin
|
||||
* @copyright 2023 Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class hook_list_table extends flexible_table {
|
||||
|
||||
/** @var \core\plugininfo\base[] The plugin list */
|
||||
protected array $plugins = [];
|
||||
|
||||
/** @var int The number of enabled plugins of this type */
|
||||
protected int $enabledplugincount = 0;
|
||||
|
||||
/** @var core_plugin_manager */
|
||||
protected core_plugin_manager $pluginmanager;
|
||||
|
||||
/** @var string The plugininfo class for this plugintype */
|
||||
protected string $plugininfoclass;
|
||||
|
||||
/** @var stdClass[] The list of emitted hooks with metadata */
|
||||
protected array $emitters;
|
||||
|
||||
public function __construct() {
|
||||
global $CFG;
|
||||
|
||||
$this->define_baseurl('/admin/hooks.php');
|
||||
parent::__construct('core_admin-hook_list_table');
|
||||
|
||||
// Add emitted hooks.
|
||||
$this->emitters = \core\hook\manager::discover_known_hooks();
|
||||
|
||||
$this->setup_column_configuration();
|
||||
$this->setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the column configuration for this table.
|
||||
*/
|
||||
protected function setup_column_configuration(): void {
|
||||
$columnlist = [
|
||||
'details' => get_string('hookname', 'core_admin'),
|
||||
'callbacks' => get_string('hookcallbacks', 'core_admin'),
|
||||
'deprecates' => get_string('hookdeprecates', 'core_admin'),
|
||||
];
|
||||
$this->define_columns(array_keys($columnlist));
|
||||
$this->define_headers(array_values($columnlist));
|
||||
|
||||
$columnswithhelp = [
|
||||
'callbacks' => new \help_icon('hookcallbacks', 'admin'),
|
||||
];
|
||||
$columnhelp = array_map(function (string $column) use ($columnswithhelp): ?\renderable {
|
||||
if (array_key_exists($column, $columnswithhelp)) {
|
||||
return $columnswithhelp[$column];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, array_keys($columnlist));
|
||||
$this->define_help_for_headers($columnhelp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the table.
|
||||
*/
|
||||
public function out(): void {
|
||||
// All hook consumers referenced from the db/hooks.php files.
|
||||
$hookmanager = \core\hook\manager::get_instance();
|
||||
$allhooks = $hookmanager->get_all_callbacks();
|
||||
|
||||
// Add any unused hooks.
|
||||
foreach (array_keys($this->emitters) as $classname) {
|
||||
if (isset($allhooks[$classname])) {
|
||||
continue;
|
||||
}
|
||||
$allhooks[$classname] = [];
|
||||
}
|
||||
|
||||
foreach ($allhooks as $classname => $consumers) {
|
||||
$this->add_data_keyed(
|
||||
$this->format_row((object) [
|
||||
'classname' => $classname,
|
||||
'callbacks' => $consumers,
|
||||
]),
|
||||
$this->get_row_class($classname),
|
||||
);
|
||||
}
|
||||
|
||||
$this->finish_output(false);
|
||||
}
|
||||
|
||||
protected function col_details(stdClass $row): string {
|
||||
return $row->classname .
|
||||
$this->get_description($row) .
|
||||
html_writer::div($this->get_tags_for_row($row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the name column content.
|
||||
*
|
||||
* @param stdClass $row
|
||||
* @return string
|
||||
*/
|
||||
protected function get_description(stdClass $row): string {
|
||||
if (!array_key_exists($row->classname, $this->emitters)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return html_writer::tag(
|
||||
'small',
|
||||
clean_text(markdown_to_html($this->emitters[$row->classname]['description']), FORMAT_HTML),
|
||||
);
|
||||
}
|
||||
|
||||
protected function col_deprecates(stdClass $row): string {
|
||||
if (!class_exists($row->classname)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rc = new \ReflectionClass($row->classname);
|
||||
if (!$rc->implementsInterface(\core\hook\deprecated_callback_replacement::class)) {
|
||||
return '';
|
||||
}
|
||||
$deprecates = call_user_func([$row->classname, 'get_deprecated_plugin_callbacks']);
|
||||
if (count($deprecates) === 0) {
|
||||
return '';
|
||||
}
|
||||
$content = html_writer::start_tag('ul');
|
||||
|
||||
foreach ($deprecates as $deprecatedmethod) {
|
||||
$content .= html_writer::tag('li', $deprecatedmethod);
|
||||
}
|
||||
$content .= html_writer::end_tag('ul');
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function col_callbacks(stdClass $row): string {
|
||||
global $CFG;
|
||||
|
||||
$hookclass = $row->classname;
|
||||
$cbinfo = [];
|
||||
foreach ($row->callbacks as $definition) {
|
||||
$iscallable = is_callable($definition['callback'], false, $callbackname);
|
||||
$isoverridden = isset($CFG->hooks_callback_overrides[$hookclass][$definition['callback']]);
|
||||
$info = "{$callbackname} ({$definition['priority']})";
|
||||
if (!$iscallable) {
|
||||
$info .= ' ';
|
||||
$info .= $this->get_tag(
|
||||
get_string('error'),
|
||||
'danger',
|
||||
get_string('hookcallbacknotcallable', 'core_admin', $callbackname),
|
||||
);
|
||||
}
|
||||
if ($isoverridden) {
|
||||
// The lang string meaning should be close enough here.
|
||||
$info .= $this->get_tag(
|
||||
get_string('hookconfigoverride', 'core_admin'),
|
||||
'warning',
|
||||
get_string('hookconfigoverride_help', 'core_admin'),
|
||||
);
|
||||
}
|
||||
|
||||
$cbinfo[] = $info;
|
||||
}
|
||||
|
||||
if ($cbinfo) {
|
||||
$output = html_writer::start_tag('ol');
|
||||
foreach ($cbinfo as $callback) {
|
||||
$class = '';
|
||||
if ($definition['disabled']) {
|
||||
$class = 'dimmed_text';
|
||||
}
|
||||
$output .= html_writer::tag('li', $callback, ['class' => $class]);
|
||||
}
|
||||
$output .= html_writer::end_tag('ol');
|
||||
return $output;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML to display the badge with tooltip.
|
||||
*
|
||||
* @param string $tag The main text to display
|
||||
* @param null|string $type The pill type
|
||||
* @param null|string $tooltip The content of the tooltip
|
||||
* @return string
|
||||
*/
|
||||
protected function get_tag(
|
||||
string $tag,
|
||||
?string $type = null,
|
||||
?string $tooltip = null,
|
||||
): string {
|
||||
$attributes = [];
|
||||
|
||||
if ($type === null) {
|
||||
$type = 'info';
|
||||
}
|
||||
|
||||
if ($tooltip) {
|
||||
$attributes['data-toggle'] = 'tooltip';
|
||||
$attributes['title'] = $tooltip;
|
||||
}
|
||||
return html_writer::span($tag, "badge badge-{$type}", $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code to display a set of tags for this table row.
|
||||
*
|
||||
* @param stdClass $row
|
||||
* @return string
|
||||
*/
|
||||
protected function get_tags_for_row(stdClass $row): string {
|
||||
if (!array_key_exists($row->classname, $this->emitters)) {
|
||||
// This hook has been defined in the db/hooks.php file
|
||||
// but does not refer to a hook in this version of Moodle.
|
||||
return $this->get_tag(
|
||||
get_string('hookunknown', 'core_admin'),
|
||||
'warning',
|
||||
get_string('hookunknown_desc', 'core_admin'),
|
||||
);
|
||||
}
|
||||
|
||||
if (!class_exists($row->classname)) {
|
||||
// This hook has been defined in a hook discovery agent, but the class it refers to could not be found.
|
||||
return $this->get_tag(
|
||||
get_string('hookclassmissing', 'core_admin'),
|
||||
'warning',
|
||||
get_string('hookclassmissing_desc', 'core_admin'),
|
||||
);
|
||||
}
|
||||
|
||||
$tags = $this->emitters[$row->classname]['tags'] ?? [];
|
||||
$taglist = array_map(function($tag): string {
|
||||
if (is_array($tag)) {
|
||||
return $this->get_tag(...$tag);
|
||||
}
|
||||
return $this->get_tag($tag, 'badge badge-info');
|
||||
}, $tags);
|
||||
|
||||
return implode("\n", $taglist);
|
||||
}
|
||||
|
||||
protected function get_row_class(string $classname): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Hooks overview page.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once($CFG->libdir . '/adminlib.php');
|
||||
require_once($CFG->libdir . '/tablelib.php');
|
||||
|
||||
admin_externalpage_setup('hooksoverview');
|
||||
require_capability('moodle/site:config', \core\context\system::instance());
|
||||
|
||||
$hookmanager = \core\hook\manager::get_instance();
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading(get_string('hooksoverview', 'core_admin'));
|
||||
|
||||
$table = new \core_admin\table\hook_list_table();
|
||||
$table->out();
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
@@ -121,4 +121,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
|
||||
"$CFG->wwwroot/$CFG->admin/purgecaches.php"));
|
||||
|
||||
$ADMIN->add('development', new admin_externalpage('thirdpartylibs', new lang_string('thirdpartylibs','admin'), "$CFG->wwwroot/$CFG->admin/thirdpartylibs.php"));
|
||||
|
||||
$ADMIN->add('development', new admin_externalpage('hooksoverview',
|
||||
new lang_string('hooksoverview', 'admin'), "$CFG->wwwroot/$CFG->admin/hooks.php"));
|
||||
} // end of speedup
|
||||
|
||||
@@ -722,6 +722,24 @@ $string['hiddenuserfields'] = 'Hide user fields';
|
||||
$string['hidefromall'] = 'Hide from all users';
|
||||
$string['hidefromnone'] = 'Hide from nobody';
|
||||
$string['hidefromstudents'] = 'Hide from students';
|
||||
$string['hookcallbacks'] = 'Callbacks';
|
||||
$string['hookcallbacks_help'] = 'The list of callbacks which will be called when the hook is dispatched.
|
||||
|
||||
The order shown is the order in which callbacks are called.
|
||||
|
||||
A callback with a higher priority will be called before one with lower priority.';
|
||||
$string['hookcallbacknotcallable'] = 'This callback is not callable. This could be because the class or method does not exist, or because the method is not public.';
|
||||
$string['hookconfigoverride'] = 'Overridden';
|
||||
$string['hookconfigoverride_help'] = 'The definition of this callback has been overridden in the site configuration file, config.php';
|
||||
$string['hookdeprecates'] = 'Deprecated lib.php callbacks';
|
||||
$string['hookdescription'] = 'Description';
|
||||
$string['hookdescriptionmissing'] = 'Hook does not have a description method';
|
||||
$string['hookclassmissing'] = 'Hook class not found';
|
||||
$string['hookclassmissing_desc'] = 'The hook discovery agent has returned a class that does not exist.';
|
||||
$string['hookunknown'] = 'Hook not found';
|
||||
$string['hookunknown_desc'] = 'The object that this callback listens to is not available. It may have been removed or renamed, or it may not be available in this version of Moodle.';
|
||||
$string['hookname'] = 'Hook';
|
||||
$string['hooksoverview'] = 'Hooks overview';
|
||||
$string['hostname'] = 'Host name';
|
||||
$string['htmleditor'] = 'HTML editor';
|
||||
$string['htmleditorsettings'] = 'HTML editor settings';
|
||||
|
||||
@@ -68,6 +68,7 @@ $string['cachedef_groupdata'] = 'Course group information';
|
||||
$string['cachedef_h5p_content_type_translations'] = 'H5P content-type libraries translations';
|
||||
$string['cachedef_h5p_libraries'] = 'H5P libraries';
|
||||
$string['cachedef_h5p_library_files'] = 'H5P library files';
|
||||
$string['cachedef_hookcallbacks'] = 'Hook callbacks';
|
||||
$string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
|
||||
$string['cachedef_langmenu'] = 'List of available languages';
|
||||
$string['cachedef_license'] = 'List of licences';
|
||||
|
||||
@@ -112,6 +112,7 @@ class core_component {
|
||||
'Psr\\Http\\Client' => 'lib/psr/http-client/src',
|
||||
'Psr\\Http\\Factory' => 'lib/psr/http-factory/src',
|
||||
'Psr\\Http\\Message' => 'lib/psr/http-message/src',
|
||||
'Psr\\EventDispatcher' => 'lib/psr/event-dispatcher/src',
|
||||
'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src',
|
||||
'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src',
|
||||
'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\hook;
|
||||
|
||||
/**
|
||||
* Interface for hook callbacks that were deprecated by the hook.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface deprecated_callback_replacement {
|
||||
/**
|
||||
* Returns list of lib.php plugin callbacks that were deprecated by the hook.
|
||||
*
|
||||
* It is used for automatic debugging messages and if present it
|
||||
* also skips relevant legacy callbacks in plugins that implemented callbacks
|
||||
* for this hook (to allow plugin compatibility with multiple Moodle branches).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_deprecated_plugin_callbacks(): array;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\hook;
|
||||
|
||||
/**
|
||||
* Interface for a hook to provide a description of itself for administrator information.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface described_hook {
|
||||
/**
|
||||
* Mandatory hook purpose description in Markdown format
|
||||
* used on Hooks overview page.
|
||||
*
|
||||
* It should include description of callback priority setting
|
||||
* rules if applicable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_hook_description(): string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\hook;
|
||||
|
||||
/**
|
||||
* This interface describes a component which can discover hooks in its own namespace.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface discovery_agent {
|
||||
/**
|
||||
* Discover hooks belonging to the component.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function discover_hooks(): array;
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\hook;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\ListenerProviderInterface;
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Hook manager implementing "Dispatcher" and "Event Provider" from PSR-14.
|
||||
*
|
||||
* Due to class/method naming restrictions and collision with
|
||||
* Moodle events the definitions from PSR-14 should be interpreted as:
|
||||
*
|
||||
* 1. Event --> Hook
|
||||
* 2. Listener --> Hook callback
|
||||
* 3. Emitter --> Hook emitter
|
||||
* 4. Dispatcher --> Hook dispatcher - implemented in manager::dispatch()
|
||||
* 5. Listener Provider --> Hook callback provider - implemented in manager::get_callbacks_for_hook()
|
||||
*
|
||||
* Note that technically any object can be a hook, but it is recommended
|
||||
* to put all hook classes into \component_name\hook namespaces and
|
||||
* each hook should implement \core\hook\described_hook interface.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class manager implements
|
||||
EventDispatcherInterface,
|
||||
ListenerProviderInterface {
|
||||
|
||||
/** @var ?manager the one instance of listener provider and dispatcher */
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array list of callback definitions for each hook class. */
|
||||
private $allcallbacks = [];
|
||||
|
||||
/** @var array list of all deprecated lib.php plugin callbacks. */
|
||||
private $alldeprecations = [];
|
||||
|
||||
/**
|
||||
* Constructor can be used only from factory methods.
|
||||
*/
|
||||
private function __construct() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method, returns instance of manager that serves
|
||||
* as hook dispatcher and callback provider.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function get_instance(): manager {
|
||||
if (!self::$instance) {
|
||||
self::$instance = new self();
|
||||
self::$instance->init_standard_callbacks();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for testing of hook manager in PHPUnit tests.
|
||||
*
|
||||
* @param array $componentfiles list of hook callback files for each component.
|
||||
* @return self
|
||||
*/
|
||||
public static function phpunit_get_instance(array $componentfiles): manager {
|
||||
if (!PHPUNIT_TEST) {
|
||||
throw new \coding_exception('Invalid call of manager::phpunit_get_instance() outside of tests');
|
||||
}
|
||||
$instance = new self();
|
||||
$instance->load_callbacks($componentfiles);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all hook caches. This is intended to be called only
|
||||
* from the admin/hooks.php page after callback override is changed.
|
||||
*
|
||||
* @return void
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function reset_caches(): void {
|
||||
if (PHPUNIT_TEST && $this === self::$instance) {
|
||||
debugging('\core\hook\manager::get_instance()->reset_caches() is not supposed to be called in PHPUnit tests',
|
||||
DEBUG_DEVELOPER);
|
||||
return;
|
||||
}
|
||||
|
||||
// WARNING: This will not work when callback overrides are changed
|
||||
// and multiple web nodes with local cache stores are present - in that
|
||||
// case admins must purge all caches when tweaking callback overrides.
|
||||
$cache = \cache::make('core', 'hookcallbacks');
|
||||
$cache->delete('callbacks');
|
||||
$cache->delete('deprecations');
|
||||
|
||||
$this->init_standard_callbacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of callbacks for given hook name.
|
||||
*
|
||||
* NOTE: this is the "Listener Provider" described in PSR-14,
|
||||
* instead of instance parameter it uses real PHP class names.
|
||||
* Moodle hooks should be final and parents of hook class are not
|
||||
* considered when resolving callbacks.
|
||||
*
|
||||
* @param string $hookclassname PHP class name of hook
|
||||
* @return array list of callback definitions
|
||||
*/
|
||||
public function get_callbacks_for_hook(string $hookclassname): array {
|
||||
return $this->allcallbacks[$hookclassname] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all callbacks found in db/hooks.php files.
|
||||
*
|
||||
* @return iterable
|
||||
*/
|
||||
public function get_all_callbacks(): iterable {
|
||||
return $this->allcallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of listeners for the specified event.
|
||||
*
|
||||
* @param object $event The object being listened to (aka hook).
|
||||
* @return iterable<callable>
|
||||
* An iterable (array, iterator, or generator) of callables. Each
|
||||
* callable MUST be type-compatible with $event.
|
||||
* Please note that in Moodle the callable must be a string.
|
||||
*/
|
||||
public function getListenersForEvent(object $event): iterable {
|
||||
// Callbacks are sorted by priority, highest first at load-time.
|
||||
$hookclassname = get_class($event);
|
||||
$callbacks = $this->get_callbacks_for_hook($hookclassname);
|
||||
|
||||
if (count($callbacks) === 0) {
|
||||
// Nothing is interested in this hook.
|
||||
return new \EmptyIterator();
|
||||
}
|
||||
|
||||
foreach ($callbacks as $definition) {
|
||||
if ($definition['disabled']) {
|
||||
continue;
|
||||
}
|
||||
$callback = $definition['callback'];
|
||||
|
||||
if ($this->is_callback_valid($definition['component'], $callback)) {
|
||||
yield $callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that callback is valid.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $callback
|
||||
* @return bool
|
||||
*/
|
||||
private function is_callback_valid(string $component, string $callback): bool {
|
||||
[$callbackclass, $callbackmethod] = explode('::', $callback, 2);
|
||||
if (!class_exists($callbackclass)) {
|
||||
debugging(
|
||||
"Hook callback definition contains invalid 'callback' class name in '$component'. " .
|
||||
"Callback class '{$callbackclass}' not found.",
|
||||
DEBUG_DEVELOPER,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$rc = new \ReflectionClass($callbackclass);
|
||||
if (!$rc->hasMethod($callbackmethod)) {
|
||||
debugging(
|
||||
"Hook callback definition contains invalid 'callback' method name in '$component'. " .
|
||||
"Callback method not found.",
|
||||
DEBUG_DEVELOPER,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$rcm = $rc->getMethod($callbackmethod);
|
||||
if (!$rcm->isStatic()) {
|
||||
debugging(
|
||||
"Hook callback definition contains invalid 'callback' method name in '$component'. " .
|
||||
"Callback method not a static method.",
|
||||
DEBUG_DEVELOPER,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_callable($callback, false, $callablename)) {
|
||||
debugging(
|
||||
"Cannot execute callback '$callablename' from '$component'" .
|
||||
"Callback method not callable.",
|
||||
DEBUG_DEVELOPER
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of Hook class names that have registered callbacks.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_hooks_with_callbacks(): array {
|
||||
return array_keys($this->allcallbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide all relevant listeners with an event to process.
|
||||
*
|
||||
* @param object $event The object to process (aka hook).
|
||||
* @return object The Event that was passed, now modified by listeners.
|
||||
*/
|
||||
public function dispatch(object $event): object {
|
||||
// We can dispatch only after the lib/setup.php includes,
|
||||
// that is right before the database connection is made,
|
||||
// the MUC caches need to be working already.
|
||||
if (!function_exists('setup_DB')) {
|
||||
debugging('Hooks cannot be dispatched yet', DEBUG_DEVELOPER);
|
||||
return $event;
|
||||
}
|
||||
|
||||
$callbacks = $this->getListenersForEvent($event);
|
||||
|
||||
if (empty($callbacks)) {
|
||||
// Nothing is interested in this hook.
|
||||
return $event;
|
||||
}
|
||||
|
||||
foreach ($callbacks as $callback) {
|
||||
// Note: PSR-14 states:
|
||||
// If passed a Stoppable Event, a Dispatcher
|
||||
// MUST call isPropagationStopped() on the Event before each Listener has been called.
|
||||
// If that method returns true it MUST return the Event to the Emitter immediately and
|
||||
// MUST NOT call any further Listeners. This implies that if an Event is passed to the
|
||||
// Dispatcher that always returns true from isPropagationStopped(), zero listeners will be called.
|
||||
// Ergo, we check for a stopped event before calling each listener, not afterwards.
|
||||
if ($event instanceof StoppableEventInterface) {
|
||||
if ($event->isPropagationStopped()) {
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
|
||||
call_user_func($callback, $event);
|
||||
}
|
||||
|
||||
// Developers need to be careful to not create infinite loops in hook callbacks.
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise list of all callbacks for each hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_standard_callbacks(): void {
|
||||
global $CFG;
|
||||
|
||||
$this->allcallbacks = [];
|
||||
$this->alldeprecations = [];
|
||||
|
||||
$cache = null;
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!PHPUNIT_TEST && !CACHE_DISABLE_ALL) {
|
||||
$cache = \cache::make('core', 'hookcallbacks');
|
||||
$callbacks = $cache->get('callbacks');
|
||||
$deprecations = $cache->get('deprecations');
|
||||
if (is_array($callbacks) && is_array($deprecations)) {
|
||||
$this->allcallbacks = $callbacks;
|
||||
$this->alldeprecations = $deprecations;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// Get list of all files with callbacks, one per component.
|
||||
$components = ['core' => "{$CFG->dirroot}/lib/db/hooks.php"];
|
||||
$plugintypes = \core_component::get_plugin_types();
|
||||
foreach ($plugintypes as $plugintype => $plugintypedir) {
|
||||
$plugins = \core_component::get_plugin_list($plugintype);
|
||||
foreach ($plugins as $pluginname => $plugindir) {
|
||||
if (!$plugindir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$components["{$plugintype}_{$pluginname}"] = "{$plugindir}/db/hooks.php";
|
||||
}
|
||||
}
|
||||
|
||||
// Load the callbacks and apply overrides.
|
||||
$this->load_callbacks($components);
|
||||
|
||||
if ($cache) {
|
||||
$cache->set('callbacks', $this->allcallbacks);
|
||||
$cache->set('deprecations', $this->alldeprecations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load callbacks from component db/hooks.php files.
|
||||
*
|
||||
* @param array $componentfiles list of all components with their callback files
|
||||
* @return void
|
||||
*/
|
||||
private function load_callbacks(array $componentfiles): void {
|
||||
$this->allcallbacks = [];
|
||||
$this->alldeprecations = [];
|
||||
|
||||
array_map(
|
||||
[$this, 'add_component_callbacks'],
|
||||
array_keys($componentfiles),
|
||||
$componentfiles,
|
||||
);
|
||||
$this->load_callback_overrides();
|
||||
$this->prioritise_callbacks();
|
||||
$this->fetch_deprecated_callbacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* In extremely special cases admins may decide to override callbacks via config.php setting.
|
||||
*/
|
||||
private function load_callback_overrides(): void {
|
||||
global $CFG;
|
||||
|
||||
if (!property_exists($CFG, 'hooks_callback_overrides')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_iterable($CFG->hooks_callback_overrides)) {
|
||||
debugging('hooks_callback_overrides must be an array', DEBUG_DEVELOPER);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($CFG->hooks_callback_overrides as $hookclassname => $overrides) {
|
||||
if (!is_iterable($overrides)) {
|
||||
debugging('hooks_callback_overrides must be an array of arrays', DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists($hookclassname, $this->allcallbacks)) {
|
||||
debugging('hooks_callback_overrides must be an array of arrays with existing hook classnames', DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($overrides as $callback => $override) {
|
||||
if (!is_array($override)) {
|
||||
debugging('hooks_callback_overrides must be an array of arrays', DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
foreach ($this->allcallbacks[$hookclassname] as $index => $definition) {
|
||||
if ($definition['callback'] === $callback) {
|
||||
if (isset($override['priority'])) {
|
||||
$definition['defaultpriority'] = $definition['priority'];
|
||||
$definition['priority'] = (int) $override['priority'];
|
||||
}
|
||||
|
||||
if (!empty($override['disabled'])) {
|
||||
$definition['disabled'] = true;
|
||||
}
|
||||
|
||||
$this->allcallbacks[$hookclassname][$index] = $definition;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
debugging("Unable to find callback '{$callback}' for '{$hookclassname}'", DEBUG_DEVELOPER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioritise the callbacks.
|
||||
*/
|
||||
private function prioritise_callbacks(): void {
|
||||
// Prioritise callbacks.
|
||||
foreach ($this->allcallbacks as $hookclassname => $hookcallbacks) {
|
||||
\core_collator::asort_array_of_arrays_by_key($hookcallbacks, 'priority', \core_collator::SORT_NUMERIC);
|
||||
$hookcallbacks = array_reverse($hookcallbacks);
|
||||
$this->allcallbacks[$hookclassname] = $hookcallbacks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of callbacks that this hook replaces.
|
||||
*/
|
||||
private function fetch_deprecated_callbacks(): void {
|
||||
$candidates = self::discover_known_hooks();
|
||||
|
||||
/** @var class-string<deprecated_callback_replacement> $hookclassname */
|
||||
foreach (array_keys($candidates) as $hookclassname) {
|
||||
if (!class_exists($hookclassname)) {
|
||||
continue;
|
||||
}
|
||||
// It's 2023 and PHP still doesn't provide a simple way to detect if a class implements an interface without
|
||||
// that class being instantiated.
|
||||
$rc = new \ReflectionClass($hookclassname);
|
||||
if (!$rc->implementsInterface(\core\hook\deprecated_callback_replacement::class)) {
|
||||
continue;
|
||||
}
|
||||
$deprecations = $hookclassname::get_deprecated_plugin_callbacks();
|
||||
if (!$deprecations) {
|
||||
continue;
|
||||
}
|
||||
foreach ($deprecations as $deprecation) {
|
||||
$this->alldeprecations[$deprecation][] = $hookclassname;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hook callbacks from file.
|
||||
*
|
||||
* @param string $component component where hook callbacks are defined
|
||||
* @param string $hookfile file with list of all callbacks for component
|
||||
* @return void
|
||||
*/
|
||||
private function add_component_callbacks(string $component, string $hookfile): void {
|
||||
if (!file_exists($hookfile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parsecallbacks = function($hookfile) {
|
||||
$callbacks = [];
|
||||
include($hookfile);
|
||||
return $callbacks;
|
||||
};
|
||||
|
||||
$callbacks = $parsecallbacks($hookfile);
|
||||
|
||||
if (!is_array($callbacks) || !$callbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($callbacks as $callbackdata) {
|
||||
if (empty($callbackdata['hook'])) {
|
||||
debugging("Hook callback definition requires 'hook' name in '$component'", DEBUG_DEVELOPER);
|
||||
continue;
|
||||
}
|
||||
|
||||
$callbackmethod = $this->normalise_callback($component, $callbackdata);
|
||||
if ($callbackmethod === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$callback = [
|
||||
'callback' => $callbackmethod,
|
||||
'component' => $component,
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
];
|
||||
|
||||
if (isset($callbackdata['priority'])) {
|
||||
$callback['priority'] = (int) $callbackdata['priority'];
|
||||
}
|
||||
|
||||
$hook = ltrim($callbackdata['hook'], '\\'); // Normalise hook class name.
|
||||
$this->allcallbacks[$hook][] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the callback class::method value.
|
||||
*
|
||||
* @param string $component
|
||||
* @param array $callback
|
||||
* @return null|string
|
||||
*/
|
||||
private function normalise_callback(string $component, array $callback): ?string {
|
||||
if (empty($callback['callback'])) {
|
||||
debugging("Hook callback definition requires 'callback' callable in '$component'", DEBUG_DEVELOPER);
|
||||
return null;
|
||||
}
|
||||
$classmethod = $callback['callback'];
|
||||
if (!is_string($classmethod)) {
|
||||
debugging("Hook callback definition contains invalid 'callback' string in '$component'", DEBUG_DEVELOPER);
|
||||
return null;
|
||||
}
|
||||
if (!str_contains($classmethod, '::')) {
|
||||
debugging(
|
||||
"Hook callback definition contains invalid 'callback' static class method string in '$component'",
|
||||
DEBUG_DEVELOPER
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalise the callback class::method name, we use it later as an identifier.
|
||||
$classmethod = ltrim($classmethod, '\\');
|
||||
|
||||
return $classmethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the plugin callback from lib.php deprecated by any hook?
|
||||
*
|
||||
* @param string $plugincallback short callback name without the component prefix
|
||||
* @return bool
|
||||
*/
|
||||
public function is_deprecated_plugin_callback(string $plugincallback): bool {
|
||||
return isset($this->alldeprecations[$plugincallback]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a hook callback in component that deprecates given lib.php plugin callback?
|
||||
*
|
||||
* NOTE: if there is both hook and deprecated callback then we ignore the old callback
|
||||
* to allow compatibility of contrib plugins with multiple Moodle branches.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $plugincallback short callback name without the component prefix
|
||||
* @return bool
|
||||
*/
|
||||
public function is_deprecating_hook_present(string $component, string $plugincallback): bool {
|
||||
if (!isset($this->alldeprecations[$plugincallback])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->alldeprecations[$plugincallback] as $hookclassname) {
|
||||
if (!isset($this->allcallbacks[$hookclassname])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->allcallbacks[$hookclassname] as $definition) {
|
||||
if ($definition['component'] === $component) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of hooks discovered through standardised Moodle methods.
|
||||
*
|
||||
* Note that the exact discovery logic may change in the future,
|
||||
* for now this looks for hooks mentioned in callback registrations
|
||||
* and non-abstract classes in \component_name\hook namespaces that
|
||||
* implement described_hook interface.
|
||||
*
|
||||
* @return array hook class names
|
||||
*/
|
||||
public static function discover_known_hooks(): array {
|
||||
$hooks = \core\hooks::discover_hooks();
|
||||
|
||||
foreach (\core_component::get_component_names() as $component) {
|
||||
$classname = "{$component}\\hooks";
|
||||
|
||||
if (!class_exists($classname)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rc = new \ReflectionClass($classname);
|
||||
if (!$rc->implementsInterface(\core\hook\hook_discover_agent::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hooks = array_merge($hooks, $classname::discover_hooks());
|
||||
}
|
||||
|
||||
return $hooks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core;
|
||||
|
||||
/**
|
||||
* Hook discovery agent for core.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class hooks implements \core\hook\discovery_agent {
|
||||
public static function discover_hooks(): array {
|
||||
// Describe any hard-coded hooks which can't be easily discovered by namespace.
|
||||
$hooks = [];
|
||||
|
||||
$hooks = array_merge($hooks, self::discover_hooks_in_namespace('core', 'hook'));
|
||||
|
||||
return $hooks;
|
||||
}
|
||||
|
||||
public static function discover_hooks_in_namespace(string $component, string $namespace): array {
|
||||
$classes = \core_component::get_component_classes_in_namespace($component, $namespace);
|
||||
|
||||
$hooks = [];
|
||||
foreach (array_keys($classes) as $classname) {
|
||||
$rc = new \ReflectionClass($classname);
|
||||
if ($rc->isAbstract()) {
|
||||
// Skip abstract classes.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_a($classname, \core\hook\manager::class, true)) {
|
||||
// Skip the manager.
|
||||
continue;
|
||||
}
|
||||
|
||||
$hooks[$classname] = [
|
||||
'class' => $classname,
|
||||
'description' => '',
|
||||
'tags' => [],
|
||||
];
|
||||
|
||||
if ($rc->implementsInterface(\core\hook\described_hook::class)) {
|
||||
$hooks[$classname]['description'] = $classname::get_hook_description();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return $hooks;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,19 @@ $definitions = array(
|
||||
'simpledata' => true,
|
||||
),
|
||||
|
||||
// Hook callbacks cache.
|
||||
// There is a static cache in hook manager, data is fetched once per page on first hook execution.
|
||||
// This cache needs to be invalidated during upgrades when code changes and when callbacks
|
||||
// overrides are updated.
|
||||
'hookcallbacks' => array(
|
||||
'mode' => cache_store::MODE_APPLICATION,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => true,
|
||||
'staticacceleration' => false,
|
||||
// WARNING: Manual cache purge may be required when overriding hook callbacks.
|
||||
'canuselocalstore' => true,
|
||||
),
|
||||
|
||||
// Cache for question definitions. This is used by the question_bank class.
|
||||
// Users probably do not need to know about this cache. They will just call
|
||||
// question_bank::load_question.
|
||||
|
||||
+46
-4
@@ -7861,9 +7861,10 @@ function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php'
|
||||
* @param string $file the name of file within the plugin that defines the
|
||||
* function. Defaults to lib.php.
|
||||
* @param bool $include Whether to include the files that contain the functions or not.
|
||||
* @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
|
||||
* @return array with [plugintype][plugin] = functionname
|
||||
*/
|
||||
function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
|
||||
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
|
||||
global $CFG;
|
||||
|
||||
if (during_initial_install() || isset($CFG->upgraderunning)) {
|
||||
@@ -7871,6 +7872,25 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true
|
||||
return [];
|
||||
}
|
||||
|
||||
$plugincallback = $function;
|
||||
$filtermigrated = function($plugincallback, $pluginfunctions): array {
|
||||
foreach ($pluginfunctions as $plugintype => $plugins) {
|
||||
foreach ($plugins as $plugin => $unusedfunction) {
|
||||
$component = $plugintype . '_' . $plugin;
|
||||
if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) {
|
||||
if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
|
||||
// Ignore the old callback, it is there only for older Moodle versions.
|
||||
unset($pluginfunctions[$plugintype][$plugin]);
|
||||
} else {
|
||||
debugging("Callback $plugincallback in $component component should be migrated to new hook callback",
|
||||
DEBUG_DEVELOPER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pluginfunctions;
|
||||
};
|
||||
|
||||
$cache = \cache::make('core', 'plugin_functions');
|
||||
|
||||
// Including both although I doubt that we will find two functions definitions with the same name.
|
||||
@@ -7925,6 +7945,9 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true
|
||||
|
||||
// If the cache is dirty, we should fall through and let it rebuild.
|
||||
if (!$dirty) {
|
||||
if ($migratedtohook && $file === 'lib.php') {
|
||||
$pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
|
||||
}
|
||||
return $pluginfunctions;
|
||||
}
|
||||
}
|
||||
@@ -7972,6 +7995,10 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true
|
||||
$cache->set($key, $pluginfunctions);
|
||||
}
|
||||
|
||||
if ($migratedtohook && $file === 'lib.php') {
|
||||
$pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
|
||||
}
|
||||
|
||||
return $pluginfunctions;
|
||||
|
||||
}
|
||||
@@ -8061,12 +8088,13 @@ function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
|
||||
* @param string $action feature's action
|
||||
* @param array $params parameters of callback function, should be an array
|
||||
* @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
|
||||
* @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
|
||||
* @return mixed
|
||||
*
|
||||
* @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
|
||||
*/
|
||||
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
|
||||
return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
|
||||
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
|
||||
return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8076,9 +8104,10 @@ function plugin_callback($type, $name, $feature, $action, $params = null, $defau
|
||||
* @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
|
||||
* @param array $params parameters of callback function
|
||||
* @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
|
||||
* @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
|
||||
* @return mixed
|
||||
*/
|
||||
function component_callback($component, $function, array $params = array(), $default = null) {
|
||||
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
|
||||
|
||||
$functionname = component_callback_exists($component, $function);
|
||||
|
||||
@@ -8093,6 +8122,19 @@ function component_callback($component, $function, array $params = array(), $def
|
||||
}
|
||||
|
||||
if ($functionname) {
|
||||
if ($migratedtohook) {
|
||||
if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) {
|
||||
if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) {
|
||||
// Do not call the old lib.php callback,
|
||||
// it is there for compatibility with older Moodle versions only.
|
||||
return null;
|
||||
} else {
|
||||
debugging("Callback $function in $component component should be migrated to new hook callback",
|
||||
DEBUG_DEVELOPER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function exists, so just return function result.
|
||||
$ret = call_user_func_array($functionname, $params);
|
||||
if (is_null($ret)) {
|
||||
|
||||
@@ -223,6 +223,10 @@ if (PHPUNIT_UTIL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the hook manager gets initialised before anybody tries to override callbacks,
|
||||
// this is not using caches intentionally to help with development.
|
||||
\core\hook\manager::get_instance();
|
||||
|
||||
// is database and dataroot ready for testing?
|
||||
list($errorcode, $message) = phpunit_util::testing_ready_problem();
|
||||
// print some version info
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 PHP-FIG
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
Event Dispatcher
|
||||
==============
|
||||
|
||||
This repository holds all interfaces related to [PSR-14 (Event Dispatcher)][psr-url].
|
||||
|
||||
Note that this is not an Event Dispatcher implementation of its own. It is merely interfaces that describe the components of an Event Dispatcher.
|
||||
|
||||
You can find [implementations][implementation-url] and [installation instructions][package-url] for the specification on the packagist.
|
||||
|
||||
[psr-url]: https://www.php-fig.org/psr/psr-14/
|
||||
[package-url]: https://packagist.org/packages/psr/event-dispatcher
|
||||
[implementation-url]: https://packagist.org/providers/psr/event-dispatcher-implementation
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "psr/event-dispatcher",
|
||||
"description": "Standard interfaces for event handling.",
|
||||
"type": "library",
|
||||
"keywords": ["psr", "psr-14", "events"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\EventDispatcher\\": "src/"
|
||||
}
|
||||
},
|
||||
"suggest": {
|
||||
"fig/event-dispatcher-util": "Provides some useful PSR-14 utilities"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psr\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Defines a dispatcher for events.
|
||||
*/
|
||||
interface EventDispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Provide all relevant listeners with an event to process.
|
||||
*
|
||||
* @param object $event
|
||||
* The object to process.
|
||||
*
|
||||
* @return object
|
||||
* The Event that was passed, now modified by listeners.
|
||||
*/
|
||||
public function dispatch(object $event);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psr\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Mapper from an event to the listeners that are applicable to that event.
|
||||
*/
|
||||
interface ListenerProviderInterface
|
||||
{
|
||||
/**
|
||||
* @param object $event
|
||||
* An event for which to return the relevant listeners.
|
||||
* @return iterable<callable>
|
||||
* An iterable (array, iterator, or generator) of callables. Each
|
||||
* callable MUST be type-compatible with $event.
|
||||
*/
|
||||
public function getListenersForEvent(object $event) : iterable;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psr\EventDispatcher;
|
||||
|
||||
/**
|
||||
* An Event whose processing may be interrupted when the event has been handled.
|
||||
*
|
||||
* A Dispatcher implementation MUST check to determine if an Event
|
||||
* is marked as stopped after each listener is called. If it is then it should
|
||||
* return immediately without calling any further Listeners.
|
||||
*/
|
||||
interface StoppableEventInterface
|
||||
{
|
||||
/**
|
||||
* Is propagation stopped?
|
||||
*
|
||||
* This will typically only be used by the Dispatcher to determine if the
|
||||
* previous listener halted propagation.
|
||||
*
|
||||
* @return bool
|
||||
* True if the Event is complete and no further listeners should be called.
|
||||
* False to continue calling listeners.
|
||||
*/
|
||||
public function isPropagationStopped() : bool;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace test_plugin;
|
||||
|
||||
/**
|
||||
* Fixture for testing of hooks.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class callbacks {
|
||||
/** @var string[] list of calls */
|
||||
public static $calls = [];
|
||||
|
||||
/**
|
||||
* Callback tester.
|
||||
*
|
||||
* @param \test_plugin\hook\hook $hook
|
||||
* @return void
|
||||
*/
|
||||
public static function test1(\test_plugin\hook\hook $hook): void {
|
||||
self::$calls[] = 'test1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback tester.
|
||||
*
|
||||
* @param \test_plugin\hook\hook $hook
|
||||
* @return void
|
||||
*/
|
||||
public static function test2(\test_plugin\hook\hook $hook): void {
|
||||
self::$calls[] = 'test2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback tester.
|
||||
*
|
||||
* @param \test_plugin\hook\stoppablehook $hook
|
||||
* @return void
|
||||
*/
|
||||
public static function stop1(\test_plugin\hook\stoppablehook $hook): void {
|
||||
self::$calls[] = 'stop1';
|
||||
$hook->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback tester.
|
||||
*
|
||||
* @param \test_plugin\hook\stoppablehook $hook
|
||||
* @return void
|
||||
*/
|
||||
public static function stop2(\test_plugin\hook\stoppablehook $hook): void {
|
||||
self::$calls[] = 'stop2';
|
||||
$hook->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback tester for exceptions.
|
||||
*
|
||||
* @param \test_plugin\hook\hook $hook
|
||||
* @return void
|
||||
*/
|
||||
public static function exception(\test_plugin\hook\hook $hook): void {
|
||||
self::$calls[] = 'exception';
|
||||
throw new \Exception('grrr');
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace test_plugin\hook;
|
||||
|
||||
/**
|
||||
* Fixture for testing of hooks.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class hook implements
|
||||
\core\hook\described_hook,
|
||||
\core\hook\deprecated_callback_replacement {
|
||||
|
||||
/**
|
||||
* Hook description.
|
||||
*/
|
||||
public static function get_hook_description(): string {
|
||||
return 'Test hook 1.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecation info.
|
||||
*/
|
||||
public static function get_deprecated_plugin_callbacks(): array {
|
||||
return ['oldcallback'];
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'priority' => 100,
|
||||
],
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'priority' => 100,
|
||||
],
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'callback' => 'test_plugin\\callbackstest2',
|
||||
'priority' => 100,
|
||||
],
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'callback' => 'test_plugin\\callbacks::exception',
|
||||
'priority' => 1000,
|
||||
],
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'callback' => 'test_plugin\\callbacks::missing',
|
||||
'priority' => 1000,
|
||||
],
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\stoppablehook',
|
||||
'callback' => 'test_plugin\\callbacks::stop1',
|
||||
'priority' => 400,
|
||||
],
|
||||
];
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
],
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\stoppablehook',
|
||||
'callback' => 'test_plugin\\callbacks::stop2',
|
||||
'priority' => 200,
|
||||
],
|
||||
];
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Fixtures for hook testing.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => 'test_plugin\\hook\\hook',
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'priority' => 200,
|
||||
],
|
||||
];
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace test_plugin\hook;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Fixture for testing of hooks.
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class stoppablehook implements
|
||||
StoppableEventInterface,
|
||||
\core\hook\described_hook {
|
||||
|
||||
/** @var bool stoppable flag */
|
||||
private $stopped = false;
|
||||
|
||||
/**
|
||||
* Hook description.
|
||||
*/
|
||||
public static function get_hook_description(): string {
|
||||
return 'Test hook 2.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop other callbacks.
|
||||
*/
|
||||
public function stop(): void {
|
||||
$this->stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if callback propagation should stop.
|
||||
*/
|
||||
public function isPropagationStopped(): bool {
|
||||
return $this->stopped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\hook;
|
||||
|
||||
/**
|
||||
* Hooks tests.
|
||||
*
|
||||
* @coversDefaultClass \core\hook\manager
|
||||
*
|
||||
* @package core
|
||||
* @author Petr Skoda
|
||||
* @copyright 2022 Open LMS
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class manager_test extends \advanced_testcase {
|
||||
/**
|
||||
* Test public factory method to get hook manager.
|
||||
* @covers ::get_instance
|
||||
*/
|
||||
public function test_get_instance() {
|
||||
$manager = manager::get_instance();
|
||||
$this->assertInstanceOf(manager::class, $manager);
|
||||
|
||||
$this->assertSame($manager, manager::get_instance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting of manager test instance.
|
||||
* @covers ::phpunit_get_instance
|
||||
*/
|
||||
public function test_phpunit_get_instance() {
|
||||
$testmanager = manager::phpunit_get_instance([]);
|
||||
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
|
||||
|
||||
// We get a new instance every time.
|
||||
$this->assertNotSame($testmanager, manager::phpunit_get_instance([]));
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reset of test instance.
|
||||
*
|
||||
* NOTE: normal hook manger instance cannot be reset in PHPUnit test
|
||||
* because it may be used to control the test environment itself.
|
||||
*
|
||||
* @covers ::reset_caches
|
||||
* @covers ::init_standard_callbacks
|
||||
*/
|
||||
public function test_reset_caches() {
|
||||
$testmanager = manager::phpunit_get_instance([]);
|
||||
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
|
||||
|
||||
$testmanager->reset_caches();
|
||||
$manager = manager::get_instance();
|
||||
$this->assertSame($manager->get_hooks_with_callbacks(), $testmanager->get_hooks_with_callbacks());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loading and parsing of callbacks from files.
|
||||
*
|
||||
* @covers ::get_callbacks_for_hook
|
||||
* @covers ::get_hooks_with_callbacks
|
||||
* @covers ::load_callbacks
|
||||
* @covers ::add_component_callbacks
|
||||
*/
|
||||
public function test_callbacks() {
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
|
||||
$this->assertCount(2, $callbacks);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'component' => 'test_plugin2',
|
||||
'disabled' => false,
|
||||
'priority' => 200,
|
||||
], $callbacks[0]);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
'component' => 'test_plugin1',
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
], $callbacks[1]);
|
||||
|
||||
$this->assertDebuggingNotCalled();
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_broken.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
|
||||
$debuggings = $this->getDebuggingMessages();
|
||||
$this->resetDebugging();
|
||||
$this->assertSame('Hook callback definition requires \'hook\' name in \'test_plugin1\'',
|
||||
$debuggings[0]->message);
|
||||
$this->assertSame('Hook callback definition requires \'callback\' callable in \'test_plugin1\'',
|
||||
$debuggings[1]->message);
|
||||
$this->assertSame('Hook callback definition contains invalid \'callback\' static class method string in \'test_plugin1\'',
|
||||
$debuggings[2]->message);
|
||||
$this->assertCount(3, $debuggings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test hook dispatching, that is callback execution.
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function test_dispatch(): void {
|
||||
require_once(__DIR__ . '/../fixtures/hook/hook.php');
|
||||
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$hook = new \test_plugin\hook\hook();
|
||||
$result = $testmanager->dispatch($hook);
|
||||
$this->assertSame($hook, $result);
|
||||
$this->assertSame(['test2', 'test1'], \test_plugin\callbacks::$calls);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$this->assertDebuggingNotCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test hook dispatching, that is callback execution.
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function test_dispatch_with_exception(): void {
|
||||
require_once(__DIR__ . '/../fixtures/hook/hook.php');
|
||||
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_exception.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
|
||||
$hook = new \test_plugin\hook\hook();
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('grrr');
|
||||
|
||||
$testmanager->dispatch($hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test hook dispatching, that is callback execution.
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function test_dispatch_with_invalid(): void {
|
||||
// Missing callbacks is ignored.
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_missing.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
|
||||
$hook = new \test_plugin\hook\hook();
|
||||
|
||||
$testmanager->dispatch($hook);
|
||||
$this->assertDebuggingCalled(
|
||||
"Hook callback definition contains invalid 'callback' method name in 'test_plugin1'. Callback method not found.",
|
||||
);
|
||||
$this->assertSame(['test2'], \test_plugin\callbacks::$calls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stoppping of hook dispatching.
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function test_dispatch_stoppable() {
|
||||
require_once(__DIR__ . '/../fixtures/hook/stoppablehook.php');
|
||||
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_stoppable.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_stoppable.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$hook = new \test_plugin\hook\stoppablehook();
|
||||
$result = $testmanager->dispatch($hook);
|
||||
$this->assertSame($hook, $result);
|
||||
$this->assertSame(['stop1'], \test_plugin\callbacks::$calls);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$this->assertDebuggingNotCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deprecated callback lookup.
|
||||
* @covers ::is_deprecated_plugin_callback
|
||||
*/
|
||||
public function testy_is_deprecated_plugin_callback() {
|
||||
require_once(__DIR__ . '/../fixtures/hook/hook.php');
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertTrue($testmanager->is_deprecated_plugin_callback('oldcallback'));
|
||||
$this->assertFalse($testmanager->is_deprecated_plugin_callback('legacycallback'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests callbacks can be overridden via CFG settings.
|
||||
* @covers ::load_callbacks
|
||||
* @covers ::dispatch
|
||||
*/
|
||||
public function test_callback_overriding() {
|
||||
global $CFG;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$componentfiles = [
|
||||
'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
|
||||
'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
|
||||
];
|
||||
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
|
||||
$this->assertCount(2, $callbacks);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'component' => 'test_plugin2',
|
||||
'disabled' => false,
|
||||
'priority' => 200,
|
||||
], $callbacks[0]);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
'component' => 'test_plugin1',
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
], $callbacks[1]);
|
||||
|
||||
$CFG->hooks_callback_overrides = [
|
||||
'test_plugin\\hook\\hook' => [
|
||||
'test_plugin\\callbacks::test2' => ['priority' => 33]
|
||||
]
|
||||
];
|
||||
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
|
||||
$this->assertCount(2, $callbacks);
|
||||
$this->normalise_callbacks($callbacks);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
'component' => 'test_plugin1',
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
], $callbacks[0]);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'component' => 'test_plugin2',
|
||||
'defaultpriority' => 200,
|
||||
'disabled' => false,
|
||||
'priority' => 33,
|
||||
], $callbacks[1]);
|
||||
|
||||
$CFG->hooks_callback_overrides = [
|
||||
'test_plugin\\hook\\hook' => [
|
||||
'test_plugin\\callbacks::test2' => ['priority' => 33, 'disabled' => true]
|
||||
]
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
|
||||
$this->assertCount(2, $callbacks);
|
||||
$this->normalise_callbacks($callbacks);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
'component' => 'test_plugin1',
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
],
|
||||
$callbacks[0]);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'component' => 'test_plugin2',
|
||||
'defaultpriority' => 200,
|
||||
'disabled' => true,
|
||||
'priority' => 33,
|
||||
], $callbacks[1]);
|
||||
|
||||
$CFG->hooks_callback_overrides = [
|
||||
'test_plugin\\hook\\hook' => [
|
||||
'test_plugin\\callbacks::test2' => ['disabled' => true],
|
||||
]
|
||||
];
|
||||
$testmanager = manager::phpunit_get_instance($componentfiles);
|
||||
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
|
||||
$callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
|
||||
$this->assertCount(2, $callbacks);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test2',
|
||||
'component' => 'test_plugin2',
|
||||
'disabled' => true,
|
||||
'priority' => 200,
|
||||
], $callbacks[0]);
|
||||
$this->assertSame([
|
||||
'callback' => 'test_plugin\\callbacks::test1',
|
||||
'component' => 'test_plugin1',
|
||||
'disabled' => false,
|
||||
'priority' => 100,
|
||||
], $callbacks[1]);
|
||||
|
||||
require_once(__DIR__ . '/../fixtures/hook/hook.php');
|
||||
require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
|
||||
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$hook = new \test_plugin\hook\hook();
|
||||
$result = $testmanager->dispatch($hook);
|
||||
$this->assertSame($hook, $result);
|
||||
$this->assertSame(['test1'], \test_plugin\callbacks::$calls);
|
||||
\test_plugin\callbacks::$calls = [];
|
||||
$this->assertDebuggingNotCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the sort order of callbacks to help with asserts.
|
||||
*
|
||||
* @param array $callbacks
|
||||
* @return void
|
||||
*/
|
||||
private function normalise_callbacks(array &$callbacks): void {
|
||||
foreach ($callbacks as &$callback) {
|
||||
ksort($callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,6 +629,14 @@ All rights reserved.</copyright>
|
||||
<license>MIT</license>
|
||||
<repository>https://github.com/php-fig/http-message</repository>
|
||||
</library>
|
||||
<library>
|
||||
<location>psr/event-dispatcher</location>
|
||||
<name>event-dispatcher</name>
|
||||
<description>Provides interfaces that descirbe an event dispatching mechanism.</description>
|
||||
<version>1.0.0</version>
|
||||
<license>MIT</license>
|
||||
<repository>https://github.com/php-fig/event-dispatcher</repository>
|
||||
</library>
|
||||
<library>
|
||||
<location>phpxmlrpc</location>
|
||||
<name>phpxmlrpc</name>
|
||||
|
||||
Reference in New Issue
Block a user