Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_admin\table;
  18  
  19  use core_plugin_manager;
  20  use flexible_table;
  21  use html_writer;
  22  use stdClass;
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  require_once("{$CFG->libdir}/tablelib.php");
  26  
  27  /**
  28   * Plugin Management table.
  29   *
  30   * @package    core_admin
  31   * @copyright  2023 Andrew Lyons <andrew@nicols.co.uk>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class hook_list_table extends flexible_table {
  35  
  36      /** @var \core\plugininfo\base[] The plugin list */
  37      protected array $plugins = [];
  38  
  39      /** @var int The number of enabled plugins of this type */
  40      protected int $enabledplugincount = 0;
  41  
  42      /** @var core_plugin_manager */
  43      protected core_plugin_manager $pluginmanager;
  44  
  45      /** @var string The plugininfo class for this plugintype */
  46      protected string $plugininfoclass;
  47  
  48      /** @var stdClass[] The list of emitted hooks with metadata */
  49      protected array $emitters;
  50  
  51      public function __construct() {
  52          global $CFG;
  53  
  54          $this->define_baseurl('/admin/hooks.php');
  55          parent::__construct('core_admin-hook_list_table');
  56  
  57          // Add emitted hooks.
  58          $this->emitters = \core\hook\manager::discover_known_hooks();
  59  
  60          $this->setup_column_configuration();
  61          $this->setup();
  62      }
  63  
  64      /**
  65       * Set up the column configuration for this table.
  66       */
  67      protected function setup_column_configuration(): void {
  68          $columnlist = [
  69              'details' => get_string('hookname', 'core_admin'),
  70              'callbacks' => get_string('hookcallbacks', 'core_admin'),
  71              'deprecates' => get_string('hookdeprecates', 'core_admin'),
  72          ];
  73          $this->define_columns(array_keys($columnlist));
  74          $this->define_headers(array_values($columnlist));
  75  
  76          $columnswithhelp = [
  77              'callbacks' => new \help_icon('hookcallbacks', 'admin'),
  78          ];
  79          $columnhelp = array_map(function (string $column) use ($columnswithhelp): ?\renderable {
  80              if (array_key_exists($column, $columnswithhelp)) {
  81                  return $columnswithhelp[$column];
  82              }
  83  
  84              return null;
  85          }, array_keys($columnlist));
  86          $this->define_help_for_headers($columnhelp);
  87      }
  88  
  89      /**
  90       * Print the table.
  91       */
  92      public function out(): void {
  93          // All hook consumers referenced from the db/hooks.php files.
  94          $hookmanager = \core\hook\manager::get_instance();
  95          $allhooks = (array)$hookmanager->get_all_callbacks();
  96  
  97          // Add any unused hooks.
  98          foreach (array_keys($this->emitters) as $classname) {
  99              if (isset($allhooks[$classname])) {
 100                  continue;
 101              }
 102              $allhooks[$classname] = [];
 103          }
 104  
 105          // Order rows by hook name, putting core first.
 106          \core_collator::ksort($allhooks);
 107          $corehooks = [];
 108          foreach ($allhooks as $classname => $consumers) {
 109              if (str_starts_with($classname, 'core\\')) {
 110                  $corehooks[$classname] = $consumers;
 111                  unset($allhooks[$classname]);
 112              }
 113          }
 114          $allhooks = array_merge($corehooks, $allhooks);
 115  
 116          foreach ($allhooks as $classname => $consumers) {
 117              $this->add_data_keyed(
 118                  $this->format_row((object) [
 119                      'classname' => $classname,
 120                      'callbacks' => $consumers,
 121                  ]),
 122                  $this->get_row_class($classname),
 123              );
 124          }
 125  
 126          $this->finish_output(false);
 127      }
 128  
 129      protected function col_details(stdClass $row): string {
 130          return $row->classname .
 131              $this->get_description($row) .
 132              html_writer::div($this->get_tags_for_row($row));
 133      }
 134  
 135      /**
 136       * Show the name column content.
 137       *
 138       * @param stdClass $row
 139       * @return string
 140       */
 141      protected function get_description(stdClass $row): string {
 142          if (!array_key_exists($row->classname, $this->emitters)) {
 143              return '';
 144          }
 145  
 146          return html_writer::tag(
 147              'small',
 148              clean_text(markdown_to_html($this->emitters[$row->classname]['description']), FORMAT_HTML),
 149          );
 150      }
 151  
 152      protected function col_deprecates(stdClass $row): string {
 153          if (!class_exists($row->classname)) {
 154              return '';
 155          }
 156  
 157          $rc = new \ReflectionClass($row->classname);
 158          if (!$rc->implementsInterface(\core\hook\deprecated_callback_replacement::class)) {
 159              return '';
 160          }
 161          $deprecates = call_user_func([$row->classname, 'get_deprecated_plugin_callbacks']);
 162          if (count($deprecates) === 0) {
 163              return '';
 164          }
 165          $content = html_writer::start_tag('ul');
 166  
 167          foreach ($deprecates as $deprecatedmethod) {
 168              $content .= html_writer::tag('li', $deprecatedmethod);
 169          }
 170          $content .= html_writer::end_tag('ul');
 171          return $content;
 172      }
 173  
 174      protected function col_callbacks(stdClass $row): string {
 175          global $CFG;
 176  
 177          $hookclass = $row->classname;
 178          $cbinfo = [];
 179          foreach ($row->callbacks as $definition) {
 180              $iscallable = is_callable($definition['callback'], false, $callbackname);
 181              $isoverridden = isset($CFG->hooks_callback_overrides[$hookclass][$definition['callback']]);
 182              $info = "{$callbackname}&nbsp;({$definition['priority']})";
 183              if (!$iscallable) {
 184                  $info .= '&nbsp;';
 185                  $info .= $this->get_tag(
 186                      get_string('error'),
 187                      'danger',
 188                      get_string('hookcallbacknotcallable', 'core_admin', $callbackname),
 189                  );
 190              }
 191              if ($isoverridden) {
 192                  // The lang string meaning should be close enough here.
 193                  $info .= $this->get_tag(
 194                      get_string('hookconfigoverride', 'core_admin'),
 195                      'warning',
 196                      get_string('hookconfigoverride_help', 'core_admin'),
 197                  );
 198              }
 199  
 200              $cbinfo[] = $info;
 201          }
 202  
 203          if ($cbinfo) {
 204              $output = html_writer::start_tag('ol');
 205              foreach ($cbinfo as $callback) {
 206                  $class = '';
 207                  if ($definition['disabled']) {
 208                      $class = 'dimmed_text';
 209                  }
 210                  $output .= html_writer::tag('li', $callback, ['class' => $class]);
 211              }
 212              $output .= html_writer::end_tag('ol');
 213              return $output;
 214          } else {
 215              return '';
 216          }
 217      }
 218  
 219      /**
 220       * Get the HTML to display the badge with tooltip.
 221       *
 222       * @param string $tag The main text to display
 223       * @param null|string $type The pill type
 224       * @param null|string $tooltip The content of the tooltip
 225       * @return string
 226       */
 227      protected function get_tag(
 228          string $tag,
 229          ?string $type = null,
 230          ?string $tooltip = null,
 231      ): string {
 232          $attributes = [];
 233  
 234          if ($type === null) {
 235              $type = 'info';
 236          }
 237  
 238          if ($tooltip) {
 239              $attributes['data-toggle'] = 'tooltip';
 240              $attributes['title'] = $tooltip;
 241          }
 242          return html_writer::span($tag, "badge badge-{$type}", $attributes);
 243      }
 244  
 245      /**
 246       * Get the code to display a set of tags for this table row.
 247       *
 248       * @param stdClass $row
 249       * @return string
 250       */
 251      protected function get_tags_for_row(stdClass $row): string {
 252          if (!array_key_exists($row->classname, $this->emitters)) {
 253              // This hook has been defined in the db/hooks.php file
 254              // but does not refer to a hook in this version of Moodle.
 255              return $this->get_tag(
 256                  get_string('hookunknown', 'core_admin'),
 257                  'warning',
 258                  get_string('hookunknown_desc', 'core_admin'),
 259              );
 260          }
 261  
 262          if (!class_exists($row->classname)) {
 263              // This hook has been defined in a hook discovery agent, but the class it refers to could not be found.
 264              return $this->get_tag(
 265                  get_string('hookclassmissing', 'core_admin'),
 266                  'warning',
 267                  get_string('hookclassmissing_desc', 'core_admin'),
 268              );
 269          }
 270  
 271          $tags = $this->emitters[$row->classname]['tags'] ?? [];
 272          $taglist = array_map(function($tag): string {
 273              if (is_array($tag)) {
 274                  return $this->get_tag(...$tag);
 275              }
 276              return $this->get_tag($tag, 'badge badge-info');
 277          }, $tags);
 278  
 279          return implode("\n", $taglist);
 280      }
 281  
 282      protected function get_row_class(string $classname): string {
 283          return '';
 284      }
 285  }