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