From e4a8ed5cc2130a6a4e1769065b54bd10b13c9352 Mon Sep 17 00:00:00 2001
From: Andrew Nicols <andrew@nicols.co.uk>
Date: Thu, 7 Mar 2024 10:55:17 +0800
Subject: [PATCH] MDL-81144 core: Convert standard_after_main_region_html to
 hook

---
 ...r_standard_main_region_html_generation.php | 81 +++++++++++++++++++
 lib/db/hooks.php                              |  5 ++
 lib/outputrenderers.php                       | 34 ++++----
 lib/tests/core_renderer_test.php              | 33 ++++++++
 ..._main_region_html_generation_callbacks.php | 39 +++++++++
 ...dard_main_region_html_generation_hooks.php | 34 ++++++++
 lib/upgrade.txt                               |  1 +
 message/classes/hook_callbacks.php            | 39 +++++++++
 message/lib.php                               |  9 ---
 9 files changed, 247 insertions(+), 28 deletions(-)
 create mode 100644 lib/classes/hook/output/after_standard_main_region_html_generation.php
 create mode 100644 lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_callbacks.php
 create mode 100644 lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_hooks.php
 create mode 100644 message/classes/hook_callbacks.php

diff --git a/lib/classes/hook/output/after_standard_main_region_html_generation.php b/lib/classes/hook/output/after_standard_main_region_html_generation.php
new file mode 100644
index 00000000000..1be1efbeb9b
--- /dev/null
+++ b/lib/classes/hook/output/after_standard_main_region_html_generation.php
@@ -0,0 +1,81 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core\hook\output;
+
+/**
+ * Hook to allow subscribers to add HTML content after the main region content has been generated.
+ *
+ * @package    core
+ * @copyright  2024 Andrew Lyons <andrew@nicols.co.uk>
+ * @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);
+                }
+            }
+        }
+    }
+}
diff --git a/lib/db/hooks.php b/lib/db/hooks.php
index 948397fabfc..be4a4788c5c 100644
--- a/lib/db/hooks.php
+++ b/lib/db/hooks.php
@@ -97,4 +97,9 @@ $callbacks = [
         'hook' => \core\hook\output\before_standard_footer_html_generation::class,
         'callback' => \core_userfeedback::class . '::before_standard_footer_html_generation',
     ],
+    [
+        'hook' => \core\hook\output\after_standard_main_region_html_generation::class,
+        'callback' => \core_message\hook_callbacks::class . '::add_messaging_widget',
+        'priority' => 0,
+    ],
 ];
diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php
index 062c8a86707..b83aa161c77 100644
--- a/lib/outputrenderers.php
+++ b/lib/outputrenderers.php
@@ -37,6 +37,8 @@
 
 use core\di;
 use core\hook\manager as hook_manager;
+use core\hook\output\after_standard_main_region_html_generation;
+use core\hook\output\before_html_attributes;
 use core\hook\output\before_standard_footer_html_generation;
 use core\hook\output\before_standard_top_of_body_html_generation;
 use core\output\named_templatable;
@@ -1105,29 +1107,23 @@ class core_renderer extends renderer_base {
      */
     public function standard_after_main_region_html() {
         global $CFG;
-        $output = '';
+
+        // Ensure that the callback exists prior to cache purge.
+        // This is a critical page path.
+        // TODO MDL-81134 Remove after LTS+1.
+        require_once(__DIR__ . '/classes/hook/output/after_standard_main_region_html_generation.php');
+
+        $hook = new after_standard_main_region_html_generation($this);
+
         if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
-            $output .= "\n".$CFG->additionalhtmlbottomofbody;
+            $hook->add_html("\n");
+            $hook->add_html($CFG->additionalhtmlbottomofbody);
         }
 
-        // 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) {
-                $output .= component_callback($name, 'standard_after_main_region_html', [], '');
-            }
-        }
+        di::get(hook_manager::class)->dispatch($hook);
+        $hook->process_legacy_callbacks();
 
