Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]
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 /** 18 * Defines classes used for plugins management 19 * 20 * This library provides a unified interface to various plugin types in 21 * Moodle. It is mainly used by the plugins management admin page and the 22 * plugins check page during the upgrade. 23 * 24 * @package core 25 * @copyright 2011 David Mudrak <david@moodle.com> 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 class core_plugin_manager { 29 30 /** the plugin is shipped with standard Moodle distribution */ 31 const PLUGIN_SOURCE_STANDARD = 'std'; 32 /** the plugin is added extension */ 33 const PLUGIN_SOURCE_EXTENSION = 'ext'; 34 35 /** the plugin uses neither database nor capabilities, no versions */ 36 const PLUGIN_STATUS_NODB = 'nodb'; 37 /** the plugin is up-to-date */ 38 const PLUGIN_STATUS_UPTODATE = 'uptodate'; 39 /** the plugin is about to be installed */ 40 const PLUGIN_STATUS_NEW = 'new'; 41 /** the plugin is about to be upgraded */ 42 const PLUGIN_STATUS_UPGRADE = 'upgrade'; 43 /** the standard plugin is about to be deleted */ 44 const PLUGIN_STATUS_DELETE = 'delete'; 45 /** the version at the disk is lower than the one already installed */ 46 const PLUGIN_STATUS_DOWNGRADE = 'downgrade'; 47 /** the plugin is installed but missing from disk */ 48 const PLUGIN_STATUS_MISSING = 'missing'; 49 50 /** the given requirement/dependency is fulfilled */ 51 const REQUIREMENT_STATUS_OK = 'ok'; 52 /** the plugin requires higher core/other plugin version than is currently installed */ 53 const REQUIREMENT_STATUS_OUTDATED = 'outdated'; 54 /** the required dependency is not installed */ 55 const REQUIREMENT_STATUS_MISSING = 'missing'; 56 /** the current Moodle version is too high for plugin. */ 57 const REQUIREMENT_STATUS_NEWER = 'newer'; 58 59 /** the required dependency is available in the plugins directory */ 60 const REQUIREMENT_AVAILABLE = 'available'; 61 /** the required dependency is available in the plugins directory */ 62 const REQUIREMENT_UNAVAILABLE = 'unavailable'; 63 64 /** the moodle version is explicitly supported */ 65 const VERSION_SUPPORTED = 'supported'; 66 /** the moodle version is not explicitly supported */ 67 const VERSION_NOT_SUPPORTED = 'notsupported'; 68 /** the plugin does not specify supports */ 69 const VERSION_NO_SUPPORTS = 'nosupports'; 70 71 /** @var core_plugin_manager holds the singleton instance */ 72 protected static $singletoninstance; 73 /** @var array of raw plugins information */ 74 protected $pluginsinfo = null; 75 /** @var array of raw subplugins information */ 76 protected $subpluginsinfo = null; 77 /** @var array cache information about availability in the plugins directory if requesting "at least" version */ 78 protected $remotepluginsinfoatleast = null; 79 /** @var array cache information about availability in the plugins directory if requesting exact version */ 80 protected $remotepluginsinfoexact = null; 81 /** @var array list of installed plugins $name=>$version */ 82 protected $installedplugins = null; 83 /** @var array list of all enabled plugins $name=>$name */ 84 protected $enabledplugins = null; 85 /** @var array list of all enabled plugins $name=>$diskversion */ 86 protected $presentplugins = null; 87 /** @var array reordered list of plugin types */ 88 protected $plugintypes = null; 89 /** @var \core\update\code_manager code manager to use for plugins code operations */ 90 protected $codemanager = null; 91 /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */ 92 protected $updateapiclient = null; 93 94 /** 95 * Direct initiation not allowed, use the factory method {@link self::instance()} 96 */ 97 protected function __construct() { 98 } 99 100 /** 101 * Sorry, this is singleton 102 */ 103 protected function __clone() { 104 } 105 106 /** 107 * Factory method for this class 108 * 109 * @return static the singleton instance 110 */ 111 public static function instance() { 112 if (is_null(static::$singletoninstance)) { 113 static::$singletoninstance = new static(); 114 } 115 return static::$singletoninstance; 116 } 117 118 /** 119 * Reset all caches. 120 * @param bool $phpunitreset 121 */ 122 public static function reset_caches($phpunitreset = false) { 123 if ($phpunitreset) { 124 static::$singletoninstance = null; 125 } else { 126 if (static::$singletoninstance) { 127 static::$singletoninstance->pluginsinfo = null; 128 static::$singletoninstance->subpluginsinfo = null; 129 static::$singletoninstance->remotepluginsinfoatleast = null; 130 static::$singletoninstance->remotepluginsinfoexact = null; 131 static::$singletoninstance->installedplugins = null; 132 static::$singletoninstance->enabledplugins = null; 133 static::$singletoninstance->presentplugins = null; 134 static::$singletoninstance->plugintypes = null; 135 static::$singletoninstance->codemanager = null; 136 static::$singletoninstance->updateapiclient = null; 137 } 138 } 139 $cache = cache::make('core', 'plugin_manager'); 140 $cache->purge(); 141 } 142 143 /** 144 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans 145 * 146 * @see self::reorder_plugin_types() 147 * @return array (string)name => (string)location 148 */ 149 public function get_plugin_types() { 150 if (func_num_args() > 0) { 151 if (!func_get_arg(0)) { 152 throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.'); 153 } 154 } 155 if ($this->plugintypes) { 156 return $this->plugintypes; 157 } 158 159 $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types()); 160 return $this->plugintypes; 161 } 162 163 /** 164 * Load list of installed plugins, 165 * always call before using $this->installedplugins. 166 * 167 * This method is caching results for all plugins. 168 */ 169 protected function load_installed_plugins() { 170 global $DB, $CFG; 171 172 if ($this->installedplugins) { 173 return; 174 } 175 176 if (empty($CFG->version)) { 177 // Nothing installed yet. 178 $this->installedplugins = array(); 179 return; 180 } 181 182 $cache = cache::make('core', 'plugin_manager'); 183 $installed = $cache->get('installed'); 184 185 if (is_array($installed)) { 186 $this->installedplugins = $installed; 187 return; 188 } 189 190 $this->installedplugins = array(); 191 192 $versions = $DB->get_records('config_plugins', array('name'=>'version')); 193 foreach ($versions as $version) { 194 $parts = explode('_', $version->plugin, 2); 195 if (!isset($parts[1])) { 196 // Invalid component, there must be at least one "_". 197 continue; 198 } 199 // Do not verify here if plugin type and name are valid. 200 $this->installedplugins[$parts[0]][$parts[1]] = $version->value; 201 } 202 203 foreach ($this->installedplugins as $key => $value) { 204 ksort($this->installedplugins[$key]); 205 } 206 207 $cache->set('installed', $this->installedplugins); 208 } 209 210 /** 211 * Return list of installed plugins of given type. 212 * @param string $type 213 * @return array $name=>$version 214 */ 215 public function get_installed_plugins($type) { 216 $this->load_installed_plugins(); 217 if (isset($this->installedplugins[$type])) { 218 return $this->installedplugins[$type]; 219 } 220 return array(); 221 } 222 223 /** 224 * Load list of all enabled plugins, 225 * call before using $this->enabledplugins. 226 * 227 * This method is caching results from individual plugin info classes. 228 */ 229 protected function load_enabled_plugins() { 230 global $CFG; 231 232 if ($this->enabledplugins) { 233 return; 234 } 235 236 if (empty($CFG->version)) { 237 $this->enabledplugins = array(); 238 return; 239 } 240 241 $cache = cache::make('core', 'plugin_manager'); 242 $enabled = $cache->get('enabled'); 243 244 if (is_array($enabled)) { 245 $this->enabledplugins = $enabled; 246 return; 247 } 248 249 $this->enabledplugins = array(); 250 251 require_once($CFG->libdir.'/adminlib.php'); 252 253 $plugintypes = core_component::get_plugin_types(); 254 foreach ($plugintypes as $plugintype => $fulldir) { 255 $plugininfoclass = static::resolve_plugininfo_class($plugintype); 256 if (class_exists($plugininfoclass)) { 257 $enabled = $plugininfoclass::get_enabled_plugins(); 258 if (!is_array($enabled)) { 259 continue; 260 } 261 $this->enabledplugins[$plugintype] = $enabled; 262 } 263 } 264 265 $cache->set('enabled', $this->enabledplugins); 266 } 267 268 /** 269 * Get list of enabled plugins of given type, 270 * the result may contain missing plugins. 271 * 272 * @param string $type 273 * @return array|null list of enabled plugins of this type, null if unknown 274 */ 275 public function get_enabled_plugins($type) { 276 $this->load_enabled_plugins(); 277 if (isset($this->enabledplugins[$type])) { 278 return $this->enabledplugins[$type]; 279 } 280 return null; 281 } 282 283 /** 284 * Load list of all present plugins - call before using $this->presentplugins. 285 */ 286 protected function load_present_plugins() { 287 if ($this->presentplugins) { 288 return; 289 } 290 291 $cache = cache::make('core', 'plugin_manager'); 292 $present = $cache->get('present'); 293 294 if (is_array($present)) { 295 $this->presentplugins = $present; 296 return; 297 } 298 299 $this->presentplugins = array(); 300 301 $plugintypes = core_component::get_plugin_types(); 302 foreach ($plugintypes as $type => $typedir) { 303 $plugs = core_component::get_plugin_list($type); 304 foreach ($plugs as $plug => $fullplug) { 305 $module = new stdClass(); 306 $plugin = new stdClass(); 307 $plugin->version = null; 308 include ($fullplug.'/version.php'); 309 310 // Check if the legacy $module syntax is still used. 311 if (!is_object($module) or (count((array)$module) > 0)) { 312 debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.'); 313 $skipcache = true; 314 } 315 316 // Check if the component is properly declared. 317 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) { 318 debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.'); 319 $skipcache = true; 320 } 321 322 $this->presentplugins[$type][$plug] = $plugin; 323 } 324 } 325 326 if (empty($skipcache)) { 327 $cache->set('present', $this->presentplugins); 328 } 329 } 330 331 /** 332 * Get list of present plugins of given type. 333 * 334 * @param string $type 335 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown 336 */ 337 public function get_present_plugins($type) { 338 $this->load_present_plugins(); 339 if (isset($this->presentplugins[$type])) { 340 return $this->presentplugins[$type]; 341 } 342 return null; 343 } 344 345 /** 346 * Returns a tree of known plugins and information about them 347 * 348 * @return array 2D array. The first keys are plugin type names (e.g. qtype); 349 * the second keys are the plugin local name (e.g. multichoice); and 350 * the values are the corresponding objects extending {@link \core\plugininfo\base} 351 */ 352 public function get_plugins() { 353 $this->init_pluginsinfo_property(); 354 355 // Make sure all types are initialised. 356 foreach ($this->pluginsinfo as $plugintype => $list) { 357 if ($list === null) { 358 $this->get_plugins_of_type($plugintype); 359 } 360 } 361 362 return $this->pluginsinfo; 363 } 364 365 /** 366 * Returns list of known plugins of the given type. 367 * 368 * This method returns the subset of the tree returned by {@link self::get_plugins()}. 369 * If the given type is not known, empty array is returned. 370 * 371 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation' 372 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base} 373 */ 374 public function get_plugins_of_type($type) { 375 global $CFG; 376 377 $this->init_pluginsinfo_property(); 378 379 if (!array_key_exists($type, $this->pluginsinfo)) { 380 return array(); 381 } 382 383 if (is_array($this->pluginsinfo[$type])) { 384 return $this->pluginsinfo[$type]; 385 } 386 387 $types = core_component::get_plugin_types(); 388 389 if (!isset($types[$type])) { 390 // Orphaned subplugins! 391 $plugintypeclass = static::resolve_plugininfo_class($type); 392 $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this); 393 return $this->pluginsinfo[$type]; 394 } 395 396 /** @var \core\plugininfo\base $plugintypeclass */ 397 $plugintypeclass = static::resolve_plugininfo_class($type); 398 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this); 399 $this->pluginsinfo[$type] = $plugins; 400 401 return $this->pluginsinfo[$type]; 402 } 403 404 /** 405 * Init placeholder array for plugin infos. 406 */ 407 protected function init_pluginsinfo_property() { 408 if (is_array($this->pluginsinfo)) { 409 return; 410 } 411 $this->pluginsinfo = array(); 412 413 $plugintypes = $this->get_plugin_types(); 414 415 foreach ($plugintypes as $plugintype => $plugintyperootdir) { 416 $this->pluginsinfo[$plugintype] = null; 417 } 418 419 // Add orphaned subplugin types. 420 $this->load_installed_plugins(); 421 foreach ($this->installedplugins as $plugintype => $unused) { 422 if (!isset($plugintypes[$plugintype])) { 423 $this->pluginsinfo[$plugintype] = null; 424 } 425 } 426 } 427 428 /** 429 * Find the plugin info class for given type. 430 * 431 * @param string $type 432 * @return string name of pluginfo class for give plugin type 433 */ 434 public static function resolve_plugininfo_class($type) { 435 $plugintypes = core_component::get_plugin_types(); 436 if (!isset($plugintypes[$type])) { 437 return '\core\plugininfo\orphaned'; 438 } 439 440 $parent = core_component::get_subtype_parent($type); 441 442 if ($parent) { 443 $class = '\\'.$parent.'\plugininfo\\' . $type; 444 if (class_exists($class)) { 445 $plugintypeclass = $class; 446 } else { 447 if ($dir = core_component::get_component_directory($parent)) { 448 // BC only - use namespace instead! 449 if (file_exists("$dir/adminlib.php")) { 450 global $CFG; 451 include_once("$dir/adminlib.php"); 452 } 453 if (class_exists('plugininfo_' . $type)) { 454 $plugintypeclass = 'plugininfo_' . $type; 455 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER); 456 } else { 457 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER); 458 $plugintypeclass = '\core\plugininfo\general'; 459 } 460 } else { 461 $plugintypeclass = '\core\plugininfo\general'; 462 } 463 } 464 } else { 465 $class = '\core\plugininfo\\' . $type; 466 if (class_exists($class)) { 467 $plugintypeclass = $class; 468 } else { 469 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER); 470 $plugintypeclass = '\core\plugininfo\general'; 471 } 472 } 473 474 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) { 475 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base'); 476 } 477 478 return $plugintypeclass; 479 } 480 481 /** 482 * Returns list of all known subplugins of the given plugin. 483 * 484 * For plugins that do not provide subplugins (i.e. there is no support for it), 485 * empty array is returned. 486 * 487 * @param string $component full component name, e.g. 'mod_workshop' 488 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base} 489 */ 490 public function get_subplugins_of_plugin($component) { 491 492 $pluginfo = $this->get_plugin_info($component); 493 494 if (is_null($pluginfo)) { 495 return array(); 496 } 497 498 $subplugins = $this->get_subplugins(); 499 500 if (!isset($subplugins[$pluginfo->component])) { 501 return array(); 502 } 503 504 $list = array(); 505 506 foreach ($subplugins[$pluginfo->component] as $subdata) { 507 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) { 508 $list[$subpluginfo->component] = $subpluginfo; 509 } 510 } 511 512 return $list; 513 } 514 515 /** 516 * Returns list of plugins that define their subplugins and the information 517 * about them from the db/subplugins.json file. 518 * 519 * @return array with keys like 'mod_quiz', and values the data from the 520 * corresponding db/subplugins.json file. 521 */ 522 public function get_subplugins() { 523 524 if (is_array($this->subpluginsinfo)) { 525 return $this->subpluginsinfo; 526 } 527 528 $plugintypes = core_component::get_plugin_types(); 529 530 $this->subpluginsinfo = array(); 531 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) { 532 foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) { 533 $component = $type.'_'.$plugin; 534 $subplugins = core_component::get_subplugins($component); 535 if (!$subplugins) { 536 continue; 537 } 538 $this->subpluginsinfo[$component] = array(); 539 foreach ($subplugins as $subplugintype => $ignored) { 540 $subplugin = new stdClass(); 541 $subplugin->type = $subplugintype; 542 $subplugin->typerootdir = $plugintypes[$subplugintype]; 543 $this->subpluginsinfo[$component][$subplugintype] = $subplugin; 544 } 545 } 546 } 547 return $this->subpluginsinfo; 548 } 549 550 /** 551 * Returns the name of the plugin that defines the given subplugin type 552 * 553 * If the given subplugin type is not actually a subplugin, returns false. 554 * 555 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz 556 * @return false|string the name of the parent plugin, eg. mod_workshop 557 */ 558 public function get_parent_of_subplugin($subplugintype) { 559 $parent = core_component::get_subtype_parent($subplugintype); 560 if (!$parent) { 561 return false; 562 } 563 return $parent; 564 } 565 566 /** 567 * Returns a localized name of a given plugin 568 * 569 * @param string $component name of the plugin, eg mod_workshop or auth_ldap 570 * @return string 571 */ 572 public function plugin_name($component) { 573 574 $pluginfo = $this->get_plugin_info($component); 575 576 if (is_null($pluginfo)) { 577 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component)); 578 } 579 580 return $pluginfo->displayname; 581 } 582 583 /** 584 * Returns a localized name of a plugin typed in singular form 585 * 586 * Most plugin types define their names in core_plugin lang file. In case of subplugins, 587 * we try to ask the parent plugin for the name. In the worst case, we will return 588 * the value of the passed $type parameter. 589 * 590 * @param string $type the type of the plugin, e.g. mod or workshopform 591 * @return string 592 */ 593 public function plugintype_name($type) { 594 595 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) { 596 // For most plugin types, their names are defined in core_plugin lang file. 597 return get_string('type_' . $type, 'core_plugin'); 598 599 } else if ($parent = $this->get_parent_of_subplugin($type)) { 600 // If this is a subplugin, try to ask the parent plugin for the name. 601 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) { 602 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent); 603 } else { 604 return $this->plugin_name($parent) . ' / ' . $type; 605 } 606 607 } else { 608 return $type; 609 } 610 } 611 612 /** 613 * Returns a localized name of a plugin type in plural form 614 * 615 * Most plugin types define their names in core_plugin lang file. In case of subplugins, 616 * we try to ask the parent plugin for the name. In the worst case, we will return 617 * the value of the passed $type parameter. 618 * 619 * @param string $type the type of the plugin, e.g. mod or workshopform 620 * @return string 621 */ 622 public function plugintype_name_plural($type) { 623 624 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) { 625 // For most plugin types, their names are defined in core_plugin lang file. 626 return get_string('type_' . $type . '_plural', 'core_plugin'); 627 628 } else if ($parent = $this->get_parent_of_subplugin($type)) { 629 // If this is a subplugin, try to ask the parent plugin for the name. 630 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) { 631 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent); 632 } else { 633 return $this->plugin_name($parent) . ' / ' . $type; 634 } 635 636 } else { 637 return $type; 638 } 639 } 640 641 /** 642 * Returns information about the known plugin, or null 643 * 644 * @param string $component frankenstyle component name. 645 * @return \core\plugininfo\base|null the corresponding plugin information. 646 */ 647 public function get_plugin_info($component) { 648 list($type, $name) = core_component::normalize_component($component); 649 $plugins = $this->get_plugins_of_type($type); 650 if (isset($plugins[$name])) { 651 return $plugins[$name]; 652 } else { 653 return null; 654 } 655 } 656 657 /** 658 * Check to see if the current version of the plugin seems to be a checkout of an external repository. 659 * 660 * @param string $component frankenstyle component name 661 * @return false|string 662 */ 663 public function plugin_external_source($component) { 664 665 $plugininfo = $this->get_plugin_info($component); 666 667 if (is_null($plugininfo)) { 668 return false; 669 } 670 671 $pluginroot = $plugininfo->rootdir; 672 673 if (is_dir($pluginroot.'/.git')) { 674 return 'git'; 675 } 676 677 if (is_file($pluginroot.'/.git')) { 678 return 'git-submodule'; 679 } 680 681 if (is_dir($pluginroot.'/CVS')) { 682 return 'cvs'; 683 } 684 685 if (is_dir($pluginroot.'/.svn')) { 686 return 'svn'; 687 } 688 689 if (is_dir($pluginroot.'/.hg')) { 690 return 'mercurial'; 691 } 692 693 return false; 694 } 695 696 /** 697 * Get a list of any other plugins that require this one. 698 * @param string $component frankenstyle component name. 699 * @return array of frankensyle component names that require this one. 700 */ 701 public function other_plugins_that_require($component) { 702 $others = array(); 703 foreach ($this->get_plugins() as $type => $plugins) { 704 foreach ($plugins as $plugin) { 705 $required = $plugin->get_other_required_plugins(); 706 if (isset($required[$component])) { 707 $others[] = $plugin->component; 708 } 709 } 710 } 711 return $others; 712 } 713 714 /** 715 * Check a dependencies list against the list of installed plugins. 716 * @param array $dependencies compenent name to required version or ANY_VERSION. 717 * @return bool true if all the dependencies are satisfied. 718 */ 719 public function are_dependencies_satisfied($dependencies) { 720 foreach ($dependencies as $component => $requiredversion) { 721 $otherplugin = $this->get_plugin_info($component); 722 if (is_null($otherplugin)) { 723 return false; 724 } 725 726 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) { 727 return false; 728 } 729 } 730 731 return true; 732 } 733 734 /** 735 * Checks all dependencies for all installed plugins 736 * 737 * This is used by install and upgrade. The array passed by reference as the second 738 * argument is populated with the list of plugins that have failed dependencies (note that 739 * a single plugin can appear multiple times in the $failedplugins). 740 * 741 * @param int $moodleversion the version from version.php. 742 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies 743 * @param int $branch the current moodle branch, null if not provided 744 * @return bool true if all the dependencies are satisfied for all plugins. 745 */ 746 public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) { 747 global $CFG; 748 if (empty($branch)) { 749 $branch = $CFG->branch ?? ''; 750 if (empty($branch)) { 751 // During initial install there is no branch set. 752 require($CFG->dirroot . '/version.php'); 753 $branch = (int)$branch; 754 // Force CFG->branch to int value during install. 755 $CFG->branch = $branch; 756 } 757 } 758 $return = true; 759 foreach ($this->get_plugins() as $type => $plugins) { 760 foreach ($plugins as $plugin) { 761 762 if (!$plugin->is_core_dependency_satisfied($moodleversion)) { 763 $return = false; 764 $failedplugins[] = $plugin->component; 765 } 766 767 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) { 768 $return = false; 769 $failedplugins[] = $plugin->component; 770 } 771 772 if (!$plugin->is_core_compatible_satisfied($branch)) { 773 $return = false; 774 $failedplugins[] = $plugin->component; 775 } 776 } 777 } 778 779 return $return; 780 } 781 782 /** 783 * Resolve requirements and dependencies of a plugin. 784 * 785 * Returns an array of objects describing the requirement/dependency, 786 * indexed by the frankenstyle name of the component. The returned array 787 * can be empty. The objects in the array have following properties: 788 * 789 * ->(numeric)hasver 790 * ->(numeric)reqver 791 * ->(string)status 792 * ->(string)availability 793 * 794 * @param \core\plugininfo\base $plugin the plugin we are checking 795 * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version 796 * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch 797 * @return array of objects 798 */ 799 public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) { 800 global $CFG; 801 802 if ($plugin->versiondisk === null) { 803 // Missing from disk, we have no version.php to read from. 804 return array(); 805 } 806 807 if ($moodleversion === null) { 808 $moodleversion = $CFG->version; 809 } 810 811 if ($moodlebranch === null) { 812 $moodlebranch = $CFG->branch; 813 } 814 815 $reqs = array(); 816 $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch); 817 818 if (!empty($reqcore)) { 819 $reqs['core'] = $reqcore; 820 } 821 822 foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) { 823 $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch); 824 } 825 826 return $reqs; 827 } 828 829 /** 830 * Helper method to resolve plugin's requirements on the moodle core. 831 * 832 * @param \core\plugininfo\base $plugin the plugin we are checking 833 * @param string|int|double $moodleversion moodle core branch to check against 834 * @return stdObject 835 */ 836 protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) { 837 838 $reqs = (object)array( 839 'hasver' => null, 840 'reqver' => null, 841 'status' => null, 842 'availability' => null, 843 ); 844 $reqs->hasver = $moodleversion; 845 846 if (empty($plugin->versionrequires)) { 847 $reqs->reqver = ANY_VERSION; 848 } else { 849 $reqs->reqver = $plugin->versionrequires; 850 } 851 852 if ($plugin->is_core_dependency_satisfied($moodleversion)) { 853 $reqs->status = self::REQUIREMENT_STATUS_OK; 854 } else { 855 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED; 856 } 857 858 // Now check if there is an explicit incompatible, supersedes requires. 859 if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) { 860 if (!$plugin->is_core_compatible_satisfied($moodlebranch)) { 861 862 $reqs->status = self::REQUIREMENT_STATUS_NEWER; 863 } 864 } 865 866 return $reqs; 867 } 868 869 /** 870 * Helper method to resolve plugin's dependecies on other plugins. 871 * 872 * @param \core\plugininfo\base $plugin the plugin we are checking 873 * @param string $otherpluginname 874 * @param string|int $requiredversion 875 * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch 876 * @return stdClass 877 */ 878 protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname, 879 $requiredversion, $moodlebranch) { 880 881 $reqs = (object)array( 882 'hasver' => null, 883 'reqver' => null, 884 'status' => null, 885 'availability' => null, 886 ); 887 888 $otherplugin = $this->get_plugin_info($otherpluginname); 889 890 if ($otherplugin !== null) { 891 // The required plugin is installed. 892 $reqs->hasver = $otherplugin->versiondisk; 893 $reqs->reqver = $requiredversion; 894 // Check it has sufficient version. 895 if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) { 896 $reqs->status = self::REQUIREMENT_STATUS_OK; 897 } else { 898 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED; 899 } 900 901 } else { 902 // The required plugin is not installed. 903 $reqs->hasver = null; 904 $reqs->reqver = $requiredversion; 905 $reqs->status = self::REQUIREMENT_STATUS_MISSING; 906 } 907 908 if ($reqs->status !== self::REQUIREMENT_STATUS_OK) { 909 if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) { 910 $reqs->availability = self::REQUIREMENT_AVAILABLE; 911 } else { 912 $reqs->availability = self::REQUIREMENT_UNAVAILABLE; 913 } 914 } 915 916 return $reqs; 917 } 918 919 /** 920 * Helper method to determine whether a moodle version is explicitly supported. 921 * 922 * @param \core\plugininfo\base $plugin the plugin we are checking 923 * @param int $branch the moodle branch to check support for 924 * @return string 925 */ 926 public function check_explicitly_supported($plugin, $branch) : string { 927 // Check for correctly formed supported. 928 if (isset($plugin->pluginsupported)) { 929 // Broken apart for readability. 930 $error = false; 931 if (!is_array($plugin->pluginsupported)) { 932 $error = true; 933 } 934 if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) { 935 $error = true; 936 } 937 if (count($plugin->pluginsupported) != 2) { 938 $error = true; 939 } 940 if ($error) { 941 throw new coding_exception(get_string('err_supported_syntax', 'core_plugin')); 942 } 943 } 944 945 if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) { 946 if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) { 947 return self::VERSION_SUPPORTED; 948 } else { 949 return self::VERSION_NOT_SUPPORTED; 950 } 951 } else { 952 // If supports aren't specified, but incompatible is, return not supported if not incompatible. 953 if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) { 954 if (!$plugin->is_core_compatible_satisfied($branch)) { 955 return self::VERSION_NOT_SUPPORTED; 956 } 957 } 958 return self::VERSION_NO_SUPPORTS; 959 } 960 } 961 962 /** 963 * Is the given plugin version available in the plugins directory? 964 * 965 * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version 966 * parameter is interpretted. 967 * 968 * @param string $component plugin frankenstyle name 969 * @param string|int $version ANY_VERSION or the version number 970 * @param bool $exactmatch false if "given version or higher" is requested 971 * @return boolean 972 */ 973 public function is_remote_plugin_available($component, $version, $exactmatch) { 974 975 $info = $this->get_remote_plugin_info($component, $version, $exactmatch); 976 977 if (empty($info)) { 978 // There is no available plugin of that name. 979 return false; 980 } 981 982 if (empty($info->version)) { 983 // Plugin is known, but no suitable version was found. 984 return false; 985 } 986 987 return true; 988 } 989 990 /** 991 * Can the given plugin version be installed via the admin UI? 992 * 993 * This check should be used whenever attempting to install a plugin from 994 * the plugins directory (new install, available update, missing dependency). 995 * 996 * @param string $component 997 * @param int $version version number 998 * @param string $reason returned code of the reason why it is not 999 * @param bool $checkremote check this version availability on moodle server 1000 * @return boolean 1001 */ 1002 public function is_remote_plugin_installable($component, $version, &$reason = null, $checkremote = true) { 1003 global $CFG; 1004 1005 // Make sure the feature is not disabled. 1006 if (!empty($CFG->disableupdateautodeploy)) { 1007 $reason = 'disabled'; 1008 return false; 1009 } 1010 1011 // Make sure the version is available. 1012 if ($checkremote && !$this->is_remote_plugin_available($component, $version, true)) { 1013 $reason = 'remoteunavailable'; 1014 return false; 1015 } 1016 1017 // Make sure the plugin type root directory is writable. 1018 list($plugintype, $pluginname) = core_component::normalize_component($component); 1019 if (!$this->is_plugintype_writable($plugintype)) { 1020 $reason = 'notwritableplugintype'; 1021 return false; 1022 } 1023 1024 if (!$checkremote) { 1025 $remoteversion = $version; 1026 } else { 1027 $remoteinfo = $this->get_remote_plugin_info($component, $version, true); 1028 $remoteversion = $remoteinfo->version->version; 1029 } 1030 $localinfo = $this->get_plugin_info($component); 1031 1032 if ($localinfo) { 1033 // If the plugin is already present, prevent downgrade. 1034 if ($localinfo->versiondb > $remoteversion) { 1035 $reason = 'cannotdowngrade'; 1036 return false; 1037 } 1038 1039 // Make sure we have write access to all the existing code. 1040 if (is_dir($localinfo->rootdir)) { 1041 if (!$this->is_plugin_folder_removable($component)) { 1042 $reason = 'notwritableplugin'; 1043 return false; 1044 } 1045 } 1046 } 1047 1048 // Looks like it could work. 1049 return true; 1050 } 1051 1052 /** 1053 * Given the list of remote plugin infos, return just those installable. 1054 * 1055 * This is typically used on lists returned by 1056 * {@link self::available_updates()} or {@link self::missing_dependencies()} 1057 * to perform bulk installation of remote plugins. 1058 * 1059 * @param array $remoteinfos list of {@link \core\update\remote_info} 1060 * @return array 1061 */ 1062 public function filter_installable($remoteinfos) { 1063 global $CFG; 1064 1065 if (!empty($CFG->disableupdateautodeploy)) { 1066 return array(); 1067 } 1068 if (empty($remoteinfos)) { 1069 return array(); 1070 } 1071 $installable = array(); 1072 foreach ($remoteinfos as $index => $remoteinfo) { 1073 if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) { 1074 $installable[$index] = $remoteinfo; 1075 } 1076 } 1077 return $installable; 1078 } 1079 1080 /** 1081 * Returns information about a plugin in the plugins directory. 1082 * 1083 * This is typically used when checking for available dependencies (in 1084 * which case the $version represents minimal version we need), or 1085 * when installing an available update or a new plugin from the plugins 1086 * directory (in which case the $version is exact version we are 1087 * interested in). The interpretation of the $version is controlled 1088 * by the $exactmatch argument. 1089 * 1090 * If a plugin with the given component name is found, data about the 1091 * plugin are returned as an object. The ->version property of the object 1092 * contains the information about the particular plugin version that 1093 * matches best the given critera. The ->version property is false if no 1094 * suitable version of the plugin was found (yet the plugin itself is 1095 * known). 1096 * 1097 * See {@link \core\update\api::validate_pluginfo_format()} for the 1098 * returned data structure. 1099 * 1100 * @param string $component plugin frankenstyle name 1101 * @param string|int $version ANY_VERSION or the version number 1102 * @param bool $exactmatch false if "given version or higher" is requested 1103 * @return \core\update\remote_info|bool 1104 */ 1105 public function get_remote_plugin_info($component, $version, $exactmatch) { 1106 1107 if ($exactmatch and $version == ANY_VERSION) { 1108 throw new coding_exception('Invalid request for exactly any version, it does not make sense.'); 1109 } 1110 1111 $client = $this->get_update_api_client(); 1112 1113 if ($exactmatch) { 1114 // Use client's get_plugin_info() method. 1115 if (!isset($this->remotepluginsinfoexact[$component][$version])) { 1116 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version); 1117 } 1118 return $this->remotepluginsinfoexact[$component][$version]; 1119 1120 } else { 1121 // Use client's find_plugin() method. 1122 if (!isset($this->remotepluginsinfoatleast[$component][$version])) { 1123 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version); 1124 } 1125 return $this->remotepluginsinfoatleast[$component][$version]; 1126 } 1127 } 1128 1129 /** 1130 * Obtain the plugin ZIP file from the given URL 1131 * 1132 * The caller is supposed to know both downloads URL and the MD5 hash of 1133 * the ZIP contents in advance, typically by using the API requests against 1134 * the plugins directory. 1135 * 1136 * @param string $url 1137 * @param string $md5 1138 * @return string|bool full path to the file, false on error 1139 */ 1140 public function get_remote_plugin_zip($url, $md5) { 1141 global $CFG; 1142 1143 if (!empty($CFG->disableupdateautodeploy)) { 1144 return false; 1145 } 1146 return $this->get_code_manager()->get_remote_plugin_zip($url, $md5); 1147 } 1148 1149 /** 1150 * Extracts the saved plugin ZIP file. 1151 * 1152 * Returns the list of files found in the ZIP. The format of that list is 1153 * array of (string)filerelpath => (bool|string) where the array value is 1154 * either true or a string describing the problematic file. 1155 * 1156 * @see zip_packer::extract_to_pathname() 1157 * @param string $zipfilepath full path to the saved ZIP file 1158 * @param string $targetdir full path to the directory to extract the ZIP file to 1159 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value 1160 * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()} 1161 */ 1162 public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') { 1163 return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir); 1164 } 1165 1166 /** 1167 * Detects the plugin's name from its ZIP file. 1168 * 1169 * Plugin ZIP packages are expected to contain a single directory and the 1170 * directory name would become the plugin name once extracted to the Moodle 1171 * dirroot. 1172 * 1173 * @param string $zipfilepath full path to the ZIP files 1174 * @return string|bool false on error 1175 */ 1176 public function get_plugin_zip_root_dir($zipfilepath) { 1177 return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath); 1178 } 1179 1180 /** 1181 * Return a list of missing dependencies. 1182 * 1183 * This should provide the full list of plugins that should be installed to 1184 * fulfill the requirements of all plugins, if possible. 1185 * 1186 * @param bool $availableonly return only available missing dependencies 1187 * @return array of \core\update\remote_info|bool indexed by the component name 1188 */ 1189 public function missing_dependencies($availableonly=false) { 1190 1191 $dependencies = array(); 1192 1193 foreach ($this->get_plugins() as $plugintype => $pluginfos) { 1194 foreach ($pluginfos as $pluginname => $pluginfo) { 1195 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) { 1196 if ($reqname === 'core') { 1197 continue; 1198 } 1199 if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) { 1200 if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) { 1201 $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false); 1202 1203 if (empty($dependencies[$reqname])) { 1204 $dependencies[$reqname] = $remoteinfo; 1205 } else { 1206 // If resolving requirements has led to two different versions of the same 1207 // remote plugin, pick the higher version. This can happen in cases like one 1208 // plugin requiring ANY_VERSION and another plugin requiring specific higher 1209 // version with lower maturity of a remote plugin. 1210 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) { 1211 $dependencies[$reqname] = $remoteinfo; 1212 } 1213 } 1214 1215 } else { 1216 if (!isset($dependencies[$reqname])) { 1217 // Unable to find a plugin fulfilling the requirements. 1218 $dependencies[$reqname] = false; 1219 } 1220 } 1221 } 1222 } 1223 } 1224 } 1225 1226 if ($availableonly) { 1227 foreach ($dependencies as $component => $info) { 1228 if (empty($info) or empty($info->version)) { 1229 unset($dependencies[$component]); 1230 } 1231 } 1232 } 1233 1234 return $dependencies; 1235 } 1236 1237 /** 1238 * Is it possible to uninstall the given plugin? 1239 * 1240 * False is returned if the plugininfo subclass declares the uninstall should 1241 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the 1242 * core vetoes it (e.g. becase the plugin or some of its subplugins is required 1243 * by some other installed plugin). 1244 * 1245 * @param string $component full frankenstyle name, e.g. mod_foobar 1246 * @return bool 1247 */ 1248 public function can_uninstall_plugin($component) { 1249 1250 $pluginfo = $this->get_plugin_info($component); 1251 1252 if (is_null($pluginfo)) { 1253 return false; 1254 } 1255 1256 if (!$this->common_uninstall_check($pluginfo)) { 1257 return false; 1258 } 1259 1260 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()! 1261 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component); 1262 foreach ($subplugins as $subpluginfo) { 1263 // Check if there are some other plugins requiring this subplugin 1264 // (but the parent and siblings). 1265 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) { 1266 $ismyparent = ($pluginfo->component === $requiresme); 1267 $ismysibling = in_array($requiresme, array_keys($subplugins)); 1268 if (!$ismyparent and !$ismysibling) { 1269 return false; 1270 } 1271 } 1272 } 1273 1274 // Check if there are some other plugins requiring this plugin 1275 // (but its subplugins). 1276 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) { 1277 $ismysubplugin = in_array($requiresme, array_keys($subplugins)); 1278 if (!$ismysubplugin) { 1279 return false; 1280 } 1281 } 1282 1283 return true; 1284 } 1285 1286 /** 1287 * Perform the installation of plugins. 1288 * 1289 * If used for installation of remote plugins from the Moodle Plugins 1290 * directory, the $plugins must be list of {@link \core\update\remote_info} 1291 * object that represent installable remote plugins. The caller can use 1292 * {@link self::filter_installable()} to prepare the list. 1293 * 1294 * If used for installation of plugins from locally available ZIP files, 1295 * the $plugins should be list of objects with properties ->component and 1296 * ->zipfilepath. 1297 * 1298 * The method uses {@link mtrace()} to produce direct output and can be 1299 * used in both web and cli interfaces. 1300 * 1301 * @param array $plugins list of plugins 1302 * @param bool $confirmed should the files be really deployed into the dirroot? 1303 * @param bool $silent perform without output 1304 * @return bool true on success 1305 */ 1306 public function install_plugins(array $plugins, $confirmed, $silent) { 1307 global $CFG, $OUTPUT; 1308 1309 if (!empty($CFG->disableupdateautodeploy)) { 1310 return false; 1311 } 1312 1313 if (empty($plugins)) { 1314 return false; 1315 } 1316 1317 $ok = get_string('statusok', 'core'); 1318 1319 // Let admins know they can expect more verbose output. 1320 $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL); 1321 1322 // Download all ZIP packages if we do not have them yet. 1323 $zips = array(); 1324 foreach ($plugins as $plugin) { 1325 if ($plugin instanceof \core\update\remote_info) { 1326 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl, 1327 $plugin->version->downloadmd5); 1328 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... '); 1329 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER); 1330 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER); 1331 if (!$zips[$plugin->component]) { 1332 $silent or $this->mtrace(get_string('error')); 1333 return false; 1334 } 1335 $silent or $this->mtrace($ok); 1336 } else { 1337 if (empty($plugin->zipfilepath)) { 1338 throw new coding_exception('Unexpected data structure provided'); 1339 } 1340 $zips[$plugin->component] = $plugin->zipfilepath; 1341 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER); 1342 } 1343 } 1344 1345 // Validate all downloaded packages. 1346 foreach ($plugins as $plugin) { 1347 $zipfile = $zips[$plugin->component]; 1348 $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... '); 1349 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component); 1350 $tmp = make_request_directory(); 1351 $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname); 1352 if (empty($zipcontents)) { 1353 $silent or $this->mtrace(get_string('error')); 1354 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER); 1355 return false; 1356 } 1357 1358 $validator = \core\update\validator::instance($tmp, $zipcontents); 1359 $validator->assert_plugin_type($plugintype); 1360 $validator->assert_moodle_version($CFG->version); 1361 // TODO Check for missing dependencies during validation. 1362 $result = $validator->execute(); 1363 if (!$silent) { 1364 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error')); 1365 foreach ($validator->get_messages() as $message) { 1366 if ($message->level === $validator::INFO) { 1367 // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL. 1368 $level = DEBUG_NORMAL; 1369 } else if ($message->level === $validator::DEBUG) { 1370 // Display [Debug] validation messages only if debugging mode is DEBUG_ALL. 1371 $level = DEBUG_ALL; 1372 } else { 1373 // Display [Warning] and [Error] always. 1374 $level = null; 1375 } 1376 if ($message->level === $validator::WARNING and !CLI_SCRIPT) { 1377 $this->mtrace(' <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level); 1378 } else { 1379 $this->mtrace(' ['.$validator->message_level_name($message->level).']', ' ', $level); 1380 } 1381 $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level); 1382 $info = $validator->message_code_info($message->msgcode, $message->addinfo); 1383 if ($info) { 1384 $this->mtrace('['.s($info).']', ' ', $level); 1385 } else if (is_string($message->addinfo)) { 1386 $this->mtrace('['.s($message->addinfo, true).']', ' ', $level); 1387 } else { 1388 $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level); 1389 } 1390 if ($icon = $validator->message_help_icon($message->msgcode)) { 1391 if (CLI_SCRIPT) { 1392 $this->mtrace(PHP_EOL.' ^^^ '.get_string('help').': '. 1393 get_string($icon->identifier.'_help', $icon->component), '', $level); 1394 } else { 1395 $this->mtrace($OUTPUT->render($icon), ' ', $level); 1396 } 1397 } 1398 $this->mtrace(PHP_EOL, '', $level); 1399 } 1400 } 1401 if (!$result) { 1402 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin')); 1403 return false; 1404 } 1405 } 1406 $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin')); 1407 1408 if (!$confirmed) { 1409 return true; 1410 } 1411 1412 // Extract all ZIP packs do the dirroot. 1413 foreach ($plugins as $plugin) { 1414 $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... '); 1415 $zipfile = $zips[$plugin->component]; 1416 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component); 1417 $target = $this->get_plugintype_root($plugintype); 1418 if (file_exists($target.'/'.$pluginname)) { 1419 $this->remove_plugin_folder($this->get_plugin_info($plugin->component)); 1420 } 1421 if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) { 1422 $silent or $this->mtrace(get_string('error')); 1423 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER); 1424 if (function_exists('opcache_reset')) { 1425 opcache_reset(); 1426 } 1427 return false; 1428 } 1429 $silent or $this->mtrace($ok); 1430 } 1431 if (function_exists('opcache_reset')) { 1432 opcache_reset(); 1433 } 1434 1435 return true; 1436 } 1437 1438 /** 1439 * Outputs the given message via {@link mtrace()}. 1440 * 1441 * If $debug is provided, then the message is displayed only at the given 1442 * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the 1443 * site has developer debugging level selected). 1444 * 1445 * @param string $msg message 1446 * @param string $eol end of line 1447 * @param null|int $debug null to display always, int only on given debug level 1448 */ 1449 protected function mtrace($msg, $eol=PHP_EOL, $debug=null) { 1450 global $CFG; 1451 1452 if ($debug !== null and !debugging(null, $debug)) { 1453 return; 1454 } 1455 1456 mtrace($msg, $eol); 1457 } 1458 1459 /** 1460 * Returns uninstall URL if exists. 1461 * 1462 * @param string $component 1463 * @param string $return either 'overview' or 'manage' 1464 * @return moodle_url uninstall URL, null if uninstall not supported 1465 */ 1466 public function get_uninstall_url($component, $return = 'overview') { 1467 if (!$this->can_uninstall_plugin($component)) { 1468 return null; 1469 } 1470 1471 $pluginfo = $this->get_plugin_info($component); 1472 1473 if (is_null($pluginfo)) { 1474 return null; 1475 } 1476 1477 if (method_exists($pluginfo, 'get_uninstall_url')) { 1478 debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.'); 1479 return $pluginfo->get_uninstall_url($return); 1480 } 1481 1482 return $pluginfo->get_default_uninstall_url($return); 1483 } 1484 1485 /** 1486 * Uninstall the given plugin. 1487 * 1488 * Automatically cleans-up all remaining configuration data, log records, events, 1489 * files from the file pool etc. 1490 * 1491 * In the future, the functionality of {@link uninstall_plugin()} function may be moved 1492 * into this method and all the code should be refactored to use it. At the moment, we 1493 * mimic this future behaviour by wrapping that function call. 1494 * 1495 * @param string $component 1496 * @param progress_trace $progress traces the process 1497 * @return bool true on success, false on errors/problems 1498 */ 1499 public function uninstall_plugin($component, progress_trace $progress) { 1500 1501 $pluginfo = $this->get_plugin_info($component); 1502 1503 if (is_null($pluginfo)) { 1504 return false; 1505 } 1506 1507 // Give the pluginfo class a chance to execute some steps. 1508 $result = $pluginfo->uninstall($progress); 1509 if (!$result) { 1510 return false; 1511 } 1512 1513 // Call the legacy core function to uninstall the plugin. 1514 ob_start(); 1515 uninstall_plugin($pluginfo->type, $pluginfo->name); 1516 $progress->output(ob_get_clean()); 1517 1518 return true; 1519 } 1520 1521 /** 1522 * Checks if there are some plugins with a known available update 1523 * 1524 * @return bool true if there is at least one available update 1525 */ 1526 public function some_plugins_updatable() { 1527 foreach ($this->get_plugins() as $type => $plugins) { 1528 foreach ($plugins as $plugin) { 1529 if ($plugin->available_updates()) { 1530 return true; 1531 } 1532 } 1533 } 1534 1535 return false; 1536 } 1537 1538 /** 1539 * Returns list of available updates for the given component. 1540 * 1541 * This method should be considered as internal API and is supposed to be 1542 * called by {@link \core\plugininfo\base::available_updates()} only 1543 * to lazy load the data once they are first requested. 1544 * 1545 * @param string $component frankenstyle name of the plugin 1546 * @return null|array array of \core\update\info objects or null 1547 */ 1548 public function load_available_updates_for_plugin($component) { 1549 global $CFG; 1550 1551 $provider = \core\update\checker::instance(); 1552 1553 if (!$provider->enabled() or during_initial_install()) { 1554 return null; 1555 } 1556 1557 if (isset($CFG->updateminmaturity)) { 1558 $minmaturity = $CFG->updateminmaturity; 1559 } else { 1560 // This can happen during the very first upgrade to 2.3. 1561 $minmaturity = MATURITY_STABLE; 1562 } 1563 1564 return $provider->get_update_info($component, array('minmaturity' => $minmaturity)); 1565 } 1566 1567 /** 1568 * Returns a list of all available updates to be installed. 1569 * 1570 * This is used when "update all plugins" action is performed at the 1571 * administration UI screen. 1572 * 1573 * Returns array of remote info objects indexed by the plugin 1574 * component. If there are multiple updates available (typically a mix of 1575 * stable and non-stable ones), we pick the most mature most recent one. 1576 * 1577 * Plugins without explicit maturity are considered more mature than 1578 * release candidates but less mature than explicit stable (this should be 1579 * pretty rare case). 1580 * 1581 * @return array (string)component => (\core\update\remote_info)remoteinfo 1582 */ 1583 public function available_updates() { 1584 1585 $updates = array(); 1586 1587 foreach ($this->get_plugins() as $type => $plugins) { 1588 foreach ($plugins as $plugin) { 1589 $availableupdates = $plugin->available_updates(); 1590 if (empty($availableupdates)) { 1591 continue; 1592 } 1593 foreach ($availableupdates as $update) { 1594 if (empty($updates[$plugin->component])) { 1595 $updates[$plugin->component] = $update; 1596 continue; 1597 } 1598 $maturitycurrent = $updates[$plugin->component]->maturity; 1599 if (empty($maturitycurrent)) { 1600 $maturitycurrent = MATURITY_STABLE - 25; 1601 } 1602 $maturityremote = $update->maturity; 1603 if (empty($maturityremote)) { 1604 $maturityremote = MATURITY_STABLE - 25; 1605 } 1606 if ($maturityremote < $maturitycurrent) { 1607 continue; 1608 } 1609 if ($maturityremote > $maturitycurrent) { 1610 $updates[$plugin->component] = $update; 1611 continue; 1612 } 1613 if ($update->version > $updates[$plugin->component]->version) { 1614 $updates[$plugin->component] = $update; 1615 continue; 1616 } 1617 } 1618 } 1619 } 1620 1621 foreach ($updates as $component => $update) { 1622 $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true); 1623 if (empty($remoteinfo) or empty($remoteinfo->version)) { 1624 unset($updates[$component]); 1625 } else { 1626 $updates[$component] = $remoteinfo; 1627 } 1628 } 1629 1630 return $updates; 1631 } 1632 1633 /** 1634 * Check to see if the given plugin folder can be removed by the web server process. 1635 * 1636 * @param string $component full frankenstyle component 1637 * @return bool 1638 */ 1639 public function is_plugin_folder_removable($component) { 1640 1641 $pluginfo = $this->get_plugin_info($component); 1642 1643 if (is_null($pluginfo)) { 1644 return false; 1645 } 1646 1647 // To be able to remove the plugin folder, its parent must be writable, too. 1648 if (!isset($pluginfo->rootdir) || !is_writable(dirname($pluginfo->rootdir))) { 1649 return false; 1650 } 1651 1652 // Check that the folder and all its content is writable (thence removable). 1653 return $this->is_directory_removable($pluginfo->rootdir); 1654 } 1655 1656 /** 1657 * Is it possible to create a new plugin directory for the given plugin type? 1658 * 1659 * @throws coding_exception for invalid plugin types or non-existing plugin type locations 1660 * @param string $plugintype 1661 * @return boolean 1662 */ 1663 public function is_plugintype_writable($plugintype) { 1664 1665 $plugintypepath = $this->get_plugintype_root($plugintype); 1666 1667 if (is_null($plugintypepath)) { 1668 throw new coding_exception('Unknown plugin type: '.$plugintype); 1669 } 1670 1671 if ($plugintypepath === false) { 1672 throw new coding_exception('Plugin type location does not exist: '.$plugintype); 1673 } 1674 1675 return is_writable($plugintypepath); 1676 } 1677 1678 /** 1679 * Returns the full path of the root of the given plugin type 1680 * 1681 * Null is returned if the plugin type is not known. False is returned if 1682 * the plugin type root is expected but not found. Otherwise, string is 1683 * returned. 1684 * 1685 * @param string $plugintype 1686 * @return string|bool|null 1687 */ 1688 public function get_plugintype_root($plugintype) { 1689 1690 $plugintypepath = null; 1691 foreach (core_component::get_plugin_types() as $type => $fullpath) { 1692 if ($type === $plugintype) { 1693 $plugintypepath = $fullpath; 1694 break; 1695 } 1696 } 1697 if (is_null($plugintypepath)) { 1698 return null; 1699 } 1700 if (!is_dir($plugintypepath)) { 1701 return false; 1702 } 1703 1704 return $plugintypepath; 1705 } 1706 1707 /** 1708 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution, 1709 * but are not anymore and are deleted during upgrades. 1710 * 1711 * The main purpose of this list is to hide missing plugins during upgrade. 1712 * 1713 * @param string $type plugin type 1714 * @param string $name plugin name 1715 * @return bool 1716 */ 1717 public static function is_deleted_standard_plugin($type, $name) { 1718 // Do not include plugins that were removed during upgrades to versions that are 1719 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE 1720 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as 1721 // Moodle 2.3 supports upgrades from 2.2.x only. 1722 $plugins = array( 1723 'qformat' => array('blackboard', 'learnwise', 'examview'), 1724 'assignment' => array('offline', 'online', 'upload', 'uploadsingle'), 1725 'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'), 1726 'block' => array('course_overview', 'messages', 'community', 'participants', 'quiz_results'), 1727 'cachestore' => array('memcache', 'memcached', 'mongodb'), 1728 'editor' => array('tinymce'), 1729 'enrol' => array('authorize'), 1730 'filter' => array('censor'), 1731 'media' => array('swf'), 1732 'portfolio' => array('picasa', 'boxnet'), 1733 'qformat' => array('webct'), 1734 'message' => array('jabber'), 1735 'mod' => array('assignment'), 1736 'quizaccess' => array('safebrowser'), 1737 'report' => array('search'), 1738 'repository' => array('alfresco', 'picasa', 'skydrive', 'boxnet'), 1739 'tinymce' => array('dragmath', 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage', 1740 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap' 1741 ), 1742 1743 'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade', 'health'), 1744 'theme' => array('bootstrapbase', 'clean', 'more', 'afterburner', 'anomaly', 'arialist', 'base', 1745 'binarius', 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor', 'fusion', 'leatherbound', 1746 'magazine', 'mymobile', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash', 1747 'standard', 'standardold'), 1748 'logstore' => ['legacy'], 1749 'webservice' => array('amf', 'xmlrpc'), 1750 ); 1751 1752 if (!isset($plugins[$type])) { 1753 return false; 1754 } 1755 return in_array($name, $plugins[$type]); 1756 } 1757 1758 /** 1759 * Defines a white list of all plugins shipped in the standard Moodle distribution 1760 * 1761 * @param string $type 1762 * @return false|array array of standard plugins or false if the type is unknown 1763 */ 1764 public static function standard_plugins_list($type) { 1765 1766 $standard_plugins = array( 1767 1768 'antivirus' => array( 1769 'clamav' 1770 ), 1771 1772 'atto' => array( 1773 'accessibilitychecker', 'accessibilityhelper', 'align', 1774 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon', 1775 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic', 1776 'link', 'managefiles', 'media', 'noautolink', 'orderedlist', 1777 'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table', 1778 'title', 'underline', 'undo', 'unorderedlist', 'h5p', 'emojipicker', 1779 ), 1780 1781 'assignsubmission' => array( 1782 'comments', 'file', 'onlinetext' 1783 ), 1784 1785 'assignfeedback' => array( 1786 'comments', 'file', 'offline', 'editpdf' 1787 ), 1788 1789 'auth' => array( 1790 'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet', 1791 'nologin', 'none', 'oauth2', 'shibboleth', 'webservice' 1792 ), 1793 1794 'availability' => array( 1795 'completion', 'date', 'grade', 'group', 'grouping', 'profile' 1796 ), 1797 1798 'block' => array( 1799 'accessreview', 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges', 1800 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month', 1801 'calendar_upcoming', 'comments', 1802 'completionstatus', 'course_list', 'course_summary', 1803 'feedback', 'globalsearch', 'glossary_random', 'html', 1804 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile', 1805 'navigation', 'news_items', 'online_users', 1806 'private_files', 'recent_activity', 'recentlyaccesseditems', 1807 'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links', 1808 'selfcompletion', 'settings', 'site_main_menu', 1809 'social_activities', 'starredcourses', 'tag_flickr', 'tag_youtube', 'tags', 'timeline' 1810 ), 1811 1812 'booktool' => array( 1813 'exportimscp', 'importhtml', 'print' 1814 ), 1815 1816 'cachelock' => array( 1817 'file' 1818 ), 1819 1820 'cachestore' => array( 1821 'file', 'session', 'static', 'apcu', 'redis' 1822 ), 1823 1824 'calendartype' => array( 1825 'gregorian' 1826 ), 1827 1828 'contenttype' => array( 1829 'h5p' 1830 ), 1831 1832 'customfield' => array( 1833 'checkbox', 'date', 'select', 'text', 'textarea' 1834 ), 1835 1836 'coursereport' => array( 1837 // Deprecated! 1838 ), 1839 1840 'datafield' => array( 1841 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu', 1842 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url' 1843 ), 1844 1845 'dataformat' => array( 1846 'html', 'csv', 'json', 'excel', 'ods', 'pdf', 1847 ), 1848 1849 'datapreset' => array( 1850 'imagegallery', 1851 'journal', 1852 'proposals', 1853 'resources', 1854 ), 1855 1856 'fileconverter' => array( 1857 'unoconv', 'googledrive' 1858 ), 1859 1860 'editor' => array( 1861 'atto', 'textarea', 'tiny', 1862 ), 1863 1864 'enrol' => array( 1865 'category', 'cohort', 'database', 'flatfile', 1866 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet', 1867 'paypal', 'self', 'fee', 1868 ), 1869 1870 'filter' => array( 1871 'activitynames', 'algebra', 'emailprotect', 1872 'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy', 1873 'urltolink', 'data', 'glossary' 1874 ), 1875 1876 'format' => array( 1877 'singleactivity', 'social', 'topics', 'weeks' 1878 ), 1879 1880 'forumreport' => array( 1881 'summary', 1882 ), 1883 1884 'gradeexport' => array( 1885 'ods', 'txt', 'xls', 'xml' 1886 ), 1887 1888 'gradeimport' => array( 1889 'csv', 'direct', 'xml' 1890 ), 1891 1892 'gradereport' => array( 1893 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview', 'summary' 1894 ), 1895 1896 'gradingform' => array( 1897 'rubric', 'guide' 1898 ), 1899 1900 'h5plib' => array( 1901 'v124' 1902 ), 1903 1904 'local' => array( 1905 ), 1906 1907 'logstore' => array( 1908 'database', 'standard', 1909 ), 1910 1911 'ltiservice' => array( 1912 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes' 1913 ), 1914 1915 'mlbackend' => array( 1916 'php', 'python' 1917 ), 1918 1919 'media' => array( 1920 'html5audio', 'html5video', 'videojs', 'vimeo', 'youtube' 1921 ), 1922 1923 'message' => array( 1924 'airnotifier', 'email', 'popup' 1925 ), 1926 1927 'mnetservice' => array( 1928 'enrol' 1929 ), 1930 1931 'mod' => array( 1932 'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 1933 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page', 1934 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop' 1935 ), 1936 1937 'paygw' => [ 1938 'paypal', 1939 ], 1940 1941 'plagiarism' => array( 1942 ), 1943 1944 'portfolio' => array( 1945 'download', 'flickr', 'googledocs', 'mahara' 1946 ), 1947 1948 'profilefield' => array( 1949 'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea' 1950 ), 1951 1952 'qbank' => [ 1953 'bulkmove', 1954 'columnsortorder', 1955 'comment', 1956 'customfields', 1957 'deletequestion', 1958 'editquestion', 1959 'exporttoxml', 1960 'exportquestions', 1961 'history', 1962 'importquestions', 1963 'managecategories', 1964 'previewquestion', 1965 'statistics', 1966 'tagquestion', 1967 'usage', 1968 'viewcreator', 1969 'viewquestionname', 1970 'viewquestiontext', 1971 'viewquestiontype', 1972 ], 1973 1974 'qbehaviour' => array( 1975 'adaptive', 'adaptivenopenalty', 'deferredcbm', 1976 'deferredfeedback', 'immediatecbm', 'immediatefeedback', 1977 'informationitem', 'interactive', 'interactivecountback', 1978 'manualgraded', 'missing' 1979 ), 1980 1981 'qformat' => array( 1982 'aiken', 'blackboard_six', 'gift', 1983 'missingword', 'multianswer', 1984 'xhtml', 'xml' 1985 ), 1986 1987 'qtype' => array( 1988 'calculated', 'calculatedmulti', 'calculatedsimple', 1989 'ddimageortext', 'ddmarker', 'ddwtos', 'description', 1990 'essay', 'gapselect', 'match', 'missingtype', 'multianswer', 1991 'multichoice', 'numerical', 'random', 'randomsamatch', 1992 'shortanswer', 'truefalse' 1993 ), 1994 1995 'quiz' => array( 1996 'grading', 'overview', 'responses', 'statistics' 1997 ), 1998 1999 'quizaccess' => array( 2000 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate', 2001 'password', 'seb', 'securewindow', 'timelimit' 2002 ), 2003 2004 'report' => array( 2005 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist', 2006 'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 2007 'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions' 2008 ), 2009 2010 'repository' => array( 2011 'areafiles', 'contentbank', 'coursefiles', 'dropbox', 'equella', 'filesystem', 2012 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud', 2013 'onedrive', 'recent', 's3', 'upload', 'url', 'user', 'webdav', 2014 'wikimedia', 'youtube' 2015 ), 2016 2017 'search' => array( 2018 'simpledb', 'solr' 2019 ), 2020 2021 'scormreport' => array( 2022 'basic', 2023 'interactions', 2024 'graphs', 2025 'objectives' 2026 ), 2027 2028 'tiny' => [ 2029 'accessibilitychecker', 2030 'autosave', 2031 'equation', 2032 'h5p', 2033 'media', 2034 'recordrtc', 2035 'link' 2036 ], 2037 2038 'theme' => array( 2039 'boost', 'classic' 2040 ), 2041 2042 'tool' => array( 2043 'admin_presets', 'analytics', 'availabilityconditions', 'behat', 'brickfield', 'capability', 'cohortroles', 2044 'componentlibrary', 'customlang', 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'httpsreplace', 'innodb', 2045 'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 2046 'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin', 2047 'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles', 2048 'usertours', 'xmldb' 2049 ), 2050 2051 'webservice' => array( 2052 'rest', 'soap' 2053 ), 2054 2055 'workshopallocation' => array( 2056 'manual', 'random', 'scheduled' 2057 ), 2058 2059 'workshopeval' => array( 2060 'best' 2061 ), 2062 2063 'workshopform' => array( 2064 'accumulative', 'comments', 'numerrors', 'rubric' 2065 ) 2066 ); 2067 2068 if (isset($standard_plugins[$type])) { 2069 return $standard_plugins[$type]; 2070 } else { 2071 return false; 2072 } 2073 } 2074 2075 /** 2076 * Remove the current plugin code from the dirroot. 2077 * 2078 * If removing the currently installed version (which happens during 2079 * updates), we archive the code so that the upgrade can be cancelled. 2080 * 2081 * To prevent accidental data-loss, we also archive the existing plugin 2082 * code if cancelling installation of it, so that the developer does not 2083 * loose the only version of their work-in-progress. 2084 * 2085 * @param \core\plugininfo\base $plugin 2086 */ 2087 public function remove_plugin_folder(\core\plugininfo\base $plugin) { 2088 2089 if (!$this->is_plugin_folder_removable($plugin->component)) { 2090 throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '', 2091 array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir), 2092 'plugin root folder is not removable as expected'); 2093 } 2094 2095 if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) { 2096 $this->archive_plugin_version($plugin); 2097 } 2098 2099 remove_dir($plugin->rootdir); 2100 clearstatcache(); 2101 if (function_exists('opcache_reset')) { 2102 opcache_reset(); 2103 } 2104 } 2105 2106 /** 2107 * Can the installation of the new plugin be cancelled? 2108 * 2109 * Subplugins can be cancelled only via their parent plugin, not separately 2110 * (they are considered as implicit requirements if distributed together 2111 * with the main package). 2112 * 2113 * @param \core\plugininfo\base $plugin 2114 * @return bool 2115 */ 2116 public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) { 2117 global $CFG; 2118 2119 if (!empty($CFG->disableupdateautodeploy)) { 2120 return false; 2121 } 2122 2123 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin() 2124 or !$this->is_plugin_folder_removable($plugin->component)) { 2125 return false; 2126 } 2127 2128 if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) { 2129 return true; 2130 } 2131 2132 return false; 2133 } 2134 2135 /** 2136 * Can the upgrade of the existing plugin be cancelled? 2137 * 2138 * Subplugins can be cancelled only via their parent plugin, not separately 2139 * (they are considered as implicit requirements if distributed together 2140 * with the main package). 2141 * 2142 * @param \core\plugininfo\base $plugin 2143 * @return bool 2144 */ 2145 public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) { 2146 global $CFG; 2147 2148 if (!empty($CFG->disableupdateautodeploy)) { 2149 // Cancelling the plugin upgrade is actually installation of the 2150 // previously archived version. 2151 return false; 2152 } 2153 2154 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin() 2155 or !$this->is_plugin_folder_removable($plugin->component)) { 2156 return false; 2157 } 2158 2159 if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) { 2160 if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) { 2161 return true; 2162 } 2163 } 2164 2165 return false; 2166 } 2167 2168 /** 2169 * Removes the plugin code directory if it is not installed yet. 2170 * 2171 * This is intended for the plugins check screen to give the admin a chance 2172 * to cancel the installation of just unzipped plugin before the database 2173 * upgrade happens. 2174 * 2175 * @param string $component 2176 */ 2177 public function cancel_plugin_installation($component) { 2178 global $CFG; 2179 2180 if (!empty($CFG->disableupdateautodeploy)) { 2181 return false; 2182 } 2183 2184 $plugin = $this->get_plugin_info($component); 2185 2186 if ($this->can_cancel_plugin_installation($plugin)) { 2187 $this->remove_plugin_folder($plugin); 2188 } 2189 2190 return false; 2191 } 2192 2193 /** 2194 * Returns plugins, the installation of which can be cancelled. 2195 * 2196 * @return array [(string)component] => (\core\plugininfo\base)plugin 2197 */ 2198 public function list_cancellable_installations() { 2199 global $CFG; 2200 2201 if (!empty($CFG->disableupdateautodeploy)) { 2202 return array(); 2203 } 2204 2205 $cancellable = array(); 2206 foreach ($this->get_plugins() as $type => $plugins) { 2207 foreach ($plugins as $plugin) { 2208 if ($this->can_cancel_plugin_installation($plugin)) { 2209 $cancellable[$plugin->component] = $plugin; 2210 } 2211 } 2212 } 2213 2214 return $cancellable; 2215 } 2216 2217 /** 2218 * Archive the current on-disk plugin code. 2219 * 2220 * @param \core\plugiinfo\base $plugin 2221 * @return bool 2222 */ 2223 public function archive_plugin_version(\core\plugininfo\base $plugin) { 2224 return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk); 2225 } 2226 2227 /** 2228 * Returns list of all archives that can be installed to cancel the plugin upgrade. 2229 * 2230 * @return array [(string)component] => {(string)->component, (string)->zipfilepath} 2231 */ 2232 public function list_restorable_archives() { 2233 global $CFG; 2234 2235 if (!empty($CFG->disableupdateautodeploy)) { 2236 return false; 2237 } 2238 2239 $codeman = $this->get_code_manager(); 2240 $restorable = array(); 2241 foreach ($this->get_plugins() as $type => $plugins) { 2242 foreach ($plugins as $plugin) { 2243 if ($this->can_cancel_plugin_upgrade($plugin)) { 2244 $restorable[$plugin->component] = (object)array( 2245 'component' => $plugin->component, 2246 'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb) 2247 ); 2248 } 2249 } 2250 } 2251 2252 return $restorable; 2253 } 2254 2255 /** 2256 * Reorders plugin types into a sequence to be displayed 2257 * 2258 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are 2259 * in a certain order that does not need to fit the expected order for the display. 2260 * Particularly, activity modules should be displayed first as they represent the 2261 * real heart of Moodle. They should be followed by other plugin types that are 2262 * used to build the courses (as that is what one expects from LMS). After that, 2263 * other supportive plugin types follow. 2264 * 2265 * @param array $types associative array 2266 * @return array same array with altered order of items 2267 */ 2268 protected function reorder_plugin_types(array $types) { 2269 $fix = array('mod' => $types['mod']); 2270 foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) { 2271 if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) { 2272 continue; 2273 } 2274 foreach ($subtypes as $subtype => $ignored) { 2275 $fix[$subtype] = $types[$subtype]; 2276 } 2277 } 2278 2279 $fix['mod'] = $types['mod']; 2280 $fix['block'] = $types['block']; 2281 $fix['qtype'] = $types['qtype']; 2282 $fix['qbank'] = $types['qbank']; 2283 $fix['qbehaviour'] = $types['qbehaviour']; 2284 $fix['qformat'] = $types['qformat']; 2285 $fix['filter'] = $types['filter']; 2286 2287 $fix['editor'] = $types['editor']; 2288 foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) { 2289 if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) { 2290 continue; 2291 } 2292 foreach ($subtypes as $subtype => $ignored) { 2293 $fix[$subtype] = $types[$subtype]; 2294 } 2295 } 2296 2297 $fix['enrol'] = $types['enrol']; 2298 $fix['auth'] = $types['auth']; 2299 $fix['tool'] = $types['tool']; 2300 foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) { 2301 if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) { 2302 continue; 2303 } 2304 foreach ($subtypes as $subtype => $ignored) { 2305 $fix[$subtype] = $types[$subtype]; 2306 } 2307 } 2308 2309 foreach ($types as $type => $path) { 2310 if (!isset($fix[$type])) { 2311 $fix[$type] = $path; 2312 } 2313 } 2314 return $fix; 2315 } 2316 2317 /** 2318 * Check if the given directory can be removed by the web server process. 2319 * 2320 * This recursively checks that the given directory and all its contents 2321 * it writable. 2322 * 2323 * @param string $fullpath 2324 * @return boolean 2325 */ 2326 public function is_directory_removable($fullpath) { 2327 2328 if (!is_writable($fullpath)) { 2329 return false; 2330 } 2331 2332 if (is_dir($fullpath)) { 2333 $handle = opendir($fullpath); 2334 } else { 2335 return false; 2336 } 2337 2338 $result = true; 2339 2340 while ($filename = readdir($handle)) { 2341 2342 if ($filename === '.' or $filename === '..') { 2343 continue; 2344 } 2345 2346 $subfilepath = $fullpath.'/'.$filename; 2347 2348 if (is_dir($subfilepath)) { 2349 $result = $result && $this->is_directory_removable($subfilepath); 2350 2351 } else { 2352 $result = $result && is_writable($subfilepath); 2353 } 2354 } 2355 2356 closedir($handle); 2357 2358 return $result; 2359 } 2360 2361 /** 2362 * Helper method that implements common uninstall prerequisites 2363 * 2364 * @param \core\plugininfo\base $pluginfo 2365 * @return bool 2366 */ 2367 protected function common_uninstall_check(\core\plugininfo\base $pluginfo) { 2368 global $CFG; 2369 // Check if uninstall is allowed from the GUI. 2370 if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) { 2371 return false; 2372 } 2373 2374 if (!$pluginfo->is_uninstall_allowed()) { 2375 // The plugin's plugininfo class declares it should not be uninstalled. 2376 return false; 2377 } 2378 2379 if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) { 2380 // The plugin is not installed. It should be either installed or removed from the disk. 2381 // Relying on this temporary state may be tricky. 2382 return false; 2383 } 2384 2385 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) { 2386 // Backwards compatibility. 2387 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()', 2388 DEBUG_DEVELOPER); 2389 return false; 2390 } 2391 2392 return true; 2393 } 2394 2395 /** 2396 * Returns a code_manager instance to be used for the plugins code operations. 2397 * 2398 * @return \core\update\code_manager 2399 */ 2400 protected function get_code_manager() { 2401 2402 if ($this->codemanager === null) { 2403 $this->codemanager = new \core\update\code_manager(); 2404 } 2405 2406 return $this->codemanager; 2407 } 2408 2409 /** 2410 * Returns a client for https://download.moodle.org/api/ 2411 * 2412 * @return \core\update\api 2413 */ 2414 protected function get_update_api_client() { 2415 2416 if ($this->updateapiclient === null) { 2417 $this->updateapiclient = \core\update\api::client(); 2418 } 2419 2420 return $this->updateapiclient; 2421 } 2422 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body