Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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