-        // Give plugins an opportunity to inject extra html content. The callback
-        // must always return a string containing valid html.
-        $pluginswithfunction = get_plugins_with_function('standard_after_main_region_html', 'lib.php');
-        foreach ($pluginswithfunction as $plugins) {
-            foreach ($plugins as $function) {
-                $output .= $function();
-            }
-        }
-
-        return $output;
+        return $hook->get_output();
     }
 
     /**
diff --git a/lib/tests/core_renderer_test.php b/lib/tests/core_renderer_test.php
index 16cc3690cfa..cbc4af204ac 100644
--- a/lib/tests/core_renderer_test.php
+++ b/lib/tests/core_renderer_test.php
@@ -95,6 +95,39 @@ final class core_renderer_test extends \advanced_testcase {
         $this->assertStringContainsString('A heading can be added', $html);
     }
 
+    /**
+     * @covers \core\hook\after_standard_main_region_html_generation
+     */
+    public function test_after_standard_main_region_html_generation(): void {
+        $page = new moodle_page();
+        $renderer = new core_renderer($page, RENDERER_TARGET_GENERAL);
+
+        $html = $renderer->standard_after_main_region_html();
+        $this->assertIsString($html);
+        $this->assertStringNotContainsString('A heading can be added', $html);
+    }
+
+    /**
+     * @covers \core\hook\after_standard_main_region_html_generation
+     */
+    public function test_after_standard_main_region_html_generation_hooked(): void {
+        require_once(__DIR__ . '/fixtures/core_renderer/after_standard_main_region_html_generation_callbacks.php');
+
+        \core\di::set(
+            \core\hook\manager::class,
+            \core\hook\manager::phpunit_get_instance([
+                'test_plugin1' => __DIR__ . '/fixtures/core_renderer/after_standard_main_region_html_generation_hooks.php',
+            ]),
+        );
+
+        $page = new moodle_page();
+        $renderer = new core_renderer($page, RENDERER_TARGET_GENERAL);
+
+        $html = $renderer->standard_after_main_region_html();
+        $this->assertIsString($html);
+        $this->assertStringContainsString('A heading can be added', $html);
+    }
+
     /**
      * @covers \core\hook\before_html_attributes
      */
diff --git a/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_callbacks.php b/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_callbacks.php
new file mode 100644
index 00000000000..0c25e568461
--- /dev/null
+++ b/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_callbacks.php
@@ -0,0 +1,39 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace test_fixtures\core_renderer;
+
+/**
+ * Hook fixture for \core_renderer::after_standard_main_region_html_generation.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+final class after_standard_main_region_html_generation_callbacks {
+    /**
+     * Fixture for adding a heading after the standard main region HTML generation.
+     *
+     * @param \core\hook\output\after_standard_main_region_html_generation $hook
+     */
+    public static function after_standard_main_region_html_generation(
+        \core\hook\output\after_standard_main_region_html_generation $hook,
+    ): void {
+        $hook->add_html("<h1>A heading can be added</h1>");
+    }
+}
diff --git a/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_hooks.php b/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_hooks.php
new file mode 100644
index 00000000000..8c17715a471
--- /dev/null
+++ b/lib/tests/fixtures/core_renderer/after_standard_main_region_html_generation_hooks.php
@@ -0,0 +1,34 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Hook fixture for \core_renderer::after_standard_main_region_html_generation.
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$callbacks = [
+    [
+        'hook' => \core\hook\output\after_standard_main_region_html_generation::class,
+        'callback' => \test_fixtures\core_renderer\after_standard_main_region_html_generation_callbacks::class
+            . '::after_standard_main_region_html_generation',
+    ],
+];
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 7e501e73389..e4ff83a489d 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -40,6 +40,7 @@ information provided here is intended especially for developers.
   - before_standard_html_head() -> core\hook\output\before_standard_head_html_generation
   - bulk_user_actions() -> core_user\hook\extend_bulk_user_actions
   - before_standard_top_of_body_html() -> core\hook\output\before_standard_top_of_body_html_generation
+  - standard_after_main_region_html() -> core\hook\output\after_standard_main_region_html_generation
   - standard_footer_html() -> core\hook\output\before_standard_footer_html_generation
   - add_htmlattributes() -> core\hook\output\before_html_attributes
 * Deprecated PARAM_ types with the exception of PARAM_CLEAN now emit a deprecation exception. These were all deprecated in Moodle 2.0.
diff --git a/message/classes/hook_callbacks.php b/message/classes/hook_callbacks.php
new file mode 100644
index 00000000000..88338ef4a2a
--- /dev/null
+++ b/message/classes/hook_callbacks.php
@@ -0,0 +1,39 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_message;
+
+/**
+ * Class hook_callbacks
+ *
+ * @package    core_message
+ * @copyright  2024 Andrew Lyons <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class hook_callbacks {
+    /**
+     * Add messaging widgets after the main region content.
+     *
+     * @param \core\hook\output\after_standard_main_region_html_generation $hook
+     */
+    public static function add_messaging_widget(
+        \core\hook\output\after_standard_main_region_html_generation $hook,
+    ): void {
+        $hook->add_html(\core_message\helper::render_messaging_widget(
+            isdrawer: true,
+        ));
+    }
+}
diff --git a/message/lib.php b/message/lib.php
index d4e179289a6..c1fb3838cec 100644
--- a/message/lib.php
+++ b/message/lib.php
@@ -798,12 +798,3 @@ function core_message_user_preferences() {
         });
     return $preferences;
 }
-
-/**
- * Render the message drawer to be included in the top of the body of each page.
- *
- * @return string HTML
- */
-function core_message_standard_after_main_region_html() {
-    return \core_message\helper::render_messaging_widget(true, null, null);
-}