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 context_system;
  20  use core_plugin_manager;
  21  use core_table\dynamic as dynamic_table;
  22  use flexible_table;
  23  use html_writer;
  24  use moodle_url;
  25  use stdClass;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  require_once("{$CFG->libdir}/tablelib.php");
  29  
  30  /**
  31   * Plugin Management table.
  32   *
  33   * @package    core_admin
  34   * @copyright  2023 Andrew Lyons <andrew@nicols.co.uk>
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  abstract class plugin_management_table extends flexible_table implements dynamic_table {
  38  
  39      /** @var \core\plugininfo\base[] The plugin list */
  40      protected array $plugins = [];
  41  
  42      /** @var int The number of enabled plugins of this type */
  43      protected int $enabledplugincount = 0;
  44  
  45      /** @var core_plugin_manager */
  46      protected core_plugin_manager $pluginmanager;
  47  
  48      /** @var string The plugininfo class for this plugintype */
  49      protected string $plugininfoclass;
  50  
  51      public function __construct() {
  52          global $CFG;
  53  
  54          parent::__construct($this->get_table_id());
  55          require_once($CFG->libdir . '/adminlib.php');
  56  
  57          // Fetch the plugininfo class.
  58          $this->pluginmanager = core_plugin_manager::instance();
  59          $this->plugininfoclass = $this->pluginmanager::resolve_plugininfo_class($this->get_plugintype());
  60  
  61          $this->guess_base_url();
  62  
  63          $this->plugins = $this->get_sorted_plugins();
  64          $this->enabledplugincount = count(array_filter($this->plugins, function ($plugin) {
  65              return $plugin->is_enabled();
  66          }));
  67  
  68          $this->setup_column_configuration();
  69          $this->set_filterset(new plugin_management_table_filterset());
  70          $this->setup();
  71      }
  72  
  73      /**
  74       * Get the list of sorted plugins.
  75       *
  76       * @return \core\plugininfo\base[]
  77       */
  78      protected function get_sorted_plugins(): array {
  79          if ($this->plugininfoclass::plugintype_supports_ordering()) {
  80              return $this->plugininfoclass::get_sorted_plugins();
  81          } else {
  82              $plugins = $this->pluginmanager->get_plugins_of_type($this->get_plugintype());
  83              return self::sort_plugins($plugins);
  84          }
  85      }
  86  
  87      /**
  88       * Sort the plugins list.
  89       *
  90       * Note: This only applies to plugins which do not support ordering.
  91       *
  92       * @param \core\plugininfo\base[] $plugins
  93       * @return \core\plugininfo\base[]
  94       */
  95      protected function sort_plugins(array $plugins): array {
  96          // The asort functions work by reference.
  97          \core_collator::asort_objects_by_property($plugins, 'displayname');
  98  
  99          return $plugins;
 100      }
 101  
 102      /**
 103       * Set up the column configuration for this table.
 104       */
 105      protected function setup_column_configuration(): void {
 106          $columnlist = $this->get_column_list();
 107          $this->define_columns(array_keys($columnlist));
 108          $this->define_headers(array_values($columnlist));
 109  
 110          $columnswithhelp = $this->get_columns_with_help();
 111          $columnhelp = array_map(function (string $column) use ($columnswithhelp): ?\renderable {
 112              if (array_key_exists($column, $columnswithhelp)) {
 113                  return $columnswithhelp[$column];
 114              }
 115  
 116              return null;
 117          }, array_keys($columnlist));
 118          $this->define_help_for_headers($columnhelp);
 119      }
 120  
 121      /**
 122       * Set the standard order of the plugins.
 123       *
 124       * @param array $plugins
 125       * @return array
 126       */
 127      protected function order_plugins(array $plugins): array {
 128          uasort($plugins, function ($a, $b) {
 129              if ($a->is_enabled() && !$b->is_enabled()) {
 130                  return -1;
 131              } else if (!$a->is_enabled() && $b->is_enabled()) {
 132                  return 1;
 133              }
 134              return strnatcasecmp($a->name, $b->name);
 135          });
 136  
 137          return $plugins;
 138      }
 139  
 140      /**
 141       * Get the plugintype for this table.
 142       *
 143       * @return string
 144       */
 145      abstract protected function get_plugintype(): string;
 146  
 147      /**
 148       * Get the action URL for this table.
 149       *
 150       * The action URL is used to perform all actions when JS is not available.
 151       *
 152       * @param array $params
 153       * @return moodle_url
 154       */
 155      abstract protected function get_action_url(array $params = []): moodle_url;
 156  
 157      /**
 158       * Provide a default implementation for guessing the base URL from the action URL.
 159       */
 160      public function guess_base_url(): void {
 161          $this->define_baseurl($this->get_action_url());
 162      }
 163  
 164      /**
 165       * Get the web service method used to toggle state.
 166       *
 167       * @return null|string
 168       */
 169      protected function get_toggle_service(): ?string {
 170          return 'core_admin_set_plugin_state';
 171      }
 172  
 173      /**
 174       * Get the web service method used to order plugins.
 175       *
 176       * @return null|string
 177       */
 178      protected function get_sortorder_service(): ?string {
 179          return 'core_admin_set_plugin_order';
 180      }
 181  
 182      /**
 183       * Get the ID of the table.
 184       *
 185       * @return string
 186       */
 187      protected function get_table_id(): string {
 188          return 'plugin_management_table-' . $this->get_plugintype();
 189      }
 190  
 191      /**
 192       * Get a list of the column titles
 193       * @return string[]
 194       */
 195      protected function get_column_list(): array {
 196          $columns = [
 197              'name' => get_string('name', 'core'),
 198              'version' => get_string('version', 'core'),
 199          ];
 200  
 201          if ($this->supports_disabling()) {
 202              $columns['enabled'] = get_string('pluginenabled', 'core_plugin');
 203          }
 204  
 205          if ($this->supports_ordering()) {
 206              $columns['order'] = get_string('order', 'core');
 207          }
 208  
 209          $columns['settings'] = get_string('settings', 'core');
 210          $columns['uninstall'] = get_string('uninstallplugin', 'core_admin');
 211  
 212          return $columns;
 213      }
 214  
 215      protected function get_columns_with_help(): array {
 216          return [];
 217      }
 218  
 219      /**
 220       * Get the context for this table.
 221       *
 222       * @return context_system
 223       */
 224      public function get_context(): context_system {
 225          return context_system::instance();
 226      }
 227  
 228      /**
 229       * Get the table content.
 230       */
 231      public function get_content(): string {
 232          ob_start();
 233          $this->out();
 234          $content = ob_get_contents();
 235          ob_end_clean();
 236          return $content;
 237      }
 238  
 239      /**
 240       * Print the table.
 241       */
 242      public function out(): void {
 243          $plugintype = $this->get_plugintype();
 244          foreach ($this->plugins as $plugininfo) {
 245              $plugin = "{$plugintype}_{$plugininfo->name}";
 246              $rowdata = (object) [
 247                  'plugin' => $plugin,
 248                  'plugininfo' => $plugininfo,
 249                  'name' => $plugininfo->displayname,
 250                  'version' => $plugininfo->versiondb,
 251              ];
 252              $this->add_data_keyed(
 253                  $this->format_row($rowdata),
 254                  $this->get_row_class($rowdata)
 255              );
 256          }
 257  
 258          $this->finish_output(false);
 259      }
 260  
 261      /**
 262       * This table is not downloadable.
 263       * @param bool $downloadable
 264       * @return bool
 265       */
 266      // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
 267      public function is_downloadable($downloadable = null): bool {
 268          return false;
 269      }
 270  
 271      /**
 272       * Show the name column content.
 273       *
 274       * @param stdClass $row
 275       * @return string
 276       */
 277      protected function col_name(stdClass $row): string {
 278          $status = $row->plugininfo->get_status();
 279          if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) {
 280              return html_writer::span(
 281                  get_string('pluginmissingfromdisk', 'core', $row->plugininfo),
 282                  'notifyproblem'
 283              );
 284          }
 285  
 286          if ($row->plugininfo->is_installed_and_upgraded()) {
 287              return $row->plugininfo->displayname;
 288          }
 289  
 290          return html_writer::span(
 291              $row->plugininfo->displayname,
 292              'notifyproblem'
 293          );
 294      }
 295  
 296      /**
 297       * Show the enable/disable column content.
 298       *
 299       * @param stdClass $row
 300       * @return string
 301       */
 302      protected function col_enabled(stdClass $row): string {
 303          global $OUTPUT;
 304  
 305          $enabled = $row->plugininfo->is_enabled();
 306          $params = [
 307              'sesskey' => sesskey(),
 308              'plugin' => $row->plugininfo->name,
 309              'action' => $enabled ? 'disable' : 'enable',
 310          ];
 311  
 312          if ($enabled) {
 313              $icon = $OUTPUT->pix_icon('t/hide', get_string('disableplugin', 'core_admin', $row->plugininfo->displayname));
 314          } else {
 315              $icon = $OUTPUT->pix_icon('t/show', get_string('enableplugin', 'core_admin', $row->plugininfo->displayname));
 316          }
 317  
 318          return html_writer::link(
 319              $this->get_action_url($params),
 320              $icon,
 321              [
 322                  'data-toggle-method' => $this->get_toggle_service(),
 323                  'data-action' => 'togglestate',
 324                  'data-plugin' => $row->plugin,
 325                  'data-state' => $enabled ? 1 : 0,
 326              ],
 327          );
 328      }
 329  
 330      protected function col_order(stdClass $row): string {
 331          global $OUTPUT;
 332  
 333          if (!$this->supports_ordering()) {
 334              return '';
 335          }
 336  
 337          if (!$row->plugininfo->is_enabled()) {
 338              return '';
 339          }
 340  
 341          if ($this->enabledplugincount <= 1) {
 342              // There is only one row.
 343              return '';
 344          }
 345  
 346          $hasup = true;
 347          $hasdown = true;
 348  
 349          if (empty($this->currentrow)) {
 350              // This is the top row.
 351              $hasup = false;
 352          }
 353  
 354          if ($this->currentrow === ($this->enabledplugincount - 1)) {
 355              // This is the last row.
 356              $hasdown = false;
 357          }
 358  
 359          if ($this->supports_ordering()) {
 360              $dataattributes = [
 361                  'data-method' => $this->get_sortorder_service(),
 362                  'data-action' => 'move',
 363                  'data-plugin' => $row->plugin,
 364              ];
 365          } else {
 366              $dataattributes = [];
 367          }
 368  
 369          if ($hasup) {
 370              $upicon = html_writer::link(
 371                  $this->get_action_url([
 372                      'sesskey' => sesskey(),
 373                      'action' => 'up',
 374                      'plugin' => $row->plugininfo->name,
 375                  ]),
 376                  $OUTPUT->pix_icon('t/up', get_string('moveup')),
 377                  array_merge($dataattributes, ['data-direction' => 'up']),
 378              );
 379          } else {
 380              $upicon = $OUTPUT->spacer();
 381          }
 382  
 383          if ($hasdown) {
 384              $downicon = html_writer::link(
 385                  $this->get_action_url([
 386                      'sesskey' => sesskey(),
 387                      'action' => 'down',
 388                      'plugin' => $row->plugininfo->name,
 389                  ]),
 390                  $OUTPUT->pix_icon('t/down', get_string('movedown')),
 391                  array_merge($dataattributes, ['data-direction' => 'down']),
 392              );
 393          } else {
 394              $downicon = $OUTPUT->spacer();
 395          }
 396  
 397          // For now just add the up/down icons.
 398          return html_writer::span($upicon . $downicon);
 399      }
 400  
 401      /**
 402       * Show the settings column content.
 403       *
 404       * @param stdClass $row
 405       * @return string
 406       */
 407      protected function col_settings(stdClass $row): string {
 408          if ($settingsurl = $row->plugininfo->get_settings_url()) {
 409              return html_writer::link($settingsurl, get_string('settings'));
 410          }
 411  
 412          return '';
 413      }
 414  
 415      /**
 416       * Show the Uninstall column content.
 417       *
 418       * @param stdClass $row
 419       * @return string
 420       */
 421      protected function col_uninstall(stdClass $row): string {
 422          $status = $row->plugininfo->get_status();
 423  
 424          if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) {
 425              return get_string('status_new', 'core_plugin');
 426          }
 427  
 428          if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) {
 429              $uninstall = get_string('status_missing', 'core_plugin') . '<br/>';
 430          } else {
 431              $uninstall = '';
 432          }
 433  
 434          if ($uninstallurl = $this->pluginmanager->get_uninstall_url($row->plugin)) {
 435              $uninstall .= html_writer::link($uninstallurl, get_string('uninstallplugin', 'core_admin'));
 436          }
 437  
 438          return $uninstall;
 439      }
 440  
 441      /**
 442       * Get the JS module used to manage this table.
 443       *
 444       * This should be a class which extends 'core_admin/plugin_management_table'.
 445       *
 446       * @return string
 447       */
 448      protected function get_table_js_module(): string {
 449          return 'core_admin/plugin_management_table';
 450      }
 451  
 452      /**
 453       * Add JS specific to this implementation.
 454       *
 455       * @return string
 456       */
 457      protected function get_dynamic_table_html_end(): string {
 458          global $PAGE;
 459  
 460          $PAGE->requires->js_call_amd($this->get_table_js_module(), 'init');
 461          return parent::get_dynamic_table_html_end();
 462      }
 463  
 464      /**
 465       * Get any class to add to the row.
 466       *
 467       * @param mixed $row
 468       * @return string
 469       */
 470      protected function get_row_class($row): string {
 471          $plugininfo = $row->plugininfo;
 472          if ($plugininfo->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
 473              return '';
 474          }
 475  
 476          if (!$plugininfo->is_enabled()) {
 477              return 'dimmed_text';
 478          }
 479          return '';
 480      }
 481  
 482      public static function get_filterset_class(): string {
 483          return self::class . '_filterset';
 484      }
 485  
 486      /**
 487       * Whether this plugin type supports the disabling of plugins.
 488       *
 489       * @return bool
 490       */
 491      protected function supports_disabling(): bool {
 492          return $this->plugininfoclass::plugintype_supports_disabling();
 493      }
 494  
 495      /**
 496       * Whether this table should show ordering fields.
 497       *
 498       * @return bool
 499       */
 500      protected function supports_ordering(): bool {
 501          return $this->plugininfoclass::plugintype_supports_ordering();
 502      }
 503  }