1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Defines classes used for plugin info.
19 *
20 * @package    core
21 * @copyright  2011 David Mudrak <david@moodle.com>
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24namespace core\plugininfo;
25
26use core_component, core_plugin_manager, moodle_url, coding_exception;
27
28defined('MOODLE_INTERNAL') || die();
29
30
31/**
32 * Base class providing access to the information about a plugin
33 *
34 * @property-read string component the component name, type_name
35 */
36abstract class base {
37
38    /** @var string the plugintype name, eg. mod, auth or workshopform */
39    public $type;
40    /** @var string full path to the location of all the plugins of this type */
41    public $typerootdir;
42    /** @var string the plugin name, eg. assignment, ldap */
43    public $name;
44    /** @var string the localized plugin name */
45    public $displayname;
46    /** @var string the plugin source, one of core_plugin_manager::PLUGIN_SOURCE_xxx constants */
47    public $source;
48    /** @var string fullpath to the location of this plugin */
49    public $rootdir;
50    /** @var int|string the version of the plugin's source code */
51    public $versiondisk;
52    /** @var int|string the version of the installed plugin */
53    public $versiondb;
54    /** @var int|float|string required version of Moodle core  */
55    public $versionrequires;
56    /** @var array explicitly supported branches of Moodle core  */
57    public $pluginsupported;
58    /** @var int first incompatible branch of Moodle core  */
59    public $pluginincompatible;
60    /** @var mixed human-readable release information */
61    public $release;
62    /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
63    public $dependencies;
64    /** @var int number of instances of the plugin - not supported yet */
65    public $instances;
66    /** @var int order of the plugin among other plugins of the same type - not supported yet */
67    public $sortorder;
68    /** @var core_plugin_manager the plugin manager this plugin info is part of */
69    public $pluginman;
70
71    /** @var array|null array of {@link \core\update\info} for this plugin */
72    protected $availableupdates;
73
74    /**
75     * Finds all enabled plugins, the result may include missing plugins.
76     * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
77     */
78    public static function get_enabled_plugins() {
79        return null;
80    }
81
82    /**
83     * Gathers and returns the information about all plugins of the given type,
84     * either on disk or previously installed.
85     *
86     * This is supposed to be used exclusively by the plugin manager when it is
87     * populating its tree of plugins.
88     *
89     * @param string $type the name of the plugintype, eg. mod, auth or workshopform
90     * @param string $typerootdir full path to the location of the plugin dir
91     * @param string $typeclass the name of the actually called class
92     * @param core_plugin_manager $pluginman the plugin manager calling this method
93     * @return array of plugintype classes, indexed by the plugin name
94     */
95    public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) {
96        // Get the information about plugins at the disk.
97        $plugins = core_component::get_plugin_list($type);
98        $return = array();
99        foreach ($plugins as $pluginname => $pluginrootdir) {
100            $return[$pluginname] = self::make_plugin_instance($type, $typerootdir,
101                $pluginname, $pluginrootdir, $typeclass, $pluginman);
102        }
103
104        // Fetch missing incorrectly uninstalled plugins.
105        $plugins = $pluginman->get_installed_plugins($type);
106
107        foreach ($plugins as $name => $version) {
108            if (isset($return[$name])) {
109                continue;
110            }
111            $plugin              = new $typeclass();
112            $plugin->type        = $type;
113            $plugin->typerootdir = $typerootdir;
114            $plugin->name        = $name;
115            $plugin->rootdir     = null;
116            $plugin->displayname = $name;
117            $plugin->versiondb   = $version;
118            $plugin->pluginman   = $pluginman;
119            $plugin->init_is_standard();
120
121            $return[$name] = $plugin;
122        }
123
124        return $return;
125    }
126
127    /**
128     * Makes a new instance of the plugininfo class
129     *
130     * @param string $type the plugin type, eg. 'mod'
131     * @param string $typerootdir full path to the location of all the plugins of this type
132     * @param string $name the plugin name, eg. 'workshop'
133     * @param string $namerootdir full path to the location of the plugin
134     * @param string $typeclass the name of class that holds the info about the plugin
135     * @param core_plugin_manager $pluginman the plugin manager of the new instance
136     * @return base the instance of $typeclass
137     */
138    protected static function make_plugin_instance($type, $typerootdir, $name, $namerootdir, $typeclass, $pluginman) {
139        $plugin              = new $typeclass();
140        $plugin->type        = $type;
141        $plugin->typerootdir = $typerootdir;
142        $plugin->name        = $name;
143        $plugin->rootdir     = $namerootdir;
144        $plugin->pluginman   = $pluginman;
145
146        $plugin->init_display_name();
147        $plugin->load_disk_version();
148        $plugin->load_db_version();
149        $plugin->init_is_standard();
150
151        return $plugin;
152    }
153
154    /**
155     * Is this plugin already installed and updated?
156     * @return bool true if plugin installed and upgraded.
157     */
158    public function is_installed_and_upgraded() {
159        if (!$this->rootdir) {
160            return false;
161        }
162        if ($this->versiondb === null and $this->versiondisk === null) {
163            // There is no version.php or version info inside it.
164            return false;
165        }
166
167        return ((float)$this->versiondb === (float)$this->versiondisk);
168    }
169
170    /**
171     * Sets {@link $displayname} property to a localized name of the plugin
172     */
173    public function init_display_name() {
174        if (!get_string_manager()->string_exists('pluginname', $this->component)) {
175            $this->displayname = '[pluginname,' . $this->component . ']';
176        } else {
177            $this->displayname = get_string('pluginname', $this->component);
178        }
179    }
180
181    /**
182     * Magic method getter, redirects to read only values.
183     *
184     * @param string $name
185     * @return mixed
186     */
187    public function __get($name) {
188        switch ($name) {
189            case 'component': return $this->type . '_' . $this->name;
190
191            default:
192                debugging('Invalid plugin property accessed! '.$name);
193                return null;
194        }
195    }
196
197    /**
198     * Return the full path name of a file within the plugin.
199     *
200     * No check is made to see if the file exists.
201     *
202     * @param string $relativepath e.g. 'version.php'.
203     * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
204     */
205    public function full_path($relativepath) {
206        if (empty($this->rootdir)) {
207            return '';
208        }
209        return $this->rootdir . '/' . $relativepath;
210    }
211
212    /**
213     * Sets {@link $versiondisk} property to a numerical value representing the
214     * version of the plugin's source code.
215     *
216     * If the value is null after calling this method, either the plugin
217     * does not use versioning (typically does not have any database
218     * data) or is missing from disk.
219     */
220    public function load_disk_version() {
221        $versions = $this->pluginman->get_present_plugins($this->type);
222
223        $this->versiondisk = null;
224        $this->versionrequires = null;
225        $this->pluginsupported = null;
226        $this->pluginincompatible = null;
227        $this->dependencies = array();
228
229        if (!isset($versions[$this->name])) {
230            return;
231        }
232
233        $plugin = $versions[$this->name];
234
235        if (isset($plugin->version)) {
236            $this->versiondisk = $plugin->version;
237        }
238        if (isset($plugin->requires)) {
239            $this->versionrequires = $plugin->requires;
240        }
241        if (isset($plugin->release)) {
242            $this->release = $plugin->release;
243        }
244        if (isset($plugin->dependencies)) {
245            $this->dependencies = $plugin->dependencies;
246        }
247
248        // Check that supports and incompatible are wellformed, exception otherwise.
249        if (isset($plugin->supported)) {
250            // Checks for structure of supported.
251            $isint = (is_int($plugin->supported[0]) && is_int($plugin->supported[1]));
252            $isrange = ($plugin->supported[0] <= $plugin->supported[1] && count($plugin->supported) == 2);
253
254            if (is_array($plugin->supported) && $isint && $isrange) {
255                $this->pluginsupported = $plugin->supported;
256            } else {
257                throw new coding_exception('Incorrect syntax in plugin supported declaration in '."$this->name");
258            }
259        }
260
261        if (isset($plugin->incompatible) && $plugin->incompatible !== null) {
262            if ((ctype_digit($plugin->incompatible) || is_int($plugin->incompatible)) && (int) $plugin->incompatible > 0) {
263                $this->pluginincompatible = intval($plugin->incompatible);
264            } else {
265                throw new coding_exception('Incorrect syntax in plugin incompatible declaration in '."$this->name");
266            }
267        }
268
269    }
270
271    /**
272     * Get the list of other plugins that this plugin requires to be installed.
273     *
274     * @return array with keys the frankenstyle plugin name, and values either
275     *      a version string (like '2011101700') or the constant ANY_VERSION.
276     */
277    public function get_other_required_plugins() {
278        if (is_null($this->dependencies)) {
279            $this->load_disk_version();
280        }
281        return $this->dependencies;
282    }
283
284    /**
285     * Is this is a subplugin?
286     *
287     * @return boolean
288     */
289    public function is_subplugin() {
290        return ($this->get_parent_plugin() !== false);
291    }
292
293    /**
294     * If I am a subplugin, return the name of my parent plugin.
295     *
296     * @return string|bool false if not a subplugin, name of the parent otherwise
297     */
298    public function get_parent_plugin() {
299        return $this->pluginman->get_parent_of_subplugin($this->type);
300    }
301
302    /**
303     * Sets {@link $versiondb} property to a numerical value representing the
304     * currently installed version of the plugin.
305     *
306     * If the value is null after calling this method, either the plugin
307     * does not use versioning (typically does not have any database
308     * data) or has not been installed yet.
309     */
310    public function load_db_version() {
311        $versions = $this->pluginman->get_installed_plugins($this->type);
312
313        if (isset($versions[$this->name])) {
314            $this->versiondb = $versions[$this->name];
315        } else {
316            $this->versiondb = null;
317        }
318    }
319
320    /**
321     * Sets {@link $source} property to one of core_plugin_manager::PLUGIN_SOURCE_xxx
322     * constants.
323     *
324     * If the property's value is null after calling this method, then
325     * the type of the plugin has not been recognized and you should throw
326     * an exception.
327     */
328    public function init_is_standard() {
329
330        $pluginman = $this->pluginman;
331        $standard = $pluginman::standard_plugins_list($this->type);
332
333        if ($standard !== false) {
334            $standard = array_flip($standard);
335            if (isset($standard[$this->name])) {
336                $this->source = core_plugin_manager::PLUGIN_SOURCE_STANDARD;
337            } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
338                and $pluginman::is_deleted_standard_plugin($this->type, $this->name)) {
339                $this->source = core_plugin_manager::PLUGIN_SOURCE_STANDARD; // To be deleted.
340            } else {
341                $this->source = core_plugin_manager::PLUGIN_SOURCE_EXTENSION;
342            }
343        }
344    }
345
346    /**
347     * Returns true if the plugin is shipped with the official distribution
348     * of the current Moodle version, false otherwise.
349     *
350     * @return bool
351     */
352    public function is_standard() {
353        return $this->source === core_plugin_manager::PLUGIN_SOURCE_STANDARD;
354    }
355
356    /**
357     * Returns true if the the given Moodle version is enough to run this plugin
358     *
359     * @param string|int|double $moodleversion
360     * @return bool
361     */
362    public function is_core_dependency_satisfied($moodleversion) {
363
364        if (empty($this->versionrequires)) {
365            return true;
366
367        } else {
368            return (double)$this->versionrequires <= (double)$moodleversion;
369        }
370    }
371
372    /**
373     * Returns true if the the given moodle branch is not stated incompatible with the plugin
374     *
375     * @param int $branch the moodle branch number
376     * @return bool true if not incompatible with moodle branch
377     */
378    public function is_core_compatible_satisfied(int $branch) : bool {
379        if (!empty($this->pluginincompatible) && ($branch >= $this->pluginincompatible)) {
380            return false;
381        } else {
382            return true;
383        }
384    }
385
386    /**
387     * Returns the status of the plugin
388     *
389     * @return string one of core_plugin_manager::PLUGIN_STATUS_xxx constants
390     */
391    public function get_status() {
392
393        $pluginman = $this->pluginman;
394
395        if (is_null($this->versiondb) and is_null($this->versiondisk)) {
396            return core_plugin_manager::PLUGIN_STATUS_NODB;
397
398        } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
399            return core_plugin_manager::PLUGIN_STATUS_NEW;
400
401        } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
402            if ($pluginman::is_deleted_standard_plugin($this->type, $this->name)) {
403                return core_plugin_manager::PLUGIN_STATUS_DELETE;
404            } else {
405                return core_plugin_manager::PLUGIN_STATUS_MISSING;
406            }
407
408        } else if ((float)$this->versiondb === (float)$this->versiondisk) {
409            // Note: the float comparison should work fine here
410            //       because there are no arithmetic operations with the numbers.
411            return core_plugin_manager::PLUGIN_STATUS_UPTODATE;
412
413        } else if ($this->versiondb < $this->versiondisk) {
414            return core_plugin_manager::PLUGIN_STATUS_UPGRADE;
415
416        } else if ($this->versiondb > $this->versiondisk) {
417            return core_plugin_manager::PLUGIN_STATUS_DOWNGRADE;
418
419        } else {
420            // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
421            throw new coding_exception('Unable to determine plugin state, check the plugin versions');
422        }
423    }
424
425    /**
426     * Returns the information about plugin availability
427     *
428     * True means that the plugin is enabled. False means that the plugin is
429     * disabled. Null means that the information is not available, or the
430     * plugin does not support configurable availability or the availability
431     * can not be changed.
432     *
433     * @return null|bool
434     */
435    public function is_enabled() {
436        if (!$this->rootdir) {
437            // Plugin missing.
438            return false;
439        }
440
441        $enabled = $this->pluginman->get_enabled_plugins($this->type);
442
443        if (!is_array($enabled)) {
444            return null;
445        }
446
447        return isset($enabled[$this->name]);
448    }
449
450    /**
451     * If there are updates for this plugin available, returns them.
452     *
453     * Returns array of {@link \core\update\info} objects, if some update
454     * is available. Returns null if there is no update available or if the update
455     * availability is unknown.
456     *
457     * Populates the property {@link $availableupdates} on first call (lazy
458     * loading).
459     *
460     * @return array|null
461     */
462    public function available_updates() {
463
464        if ($this->availableupdates === null) {
465            // Lazy load the information about available updates.
466            $this->availableupdates = $this->pluginman->load_available_updates_for_plugin($this->component);
467        }
468
469        if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
470            $this->availableupdates = array();
471            return null;
472        }
473
474        $updates = array();
475
476        foreach ($this->availableupdates as $availableupdate) {
477            if ($availableupdate->version > $this->versiondisk) {
478                $updates[] = $availableupdate;
479            }
480        }
481
482        if (empty($updates)) {
483            return null;
484        }
485
486        return $updates;
487    }
488
489    /**
490     * Returns the node name used in admin settings menu for this plugin settings (if applicable)
491     *
492     * @return null|string node name or null if plugin does not create settings node (default)
493     */
494    public function get_settings_section_name() {
495        return null;
496    }
497
498    /**
499     * Returns the URL of the plugin settings screen
500     *
501     * Null value means that the plugin either does not have the settings screen
502     * or its location is not available via this library.
503     *
504     * @return null|moodle_url
505     */
506    public function get_settings_url() {
507        $section = $this->get_settings_section_name();
508        if ($section === null) {
509            return null;
510        }
511        $settings = admin_get_root()->locate($section);
512        if ($settings) {
513            if ($settings instanceof \admin_settingpage) {
514                return new moodle_url('/admin/settings.php', [
515                    'section' => $section,
516                ]);
517            }
518
519            if ($settings instanceof \admin_externalpage) {
520                return new moodle_url($settings->url);
521            }
522
523            if ($settings instanceof \admin_category) {
524                return $settings->get_settings_page_url();
525            }
526        }
527        return null;
528    }
529
530    /**
531     * Loads plugin settings to the settings tree
532     *
533     * This function usually includes settings.php file in plugins folder.
534     * Alternatively it can create a link to some settings page (instance of admin_externalpage)
535     *
536     * @param \part_of_admin_tree $adminroot
537     * @param string $parentnodename
538     * @param bool $hassiteconfig whether the current user has moodle/site:config capability
539     */
540    public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
541    }
542
543    /**
544     * Should there be a way to uninstall the plugin via the administration UI.
545     *
546     * By default uninstallation is not allowed, plugin developers must enable it explicitly!
547     *
548     * @return bool
549     */
550    public function is_uninstall_allowed() {
551        return false;
552    }
553
554    /**
555     * Optional extra warning before uninstallation, for example number of uses in courses.
556     *
557     * @return string
558     */
559    public function get_uninstall_extra_warning() {
560        return '';
561    }
562
563    /**
564     * Pre-uninstall hook.
565     *
566     * This is intended for disabling of plugin, some DB table purging, etc.
567     *
568     * NOTE: to be called from uninstall_plugin() only.
569     * @private
570     */
571    public function uninstall_cleanup() {
572        // Override when extending class,
573        // do not forget to call parent::pre_uninstall_cleanup() at the end.
574    }
575
576    /**
577     * Returns relative directory of the plugin with heading '/'
578     *
579     * @return string
580     */
581    public function get_dir() {
582        global $CFG;
583
584        return substr($this->rootdir, strlen($CFG->dirroot));
585    }
586
587    /**
588     * Hook method to implement certain steps when uninstalling the plugin.
589     *
590     * This hook is called by {@link core_plugin_manager::uninstall_plugin()} so
591     * it is basically usable only for those plugin types that use the default
592     * uninstall tool provided by {@link self::get_default_uninstall_url()}.
593     *
594     * @param \progress_trace $progress traces the process
595     * @return bool true on success, false on failure
596     */
597    public function uninstall(\progress_trace $progress) {
598        return true;
599    }
600
601    /**
602     * Where should we return after plugin of this type is uninstalled?
603     * @param string $return
604     * @return moodle_url
605     */
606    public function get_return_url_after_uninstall($return) {
607        if ($return === 'manage') {
608            if ($url = $this->get_manage_url()) {
609                return $url;
610            }
611        }
612        return new moodle_url('/admin/plugins.php#plugin_type_cell_'.$this->type);
613    }
614
615    /**
616     * Return URL used for management of plugins of this type.
617     * @return moodle_url
618     */
619    public static function get_manage_url() {
620        return null;
621    }
622
623    /**
624     * Returns URL to a script that handles common plugin uninstall procedure.
625     *
626     * This URL is intended for all plugin uninstallations.
627     *
628     * @param string $return either 'overview' or 'manage'
629     * @return moodle_url
630     */
631    public final function get_default_uninstall_url($return = 'overview') {
632        return new moodle_url('/admin/plugins.php', array(
633            'uninstall' => $this->component,
634            'confirm' => 0,
635            'return' => $return,
636        ));
637    }
638}
639