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