ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK,}\ after_login_completed.phpnu[. namespace core_user\hook; use core\hook\stoppable_trait; /** * Allow plugins to callback as soon possible after user has completed login. * * @package core_user * @copyright 2024 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allow plugins to callback as soon possible after user has completed login.')] #[\core\attribute\tags('user', 'login')] class after_login_completed implements \Psr\EventDispatcher\StoppableEventInterface { use stoppable_trait; } PK,}\Ճbefore_user_updated.phpnu[. namespace core_user\hook; use stdClass; /** * Hook before user information and data updates. * * @package core_user * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions before a user is updated.')] #[\core\attribute\tags('user')] class before_user_updated { /** * Constructor for the hook. * * @param stdClass $user The user instance * @param stdClass $currentuserdata The old user instance */ public function __construct( /** @var stdClass The user instance */ public readonly stdClass $user, /** @var stdClass The old user instance */ public readonly stdClass $currentuserdata, ) { } } PK,}\Wv<>>extend_user_menu.phpnu[. namespace core_user\hook; /** * Hook to modify user menu. * * @package core_user * @copyright 2024 Guillaume Barat * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('user')] #[\core\attribute\label('Allows plugins to add any elements to the user menu')] final class extend_user_menu { /** * Hook to modify user menu. * * @param array $navitems Menu item to add. */ public function __construct( /** @var array The navigation items */ public array $navitems = [], ) { } /** * Add navigation item. * * @param null|\stdClass $output */ public function add_navitem(?\stdClass $output): void { if ($output) { if (property_exists($output, 'itemtype')) { $this->navitems[] = $output; } } } /** * Returns a class with the detail for the menu. * * @return array */ public function get_navitems(): array { return $this->navitems; } } PK,}\v>Yttextend_bulk_user_actions.phpnu[. 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 deprecated_callback_replacement, described_hook { /** * 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 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; } } PK,}\^`before_user_deleted.phpnu[. namespace core_user\hook; use stdClass; use Psr\EventDispatcher\StoppableEventInterface; /** * Hook before user deletion. * * @package core_user * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions before a user is deleted.')] #[\core\attribute\tags('user')] class before_user_deleted implements StoppableEventInterface { use \core\hook\stoppable_trait; /** * Constructor for the hook. * * @param stdClass $user The user instance */ public function __construct( /** @var stdClass The user instance */ public readonly stdClass $user, ) { } } PK,}\3U  extend_default_homepage.phpnu[. declare(strict_types=1); namespace core_user\hook; use core\attribute\{label, tags}; use core\lang_string; use core\url; /** * Hook to allow callbacks to extend the default homepage options * * @package core_user * @copyright 2024 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[label('Allows callbacks to extend the default homepage options')] #[tags('user')] final class extend_default_homepage { /** @var array $options */ private array $options = []; /** * Hook constructor * * @param bool $userpreference Whether this is being selected as a user preference */ public function __construct( /** @var bool $userpreference Whether this is being selected as a user preference */ public readonly bool $userpreference = false, ) { } /** * To be called by callback to add an option * * @param url $url URL that can be used as a site homepage. Must be a local URL. * @param lang_string|string $title */ public function add_option(url $url, lang_string|string $title): void { $this->options[$url->out_as_local_url()] = $title; } /** * Returns all added options * * @return array */ public function get_options(): array { return $this->options; } } PK\Gbefore_file_created_test.phpnu[. namespace core_files\hook; use coding_exception; /** * Tests for before_file_created hook. * * @package core_files * @category test * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_files\hook\before_file_created */ final class before_file_created_test extends \advanced_testcase { public function test_init_with_file_and_content_throws_exception(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Only one of $filepath or $filecontent can be set'); new before_file_created(new \stdClass(), 'path', 'content'); } public function test_init_with_no_file_and_no_content_throws_exception(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Either $filepath or $filecontent must be set'); new before_file_created(new \stdClass()); } public function test_content_updated(): void { $hook = new before_file_created(new \stdClass(), filecontent: 'data'); $this->assertFalse($hook->has_changed()); $this->assertTrue($hook->has_filecontent()); $this->assertFalse($hook->has_filepath()); $hook->update_filecontent('data'); $this->assertEquals('data', $hook->get_filecontent()); $this->assertFalse($hook->has_changed()); $this->assertNull($hook->get_filepath()); $hook->update_filecontent('new data'); $this->assertEquals('new data', $hook->get_filecontent()); $this->assertTrue($hook->has_changed()); $this->assertNull($hook->get_filepath()); } public function test_file_updated(): void { $initialdata = self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'); $newdata = __FILE__; $hook = new before_file_created( new \stdClass(), filepath: $initialdata, ); $this->assertFalse($hook->has_changed()); $this->assertFalse($hook->has_filecontent()); $this->assertTrue($hook->has_filepath()); $hook->update_filepath($initialdata); $this->assertNull($hook->get_filecontent()); $this->assertEquals($initialdata, $hook->get_filepath()); $this->assertFalse($hook->has_changed()); $hook->update_filepath($newdata); $this->assertNull($hook->get_filecontent()); $this->assertEquals($newdata, $hook->get_filepath()); $this->assertTrue($hook->has_changed()); } public function test_cannot_update_file_when_content_set(): void { $hook = new before_file_created(new \stdClass(), filecontent: 'data'); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Cannot update file path when the file path is not set'); $hook->update_filepath('new path'); } public function test_cannot_update_content_when_file_ste(): void { $hook = new before_file_created(new \stdClass(), filepath: __FILE__); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Cannot update file content when the file content is not set'); $hook->update_filecontent('new path'); } } PK\55after_cm_completion_updated.phpnu[. namespace core_completion\hook; /** * Hook after course module creation. * * This hook will be dispatched after a course module is created and events are fired. * * @package core_completion * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after completion is updated.')] #[\core\attribute\tags('completion', 'course')] class after_cm_completion_updated { /** * Constructor for the hook * * @param \cm_info $cm The course module info * @param \stdClass $data completion data */ public function __construct( /** @var \cm_info The course module info */ public readonly \cm_info $cm, /** @var \stdClass completion data */ public readonly \stdClass $data ) { } } PK/\pG after_form_validation.phpnu[. namespace core_course\hook; use course_edit_form; /** * Allows plugins to extend course form validation. * * @see course_edit_form::validation() * * @package core_course * @copyright 2023 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allow plugins to extend a validation of the course editing form')] #[\core\attribute\tags('course')] class after_form_validation { /** * Plugin errors. * * @var array */ protected $errors = []; /** * Creates new hook. * * @param course_edit_form $formwrapper Course form wrapper.. * @param array $data Submitted data. * @param array $files Submitted files. */ public function __construct( /** @var course_edit_form Course form wrapper */ public readonly course_edit_form $formwrapper, /** @var array The submitted data */ private array $data, /** @var array Submitted files */ private array $files = [], ) { } /** * Returns submitted data. * * @return array */ public function get_data(): array { return $this->data; } /** * Returns submitted files. * * @return array */ public function get_files(): array { return $this->files; } /** * Return plugin generated errors. * * @return array */ public function get_errors(): array { return $this->errors; } /** * Plugins implementing a callback can add validation errors. * * @param array $errors Validation errors generated by a plugin. */ public function add_errors(array $errors): void { $this->errors = array_merge($this->errors, $errors); } } PK/\Mafter_course_created.phpnu[. namespace core_course\hook; use stdClass; /** * Hook after course creation. * * This hook will be dispatched after the course is created and events are fired. * * @package core_course * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a course is created.')] #[\core\attribute\tags('course')] class after_course_created { /** * Constructor for the hook. * * @param stdClass $course The course instance. */ public function __construct( /** @var stdClass The course instance */ public readonly stdClass $course, ) { } } PK/\Qbefore_course_deleted.phpnu[. namespace core_course\hook; use stdClass; use Psr\EventDispatcher\StoppableEventInterface; /** * Hook before course deletion. * * @package core_course * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions before a course is deleted.')] #[\core\attribute\tags('course')] class before_course_deleted implements StoppableEventInterface { use \core\hook\stoppable_trait; /** * Constructor for the hook. * * @param stdClass $course The course instance. */ public function __construct( /** @var stdClass The course instance */ public readonly stdClass $course, ) { } } PK/\2Rafter_course_updated.phpnu[. namespace core_course\hook; use stdClass; /** * Hook after course updates. * * @package core_course * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a course is updated.')] #[\core\attribute\tags('course')] class after_course_updated { /** * Constructor for the hook. * * @param stdClass $course The course instance. * @param stdClass $oldcourse The old course instance. * @param bool $changeincoursecat Whether the course category has changed. */ public function __construct( /** @var stdClass The course instance */ public readonly stdClass $course, /** @var stdClass The old course instance */ public readonly stdClass $oldcourse, /** @var bool Whether the course category has changed */ public readonly bool $changeincoursecat = false, ) { } } PK/\>Ubefore_course_viewed.phpnu[. /** * Contains class before_course_viewed responsible for external course routing * * @package core_course * @copyright 2024 Jacob Viertel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_course\hook; use stdClass; use core\hook\described_hook; /** * External course redirect hook before_course_viewed * */ final class before_course_viewed implements described_hook { /** * The course being viewed * @var stdClass */ public $course; /** * Constructor for the hook. * @param stdClass $course The course instance. */ public function __construct(stdClass $course) { $this->course = $course; } /** * Hooks description * * @return string */ public static function get_hook_description(): string { return 'Hook dispatched just before viewing a course in course/view.php.'; } /** * Hooks tags * * @return array */ public static function get_hook_tags(): array { return ['course', 'view', 'routing', 'navigation']; } } PK/\2n )before_activitychooserbutton_exported.phpnu[. namespace core_course\hook; use cm_info; use section_info; use core\hook\described_hook; use core_course\output\activitychooserbutton; /** * Hook before activity chooser button export. * * @package core_course * @copyright 2024 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class before_activitychooserbutton_exported implements described_hook { /** * Constructor. * * @param activitychooserbutton $activitychooserbutton the activity chooser button output * @param section_info $section the course section * @param cm_info|null $cm the course module */ public function __construct( /** @var activitychooserbutton the activity chooser button output */ protected activitychooserbutton $activitychooserbutton, /** @var section_info the course section */ protected section_info $section, /** @var cm_info|null the course module */ protected ?cm_info $cm = null, ) { } /** * Describes the hook purpose. * * @return string */ public static function get_hook_description(): string { return 'This hook is triggered when a activity chooser button is exported.'; } /** * List of tags that describe this hook. * * @return string[] */ public static function get_hook_tags(): array { return ['course']; } /** * Get activitychooserbutton output instance. * * @return activitychooserbutton */ public function get_activitychooserbutton(): activitychooserbutton { return $this->activitychooserbutton; } /** * Get course section instance. * * @return section_info */ public function get_section(): section_info { return $this->section; } /** * Get course module instance. * * @return cm_info|null */ public function get_cm(): ?cm_info { return $this->cm; } } PK/\a`after_form_definition.phpnu[. namespace core_course\hook; use core\hook\described_hook; use course_edit_form; use MoodleQuickForm; /** * Allows plugins to extend course form definition and add/remove/update form elements. * * @see course_edit_form::definition() * * @package core_course * @copyright 2023 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins to extend course editing form')] #[\core\attribute\tags('course')] class after_form_definition { /** * Creates new hook. * * @param course_edit_form $formwrapper Course form wrapper. * @param MoodleQuickForm $mform Form to be extended. */ public function __construct( /** @var course_edit_form The form wrapper for the edit form */ public readonly course_edit_form $formwrapper, /** @var MoodlequickForm The form to be extended */ public readonly MoodleQuickForm $mform, ) { } } PK/\(4~after_form_submission.phpnu[. namespace core_course\hook; /** * Allows plugins to extend course form submission. * * @see create_course() * @see update_course() * * @package core_course * @copyright 2023 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins to extend saving of the course editing form')] #[\core\attribute\tags('course')] class after_form_submission { /** * Creates new hook. * * @param \stdClass $data Submitted data * @param bool $isnewcourse Whether this is a new course */ public function __construct( /** @var \stdClass The submitted data */ protected \stdClass $data, /** @var bool Whether this is a new course */ public readonly bool $isnewcourse = false, ) { } /** * Returns submitted data. * * @return \stdClass */ public function get_data(): \stdClass { return $this->data; } } PK/\1$after_form_definition_after_data.phpnu[. namespace core_course\hook; use course_edit_form; use MoodleQuickForm; /** * Allows plugins to extend course form after data is set. * * @see course_edit_form::definition_after_data() * * @package core_course * @copyright 2023 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins to extend course editing form after data is set')] #[\core\attribute\tags('course')] class after_form_definition_after_data { /** * Creates new hook. * * @param course_edit_form $formwrapper Course form wrapper.. * @param MoodleQuickForm $mform Form to be extended. */ public function __construct( /** @var course_edit_form The form wrapper for the edit form */ public readonly course_edit_form $formwrapper, /** @var MoodlequickForm The form to be extended */ public readonly MoodleQuickForm $mform, ) { } } PKs\Bnavigation/secondary_extend.phpnu[. namespace core\hook\navigation; use core\hook\stoppable_trait; use core\navigation\views\secondary; /** * Allows plugins to insert nodes into site secondary navigation * * @package core * @author Sumaiya Javed * @copyright 2024 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins to insert nodes into site secondary navigation')] #[\core\attribute\tags('navigation')] final class secondary_extend { use stoppable_trait; /** * Creates new hook. * * @param secondary $secondaryview secondary navigation view */ public function __construct( /** * @var secondary $secondaryview secondary navigation view */ public readonly secondary $secondaryview, ) { } /** * secondary navigation view * * @return secondary */ public function get_secondaryview(): secondary { return $this->secondaryview; } } PKs\Q`77navigation/primary_extend.phpnu[. namespace core\hook\navigation; use core\hook\stoppable_trait; use core\navigation\views\primary; /** * Allows plugins to insert nodes into site primary navigation * * @package core * @copyright 2023 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins to insert nodes into site primary navigation')] #[\core\attribute\tags('navigation')] class primary_extend implements \Psr\EventDispatcher\StoppableEventInterface { use stoppable_trait; /** * Creates new hook. * * @param primary $primaryview Primary navigation view */ public function __construct( public readonly primary $primaryview, ) { } /** * Primary navigation view * * @return primary */ public function get_primaryview(): primary { return $this->primaryview; } } PKs\ stoppable_trait.phpnu[. namespace core\hook; /** * Moodle specific implementation of * \Psr\EventDispatcher\StoppableEventInterface * interface from PSR-14. * * @package core * @author Petr Skoda * @copyright 2023 Open LMS * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait stoppable_trait { /** @var bool $propagationstopped propagation is stopped */ private $propagationstopped = false; /** * Allows plugins to notify the hook dispatcher that hook propagation should be stopped */ public function stop_propagation(): void { $this->propagationstopped = true; } /** * 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 { return $this->propagationstopped; } } PKs\T4T T di_configuration.phpnu[. namespace core\hook; use DI\ContainerBuilder; use DI\Definition; use core\attribute\label; /** * Allow for init-time configuration of the Dependency Injection container. * * @package core * @copyright 2023 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[label('A hook to allow per-component configuration of the DI container.')] class di_configuration { /** * Create the Dependency Injection configuration hook instance. * * @param ContainerBuilder $builder */ public function __construct( /** @var ContainerBuilder The PHP-DI Builder */ protected ContainerBuilder $builder, ) { } /** * Add a definition to the Dependency Injection container. * * A definition is a callable that returns an instance of the service. * * The callable can take arguments which are resolved using the DI container, for example a definition for the * following example service requires \moodle_database, and \core\formatting which will be resolved using the DI * container. * * * $hook->add_definition( * id: \mod\example\service::class, * definition: function ( * \moodle_database $db, * \core\formatting $formatter, * ): \mod\example\service { * return new \mod\example\service( * $database, * $formatter, * $some, * $other, * $args, * )' * }, * ); * * * @param string $id The identifier of the container entry * @param callable|Definition\Definition|Definition\SelfResolvingDefinition|Definition\Helper\DefinitionHelper $definition * The definition of the container entry * @return self * @example */ public function add_definition( string $id, callable|Definition\Definition|Definition\SelfResolvingDefinition|Definition\Helper\DefinitionHelper $definition, ): self { $this->builder->addDefinitions([ $id => $definition, ]); return $this; } } PKs\ff manager.phpnu[. namespace core\hook; use core\attribute_helper; 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 = []; /** @var array list of redirected callbacks in PHPUnit tests */ private $redirectedcallbacks = []; /** * Constructor can be used only from factory methods. * * @param bool $phpunit Whether this is a PHPUnit instantiated instance */ private function __construct( /** @var bool Whether this is a PHPUnit instantiated instance */ private bool $phpunit = false, ) { } /** * 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. * * Please note that the result of this method should typically be passed to \core\di::set(). * * @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( phpunit: true, ); $instance->load_callbacks($componentfiles); return $instance; } /** * Whether to warn when an unmigrated legacy hook is found. * * @return bool */ public function warn_on_unmigrated_legacy_hooks(): bool { if ($this->phpunit) { // This is an empty instance used in PHPUnit tests. // It does not know of any migrated hooks. return false; } return true; } /** * Override hook callbacks for testing purposes. * * @param string $hookname * @param callable $callback */ public function phpunit_redirect_hook(string $hookname, callable $callback): void { if (!PHPUNIT_TEST) { throw new \coding_exception('Invalid call of manager::phpunit_redirect_hook() outside of tests'); } $this->redirectedcallbacks[$hookname] = $callback; } /** * Cancel all redirections of hook callbacks. */ public function phpunit_stop_redirections(): void { if (!PHPUNIT_TEST) { throw new \coding_exception('Invalid call of manager::phpunit_stop_redirections() outside of tests'); } $this->redirectedcallbacks = []; } /** * 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. * * @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 * 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 { // phpcs:ignore // 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; } } } /** * Get the list of callbacks that the given hook class replaces (if any). * * @param string $hookclassname * @return array */ public static function get_replaced_callbacks(string $hookclassname): array { if (!class_exists($hookclassname)) { return []; } if (is_subclass_of($hookclassname, \core\hook\deprecated_callback_replacement::class)) { return $hookclassname::get_deprecated_plugin_callbacks(); } if ($replaces = attribute_helper::instance($hookclassname, \core\attribute\hook\replaces_callbacks::class)) { return $replaces->callbacks; } return []; } /** * 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 { // It is only safe to dispatch hooks after early setup is complete. // This includes, but is not limited to: // - configuring exception handlers // - configuring autoloaders // - configuring date/time. // The database connection is not required. It is up to individual hooks to understand their context. // At this time there is no way to check that setup is complete, but we can check that 'setup.php' has been included. if (!function_exists('setup_DB')) { debugging('Hooks cannot be dispatched yet', DEBUG_DEVELOPER); return $event; } if (PHPUNIT_TEST) { $hookclassname = get_class($event); if (isset($this->redirectedcallbacks[$hookclassname])) { call_user_func($this->redirectedcallbacks[$hookclassname], $event); 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. */ private function init_standard_callbacks(): void { global $CFG; $this->allcallbacks = []; $this->alldeprecations = []; // @codeCoverageIgnoreStart $shouldcache = $this->should_cache(); if ($shouldcache) { $cache = $this->get_cache(); $callbacks = $cache['callbacks'] ?? null; $deprecations = $cache['deprecations'] ?? null; $overrideshash = $cache['overrideshash'] ?? null; $usecache = is_array($callbacks); $usecache = $usecache && is_array($deprecations); $usecache = $usecache && $this->calculate_overrides_hash() === $overrideshash; if ($usecache) { $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); // @codeCoverageIgnoreStart if ($shouldcache) { $this->set_cache( $this->allcallbacks, $this->alldeprecations, $this->calculate_overrides_hash(), ); } // @codeCoverageIgnoreEnd } /** * Load callbacks from component db/hooks.php files. * * @param array $componentfiles list of all components with their callback files */ 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); } } } } /** * Calculate a hash of the overrides. * This is used to inform if the overrides have changed, which invalidates the cache. * * Overrides are only configured in config.php where there is no other mechanism to invalidate the cache. * * @return null|string */ private function calculate_overrides_hash(): ?string { global $CFG; if (!property_exists($CFG, 'hooks_callback_overrides')) { return null; } if (!is_iterable($CFG->hooks_callback_overrides)) { return null; } return sha1(json_encode($CFG->hooks_callback_overrides)); } /** * 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(); foreach (array_keys($candidates) as $hookclassname) { foreach (self::get_replaced_callbacks($hookclassname) as $replacedcallback) { $this->alldeprecations[$replacedcallback][] = $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 */ 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_array($classmethod)) { if (count($classmethod) !== 2) { debugging("Hook callback definition contains invalid 'callback' array in '$component'", DEBUG_DEVELOPER); return null; } $classmethod = implode('::', $classmethod); } 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 * @deprecated in favour of get_hooks_deprecating_plugin_callback since Moodle 4.4. * @todo Remove in Moodle 4.8 (MDL-80327). */ public function is_deprecated_plugin_callback(string $plugincallback): bool { debugging( 'is_deprecated_plugin_callback method is deprecated, use get_hooks_deprecating_plugin_callback instead.', DEBUG_DEVELOPER, ); return (bool)$this->get_hooks_deprecating_plugin_callback($plugincallback); } /** * If the plugin callback from lib.php is deprecated by any hooks, return the hooks' classnames. * * @param string $plugincallback short callback name without the component prefix * @return ?array */ public function get_hooks_deprecating_plugin_callback(string $plugincallback): ?array { return $this->alldeprecations[$plugincallback] ?? null; } /** * 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 hook namespaces or discovery agents. * * The hooks overview page includes also all other classes that are * referenced in callback registrations in db/hooks.php files, those * are not included here. * * @return array hook class names */ public static function discover_known_hooks(): array { // All classes in hook namespace of core and plugins, unless plugin has a discovery agent. $hooks = \core\hooks::discover_hooks(); // Look for hooks classes in all plugins that implement discovery agent interface. foreach (\core_component::get_component_names() as $component) { $classname = "{$component}\\hooks"; if (!class_exists($classname)) { continue; } if (!is_subclass_of($classname, discovery_agent::class)) { continue; } $hooks = array_merge($hooks, $classname::discover_hooks()); } return $hooks; } /** * Get the path to the hook cache. * * @return string */ protected function get_cache_path(): string { return make_localcache_directory('') . '/hookcallbacks.json'; } /** * Whether we should enable caching of hook data. * * The cache is disabled during unit tests, when CACHE_DISABLE_ALL is set, and during upgrades. * * @return bool */ protected function should_cache(): bool { if (PHPUNIT_TEST) { return false; } if (CACHE_DISABLE_ALL) { return false; } if ($this->is_upgrade_running()) { // Do not use the cache during upgrade. return false; } return true; } /** * Fetch and decode the hook cache. * * @return array|null */ protected function get_cache(): ?array { $cachepath = $this->get_cache_path(); if (!file_exists($cachepath)) { return null; } return json_decode(file_get_contents($cachepath), true) ?? []; } /** * Store all relevant data in the cache. * * @param array $callbacks * @param array $deprecations * @param string|null $hash */ protected function set_cache( array $callbacks, array $deprecations, ?string $hash, ): void { $cachedata = [ 'callbacks' => $callbacks, 'deprecations' => $deprecations, 'overrideshash' => $hash, ]; // Write to a temp file and rename it to ensure atomicity of reads. // If we write directly to the cache file, another process may read it during the write and get corrupted data. $cachepath = $this->get_cache_path(); $tmppath = "{$cachepath}." . uniqid('tmp', true); file_put_contents($tmppath, json_encode($cachedata)); rename($tmppath, $cachepath); clearstatcache(true, $cachepath); } /** * Check whether upgrade is currently running. * * @return bool */ protected function is_upgrade_running(): bool { global $CFG; // Note: This mimics the test in lib/setuplib.php during upgrade. return !empty($CFG->upgraderunning); } } PKs\{b/access/after_role_assigned.phpnu[. namespace core\hook\access; use context; /** * Hook after a role is assigned. * * @package core * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a role is assigned to a user.')] #[\core\attribute\tags('role', 'user')] class after_role_assigned { /** * Constructor for the hook. * * @param context $context The context of the role assignment. * @param int $userid The user id of the user. */ public function __construct( /** @var context The context of the role assignment */ public readonly context $context, /** @var int The user id of the user */ public readonly int $userid, ) { } } PKs\L access/after_role_unassigned.phpnu[. namespace core\hook\access; use context; /** * Hook after a role is unassigned. * * @package core * @copyright 2024 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after a role is unassigned for a user.')] #[\core\attribute\tags('role', 'user')] class after_role_unassigned { /** * Constructor for the hook. * * @param context $context The context of the role assignment. * @param int $userid The user id of the user. * */ public function __construct( /** @var context The context of the role assignment */ public readonly context $context, /** @var int The user id of the user */ public readonly int $userid, ) { } } PKs\aWaccess/after_role_switched.phpnu[. namespace core\hook\access; use context; /** * Hook after we switch user role. * * @package core * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allows plugins or features to perform actions after switching roles.')] #[\core\attribute\tags('role', 'user')] class after_role_switched { /** * Constructor for the hook. * * @param context $context The context of the role assignment. * @param int $roleid The new role id to switch to. */ public function __construct( /** @var context The context of the role assignment */ public readonly context $context, /** @var int The new role id to switch to */ public readonly int $roleid, ) { } } PKs\\ 1output/before_standard_footer_html_generation.phpnu[. namespace core\hook\output; use renderer_base; /** * Hook to allow subscribers to add HTML content to the footer. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add any elements to the page footer.')] #[\core\attribute\hook\replaces_callbacks('standard_footer_html')] final class before_standard_footer_html_generation { /** * Hook to allow subscribers to add HTML content before the footer. * * @param renderer_base $renderer * @param string $output Initial output */ public function __construct( /** @var renderer_base The page renderer object */ public readonly renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML to the top of the body. * * Must be a string containing valid html head content. * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } /** * Process legacy callbacks. * * Legacy callback 'standard_footer_html' is deprecated since Moodle 4.4 */ public function process_legacy_callbacks(): void { // Give plugins an opportunity to add any footer elements. // The callback must always return a string containing valid html footer content. $pluginswithfunction = get_plugins_with_function(function: 'standard_footer_html', migratedtohook: true); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $this->add_html($function()); } } } } PKs\k output/after_http_headers.phpnu[. namespace core\hook\output; /** * Class after_http_headers * * @package core * @copyright 2024 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to make changes after headers are sent')] class after_http_headers { /** * Hook to allow subscribers to modify the process after headers are sent. * * @param \renderer_base $renderer * @param string $output */ public function __construct( /** @var \renderer_base The page renderer object */ public readonly \renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML after headers content. * * Must be a string containing valid html content. * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } } PKs\ !output/before_html_attributes.phpnu[. namespace core\hook\output; /** * Class before_html_attributes * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object * @property array $attributes The list of HTML attributes to be added to the tag. */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add, remove or modify any attributes of the html tag.')] #[\core\attribute\hook\replaces_callbacks('add_htmlattributes')] final class before_html_attributes { /** * Constructor for the before_html_attributes hook. * * @param \renderer_base $renderer The page renderer object * @param array $attributes The list of HTML attributes initially on the tag */ public function __construct( /** @var \renderer_base The page renderer */ public readonly \renderer_base $renderer, /** @var array The list of HTML attributes initially on the tag */ private array $attributes = [], ) { } /** * Add an HTML attribute to the list. * * @param string $name * @param string $value */ public function add_attribute(string $name, string $value): void { $this->attributes[$name] = $value; } /** * Get the list of attributes. * * @return array */ public function get_attributes(): array { return $this->attributes; } /** * Remove an HTML attribute from the list. * * @param string $name */ public function remove_attribute(string $name): void { unset($this->attributes[$name]); } /** * Process legacy callbacks. */ public function process_legacy_callbacks(): void { // Legacy callback 'add_htmlattributes' is deprecated since Moodle 4.4. // This function should return an array of html attribute names => values. $pluginswithfunction = get_plugins_with_function( function: 'add_htmlattributes', migratedtohook: true, ); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $newattrs = $function(); unset($newattrs['dir']); unset($newattrs['lang']); unset($newattrs['xmlns']); unset($newattrs['xml:lang']); foreach ($newattrs as $name => $value) { $this->add_attribute($name, $value); } } } } } PKs\on n (output/before_footer_html_generation.phpnu[. namespace core\hook\output; /** * Hook to allow subscribers to add HTML content to the footer. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add any elements to the footer before JS is finalized')] #[\core\attribute\hook\replaces_callbacks('before_footer')] final class before_footer_html_generation { /** * Hook to allow subscribers to add HTML content to the footer. * * @param \renderer_base $renderer * @param string $output Initial output */ public function __construct( /** @var \renderer_base The page renderer object */ public readonly \renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML to the top of the body. * * Must be a string containing valid html head content. * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } /** * Process legacy callbacks. */ public function process_legacy_callbacks(): void { $pluginswithfunction = get_plugins_with_function(function: 'before_footer', migratedtohook: true); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $extrafooter = $function(); if (is_string($extrafooter)) { $this->add_html($extrafooter); } } } } } PKs\tv)EEoutput/extend_url.phpnu[. namespace core\hook\output; use moodle_url; /** * Allow augmentation of the moodle URL. * * The hook carries a mutable moodle_url. Callbacks can add query params. * * @package core * @copyright 2025 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Allow augmentation of the moodle URL')] #[\core\attribute\tags('output')] final class extend_url { /** * Create a new instance of the hook. * * @param moodle_url $url The url of the page */ public function __construct( /** @var moodle_url The url of the page */ private moodle_url $url, ) { } /** * Get the URL being modified. * * @return moodle_url */ public function get_url(): moodle_url { return $this->url; } /** * Add a query parameter to the URL. * * @param string $name The name of the parameter * @param string $value The value of the parameter */ public function add_param( string $name, string $value, ): void { $this->url->param($name, $value); } } PKs\b)Z3N N /output/before_standard_head_html_generation.phpnu[. namespace core\hook\output; /** * Allows plugins to add any elements to the page html tag * * @package core * @copyright 2023 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add any elements to the page <head> html tag.')] #[\core\attribute\hook\replaces_callbacks('before_standard_html_head')] final class before_standard_head_html_generation { /** * Hook to allow subscribers to add HTML content to page head tag. * * @param renderer_base $renderer * @param string $output Initial output */ public function __construct( /** @var \renderer_base The core_renderer instance used for the generation */ public readonly \renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML to the page. * * Must be a string containing valid html head content * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } /** * Process legacy callbacks. * * Legacy callback 'before_standard_html_head' is deprecated since Moodle 4.4 */ public function process_legacy_callbacks(): void { $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php', true, true); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $output = $function(); $this->add_html((string)$output); } } } } PKs\x-4GGoutput/before_http_headers.phpnu[. namespace core\hook\output; /** * Class before_http_headers * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to make changes before headers are sent')] #[\core\attribute\hook\replaces_callbacks('before_http_headers')] class before_http_headers { /** * Hook to allow subscribers to modify the process before headers are sent. * * @param \renderer_base $renderer */ public function __construct( /** @var \renderer_base The page renderer object */ public readonly \renderer_base $renderer, ) { } /** * Process legacy callbacks. */ public function process_legacy_callbacks(): void { $pluginswithfunction = get_plugins_with_function(function: 'before_http_headers', migratedtohook: true); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $function(); } } } } PKs\ql 5output/after_standard_main_region_html_generation.phpnu[. namespace core\hook\output; /** * Hook to allow subscribers to add HTML content after the main region content has been generated. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add any elements to the footer before JS is finalized')] #[\core\attribute\hook\replaces_callbacks('standard_after_main_region_html')] final class after_standard_main_region_html_generation { /** * Hook to allow subscribers to add HTML content after the main region content has been generated. * * @param renderer_base $renderer * @param string $output Initial output */ public function __construct( /** @var \renderer_base The page renderer object */ public readonly \renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML to the top of the body. * * Must be a string containing valid html head content. * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } /** * Process legacy callbacks. */ public function process_legacy_callbacks(): void { $pluginswithfunction = get_plugins_with_function(function: 'standard_after_main_region_html', migratedtohook: true); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $extrafooter = $function(); if (is_string($extrafooter)) { $this->add_html($extrafooter); } } } } } PKs\7= 6output/before_standard_top_of_body_html_generation.phpnu[. namespace core\hook\output; /** * Hook to allow subscribers to add HTML content to the top of the page body. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \renderer_base $renderer The page renderer object */ #[\core\attribute\tags('output')] #[\core\attribute\label('Allows plugins to add any elements to the page <head> html tag.')] #[\core\attribute\hook\replaces_callbacks('before_standard_top_of_body_html')] final class before_standard_top_of_body_html_generation { /** * Hook to allow subscribers to add HTML content to the top of the page body. * * @param \renderer_base $renderer * @param string $output Initial output */ public function __construct( /** @var \renderer_base The page renderer object */ public readonly \renderer_base $renderer, /** @var string The collected output */ private string $output = '', ) { } /** * Plugins implementing callback can add any HTML to the top of the body. * * Must be a string containing valid html head content. * * @param null|string $output */ public function add_html(?string $output): void { if ($output) { $this->output .= $output; } } /** * Returns all HTML added by the plugins * * @return string */ public function get_output(): string { return $this->output; } /** * Process legacy callbacks. * * Legacy callback 'before_standard_top_of_body_html' is deprecated since Moodle 4.4 */ public function process_legacy_callbacks(): void { // Give subsystems an opportunity to inject extra html content. The callback // must always return a string containing valid html. foreach (\core_component::get_core_subsystems() as $name => $path) { if ($path) { $this->add_html( component_callback( component: $name, function: 'before_standard_top_of_body_html', default: '', migratedtohook: true, ), ); } } // Give plugins an opportunity to inject extra html content. The callback // must always return a string containing valid html. $pluginswithfunction = get_plugins_with_function( function: 'before_standard_top_of_body_html', migratedtohook: true, ); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { $this->add_html($function() ?? ''); } } } } PKs\described_hook.phpnu[. 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 { /** * 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; /** * List of tags that describe this hook. * * @return string[] */ public static function get_hook_tags(): array; } PKs\after_config.phpnu[. namespace core\hook; /** * Class after_config * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\tags('configuration', 'core')] #[\core\attribute\label('Allows plugins to perform actions immediately after configuration')] #[\core\attribute\hook\replaces_callbacks('after_config')] class after_config { /** * Process legacy callbacks. */ public function process_legacy_callbacks(): void { $pluginswithfunction = get_plugins_with_function( function: 'after_config', migratedtohook: true, ); foreach ($pluginswithfunction as $plugins) { foreach ($plugins as $function) { try { $function(); } catch (\Throwable $e) { debugging("Exception calling '$function'", DEBUG_DEVELOPER, $e->getTrace()); } } } } } PKs\fXH##$task/after_failed_task_max_delay.phpnu[. namespace core\hook\task; use core\hook\described_hook; use core\task\task_base; /** * Hook to allow plugins to get information when a task has reached the maximum fail delay in adhoc and scheduled task * * @package core * @copyright 2024 Raquel Ortega * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\tags('task')] #[\core\attribute\label('Allow plugins to get information when a task reaches its maximum failure delay.')] final class after_failed_task_max_delay { /** * Constructor. * @param task_base $task failed task. */ public function __construct( protected task_base $task, ) { } /** * Get the task object. * * @return task_base Task object. */ public function get_task(): task_base { return $this->task; } } PKs\/K#deprecated_callback_replacement.phpnu[. namespace core\hook; /** * Interface for describing of lib.php callbacks that were deprecated by the hook. * * Please note that, from Moodle 4.4, you can instead use the \core\attribute\hook\replaces_callback attribute. * * @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; } PKs\EpTdiscovery_agent.phpnu[. namespace core\hook; /** * This interface describes a class which can discover all hook * classes of a plugin. * * To add new discovery agent in your plugin you need to add your_plugin\hooks * class that implements this interface. * * @package core * @copyright Andrew Lyons * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface discovery_agent { /** * Returns a list of hooks for component. * * @return array */ public static function discover_hooks(): array; } PK\Ed7CJJ"navigation/primary_extend_test.phpnu[. namespace core\hook\navigation; /** * Test hook for primary navigation. * * @coversDefaultClass \core\hook\navigation\primary_extend * * @package core * @author Petr Skoda * @copyright 2023 Open LMS * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class primary_extend_test extends \advanced_testcase { /** * Test stoppable_trait. * @covers ::stop_propagation */ public function test_stop_propagation(): void { global $PAGE; $this->resetAfterTest(); $PAGE = new \moodle_page(); $PAGE->set_url('/'); $primarynav = new \core\navigation\views\primary($PAGE); $hook = new primary_extend($primarynav); $this->assertInstanceOf('Psr\EventDispatcher\StoppableEventInterface', $hook); $this->assertFalse($hook->isPropagationStopped()); $hook->stop_propagation(); $this->assertTrue($hook->isPropagationStopped()); } /** * Test hook is triggered when initialising primary navigation menu. * @covers \core\navigation\views\primary::initialise */ public function test_trigggering(): void { global $PAGE; $this->resetAfterTest(); $PAGE = new \moodle_page(); $PAGE->set_url('/'); $count = 0; $receivedhook = null; $testcallback = function(primary_extend $hook) use (&$receivedhook, &$count): void { $count++; $receivedhook = $hook; }; $this->redirectHook(primary_extend::class, $testcallback); $primarynav = new \core\navigation\views\primary($PAGE); $this->assertSame(0, $count); $this->assertNull($receivedhook); $primarynav->initialise(); $this->assertSame(1, $count); $this->assertInstanceOf(primary_extend::class, $receivedhook); } /** * Verify that nothing except this hook modifies the primary menu. * @covers \core\navigation\views\primary::initialise */ public function test_unsupported_hacks(): void { global $PAGE; $this->resetAfterTest(); $PAGE = new \moodle_page(); $PAGE->set_url('/'); $testcallback = function(primary_extend $hook): void { // Nothing to do, propagation is stopped by hook redirection. }; $this->redirectHook(primary_extend::class, $testcallback); $primarynav = new \core\navigation\views\primary($PAGE); $primarynav->initialise(); $this->assertSame(['home'], $primarynav->get_children_key_list(), 'Unsupported primary menu modification detected, use new primary_extend hook instead.'); $this->setAdminUser(); $primarynav = new \core\navigation\views\primary($PAGE); $primarynav->initialise(); $this->assertSame(['home', 'myhome', 'mycourses'], $primarynav->get_children_key_list(), 'Unsupported primary menu modification detected, use new primary_extend hook instead.'); } /** * Test adding of primary menu items via hook. * @covers \core\navigation\views\primary::initialise */ public function test_primary_menu_extending(): void { global $PAGE; $this->resetAfterTest(); $PAGE = new \moodle_page(); $PAGE->set_url('/'); $testcallback = function(primary_extend $hook): void { $primaryview = $hook->get_primaryview(); $primaryview->add('Pokus', null); }; $this->redirectHook(primary_extend::class, $testcallback); $primarynav = new \core\navigation\views\primary($PAGE); $primarynav->initialise(); $keys = $primarynav->get_children_key_list(); $this->assertCount(2, $keys); $firstkey = array_shift($keys); $this->assertSame('home', $firstkey); $secondkey = array_shift($keys); /** @var \navigation_node $pokus */ $pokus = $primarynav->get($secondkey); $this->assertInstanceOf(\navigation_node::class, $pokus); $this->assertSame('Pokus', $pokus->text); } /** * Test replacing of the whole primary menu. * @covers \core\navigation\views\primary::initialise */ public function test_primary_menu_replacing(): void { global $PAGE; $this->resetAfterTest(); $PAGE = new \moodle_page(); $PAGE->set_url('/'); $testcallback = function(primary_extend $hook): void { $primaryview = $hook->get_primaryview(); $keys = $primaryview->get_children_key_list(); foreach ($keys as $key) { $item = $primaryview->get($key); $item->remove(); } $primaryview->add('Pokus', null); // Technically we do not need to stop because observers are overridden, // but this can be used as an example for plugin that wants to stop // adding of primary menu items from plugins. $hook->stop_propagation(); }; $this->redirectHook(primary_extend::class, $testcallback); $primarynav = new \core\navigation\views\primary($PAGE); $primarynav->initialise(); $keys = $primarynav->get_children_key_list(); $this->assertCount(1, $keys); $firstkey = array_shift($keys); /** @var \navigation_node $pokus */ $pokus = $primarynav->get($firstkey); $this->assertInstanceOf(\navigation_node::class, $pokus); $this->assertSame('Pokus', $pokus->text); } } PK\N$ $ before_course_viewed_test.phpnu[. namespace core\hook; /** * Test hook for external routing * * @coversDefaultClass \core_course\hook\before_course_viewed * * @package core * @copyright 2024 Jacob Viertel * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class before_course_viewed_test extends \advanced_testcase { /** * Test hook description. * @covers ::get_hook_description */ public function test_hook_description(): void { $description = \core_course\hook\before_course_viewed::get_hook_description(); $this->assertIsString($description); $this->assertSame('Hook dispatched just before viewing a course in course/view.php.', $description); } /** * Test hook tags. * @covers ::get_hook_tags */ public function test_hook_tags(): void { $tags = \core_course\hook\before_course_viewed::get_hook_tags(); $this->assertIsArray($tags); $this->assertContains('course', $tags); $this->assertContains('view', $tags); $this->assertContains('routing', $tags); $this->assertContains('navigation', $tags); } /** * Test hook initialization with course data. * @covers ::__construct */ public function test_hook_initialization(): void { $course = new \stdClass(); $course->id = 1; $course->fullname = 'Test Course'; $hook = new \core_course\hook\before_course_viewed($course); $this->assertInstanceOf(\core_course\hook\before_course_viewed::class, $hook); $this->assertSame($course, $hook->course); } /** * Test hook dispatch and propagation. * @covers \core\hook\manager::dispatch */ public function test_hook_dispatch(): void { $course = new \stdClass(); $course->id = 1; $course->fullname = 'Test Course'; $hook = new \core_course\hook\before_course_viewed($course); $count = 0; $receivedhook = null; $testcallback = function (\core_course\hook\before_course_viewed $hook) use (&$receivedhook, &$count): void { $count++; $receivedhook = $hook; }; $this->redirectHook(\core_course\hook\before_course_viewed::class, $testcallback); \core\hook\manager::get_instance()->dispatch($hook); $this->assertSame(1, $count); $this->assertSame($hook, $receivedhook); } } PK\S).Z.Zmanager_test.phpnu[. namespace core\hook; use core\di; use core\tests\fake_plugins_test_trait; /** * Hooks tests. * * @package core * @author Petr Skoda * @copyright 2022 Open LMS * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core\hook\manager */ final class manager_test extends \advanced_testcase { use fake_plugins_test_trait; /** * Test public factory method to get hook manager. */ public function test_get_instance(): void { $manager = manager::get_instance(); $this->assertInstanceOf(manager::class, $manager); $this->assertSame($manager, manager::get_instance()); } /** * Test getting of manager test instance. */ public function test_phpunit_get_instance(): void { $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 loading and parsing of callbacks from files. */ public function test_callbacks(): void { $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. */ 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. */ 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. */ public function test_dispatch_with_invalid(): void { require_once(__DIR__ . '/../fixtures/hook/hook.php'); require_once(__DIR__ . '/../fixtures/hook/callbacks.php'); // 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. */ public function test_dispatch_stoppable(): void { 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(); } /** * Tests callbacks can be overridden via CFG settings. */ public function test_callback_overriding(): void { 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(); $CFG->hooks_callback_overrides = []; } /** * Register a fake plugin called hooktest in the component manager. * * Tests consuming this helpers must run in a separate process. */ protected function setup_hooktest_plugin(): void { global $CFG; $this->add_mocked_plugintype('fake', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins"); $this->add_mocked_plugin('fake', 'hooktest', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins/hooktest"); } /** * Call a plugin callback that has been replaced by a hook, but has no hook callback. * * The original callback should be called, but a debugging message should be output. * * @runInSeparateProcess */ public function test_migrated_callback(): void { $this->resetAfterTest(true); // Include plugin hook discovery agent, and the hook that replaces the callback. require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php'); // Register the fake plugin with the component manager. $this->setup_hooktest_plugin(); // Register the fake plugin with the hook manager, but don't define any hook callbacks. di::set( manager::class, manager::phpunit_get_instance( [ 'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php', ], ), ); // Confirm a non-deprecated callback is called as expected. $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback')); // Confirm the deprecated callback is called as expected. $this->assertEquals( 'Called deprecated callback', component_callback('fake_hooktest', 'old_callback', [], null, true) ); $this->assertDebuggingNotCalled(); // Forcefully modify the PHPUnit flag on the manager to ensure the debugging message is output. $manager = di::get(manager::class); $rp = new \ReflectionProperty($manager, 'phpunit'); $rp->setValue($manager, false); component_callback('fake_hooktest', 'old_callback', [], null, true); $this->assertDebuggingCalled( 'Callback old_callback in fake_hooktest component should be migrated to new hook ' . 'callback for fake_hooktest\hook\hook_replacing_callback', ); } /** * Call a plugin callback that has been replaced by a hook, and has a hook callback. * * The original callback should not be called, and no debugging should be output. * * @runInSeparateProcess */ public function test_migrated_callback_with_replacement(): void { $this->resetAfterTest(true); // Include plugin hook discovery agent, and the hook that replaces the callback, and a hook callback for the hook. require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php'); // Register the fake plugin with the component manager. $this->setup_hooktest_plugin(); // Register the fake plugin with the hook manager, including the hook callback. di::set( manager::class, manager::phpunit_get_instance([ 'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php', ]), ); // Confirm a non-deprecated callback is called as expected. $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback')); // Confirm the deprecated callback is not called, as expected. $this->assertNull(component_callback('fake_hooktest', 'old_callback', [], null, true)); $this->assertDebuggingNotCalled(); } /** * Call a plugin class callback that has been replaced by a hook, but has no hook callback. * * The original class callback should be called, but a debugging message should be output. * * @runInSeparateProcess */ public function test_migrated_class_callback(): void { $this->resetAfterTest(true); // Include plugin hook discovery agent, the class containing callbacks, and the hook that replaces the class callback. require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php'); // Register the fake plugin with the component manager. $this->setup_hooktest_plugin(); // Register the fake plugin with the hook manager, but don't define any hook callbacks. di::set( manager::class, manager::phpunit_get_instance([ 'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php', ]), ); // Confirm a non-deprecated class callback is called as expected. $this->assertEquals( 'Called current class callback', component_class_callback('fake_hooktest\callbacks', 'current_class_callback', []) ); // Confirm the deprecated class callback is called as expected. $this->assertEquals( 'Called deprecated class callback', component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true) ); $this->assertDebuggingNotCalled(); // Forcefully modify the PHPUnit flag on the manager to ensure the debugging message is output. $manager = di::get(manager::class); $rp = new \ReflectionProperty($manager, 'phpunit'); $rp->setValue($manager, false); component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true); $this->assertDebuggingCalled( 'Callback callbacks::old_class_callback in fake_hooktest component should be migrated to new hook ' . 'callback for fake_hooktest\hook\hook_replacing_class_callback', ); } /** * Call a plugin class callback that has been replaced by a hook, and has a hook callback. * * The original callback should not be called, and no debugging should be output. * * @runInSeparateProcess */ public function test_migrated_class_callback_with_replacement(): void { $this->resetAfterTest(true); // Include plugin hook discovery agent, the class containing callbacks, the hook that replaces the class callback, // and a hook callback for the new hook. require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php'); require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php'); // Register the fake plugin with the component manager. $this->setup_hooktest_plugin(); // Register the fake plugin with the hook manager, including the hook callback. di::set( manager::class, manager::phpunit_get_instance([ 'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php', ]), ); // Confirm a non-deprecated class callback is called as expected. $this->assertEquals( 'Called current class callback', component_class_callback('fake_hooktest\callbacks', 'current_class_callback', []) ); // Confirm the deprecated class callback is not called, as expected. $this->assertNull(component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true)); $this->assertDebuggingNotCalled(); } /** * Test verifying that callbacks for deprecated plugins are not returned and hook dispatching won't call into these plugins. * * @runInSeparateProcess * @return void */ public function test_get_callbacks_for_hook_deprecated_plugintype(): void { $this->resetAfterTest(); // Inject the fixture 'fake' plugin type into component sources, which includes a single 'fake_fullfeatured' plugin. // This 'fake_fullfeatured' plugin is an available plugin at this stage (not yet deprecated). $this->add_full_mocked_plugintype( plugintype: 'fake', path: 'lib/tests/fixtures/fakeplugins/fake', ); // Force reset the static instance cache \core\hook\manager::$instance so that a fresh instance is instantiated, ensuring // the component lists are re-run and the hook manager can see the injected mock plugin and it's callbacks. // Note: we can't use \core\hook\manager::phpunit_get_instance() because that doesn't load in component callbacks from disk. $hookmanrc = new \ReflectionClass(\core\hook\manager::class); $hookmanrc->setStaticPropertyValue('instance', null); $manager = \core\hook\manager::get_instance(); // Get all registered callbacks for the hook listened to by the mock plugin (after_course_created). $listeners = $manager->get_callbacks_for_hook(\core_course\hook\after_course_created::class); $componentswithcallbacks = array_column($listeners, 'component'); // Verify the available mock plugin is returned as a listener. $this->assertContains('fake_fullfeatured', $componentswithcallbacks); // Deprecate the 'fake' plugin type. $this->deprecate_full_mocked_plugintype('fake'); // Force a fresh plugin manager instance, again to ensure the up-to-date component lists are used. $hookmanrc->setStaticPropertyValue('instance', null); $manager = \core\hook\manager::get_instance(); // And verify the plugin is now not returned as a listener, since it's deprecated. $listeners = $manager->get_callbacks_for_hook(\core_course\hook\after_course_created::class); $componentswithcallbacks = array_column($listeners, 'component'); $this->assertNotContains('fake_fullfeatured', $componentswithcallbacks); } /** * Normalise the sort order of callbacks to help with asserts. * * @param array $callbacks */ private function normalise_callbacks(array &$callbacks): void { foreach ($callbacks as &$callback) { ksort($callback); } } } PK,}\ after_login_completed.phpnu[PK,}\Ճbefore_user_updated.phpnu[PK,}\Wv<>>C extend_user_menu.phpnu[PK,}\v>Yttextend_bulk_user_actions.phpnu[PK,}\^`#before_user_deleted.phpnu[PK,}\3U  y)extend_default_homepage.phpnu[PK\G1before_file_created_test.phpnu[PK\55Aafter_cm_completion_updated.phpnu[PK/\pG 4Hafter_form_validation.phpnu[PK/\MPRafter_course_created.phpnu[PK/\Q/Xbefore_course_deleted.phpnu[PK/\2R:^after_course_updated.phpnu[PK/\>U)ebefore_course_viewed.phpnu[PK/\2n )wlbefore_activitychooserbutton_exported.phpnu[PK/\a`_wafter_form_definition.phpnu[PK/\(4~A~after_form_submission.phpnu[PK/\1$+after_form_definition_after_data.phpnu[PKs\Bnavigation/secondary_extend.phpnu[PKs\Q`77navigation/primary_extend.phpnu[PKs\ ostoppable_trait.phpnu[PKs\T4T T di_configuration.phpnu[PKs\ff manager.phpnu[PKs\{b/Caccess/after_role_assigned.phpnu[PKs\L access/after_role_unassigned.phpnu[PKs\aWaccess/after_role_switched.phpnu[PKs\\ 1 &output/before_standard_footer_html_generation.phpnu[PKs\k 21output/after_http_headers.phpnu[PKs\ !k9output/before_html_attributes.phpnu[PKs\on n (Foutput/before_footer_html_generation.phpnu[PKs\tv)EEZQoutput/extend_url.phpnu[PKs\b)Z3N N /Xoutput/before_standard_head_html_generation.phpnu[PKs\x-4GGcoutput/before_http_headers.phpnu[PKs\ql 5&koutput/after_standard_main_region_html_generation.phpnu[PKs\7= 6ovoutput/before_standard_top_of_body_html_generation.phpnu[PKs\described_hook.phpnu[PKs\Safter_config.phpnu[PKs\fXH##$;task/after_failed_task_max_delay.phpnu[PKs\/K#deprecated_callback_replacement.phpnu[PKs\EpTdiscovery_agent.phpnu[PK\Ed7CJJ"6navigation/primary_extend_test.phpnu[PK\N$ $ һbefore_course_viewed_test.phpnu[PK\S).Z.ZCmanager_test.phpnu[PK**?"