PageRenderTime 63ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/classes/plugin_manager.php

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