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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body