Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Provides tool_installaddon_installer class.
  20   *
  21   * @package     tool_installaddon
  22   * @subpackage  classes
  23   * @copyright   2013 David Mudrak <david@moodle.com>
  24   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Implements main plugin features.
  31   *
  32   * @copyright 2013 David Mudrak <david@moodle.com>
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class tool_installaddon_installer {
  36  
  37      /** @var tool_installaddon_installfromzip_form */
  38      protected $installfromzipform = null;
  39  
  40      /**
  41       * Factory method returning an instance of this class.
  42       *
  43       * @return tool_installaddon_installer
  44       */
  45      public static function instance() {
  46          return new static();
  47      }
  48  
  49      /**
  50       * Returns the URL to the main page of this admin tool
  51       *
  52       * @param array optional parameters
  53       * @return moodle_url
  54       */
  55      public function index_url(array $params = null) {
  56          return new moodle_url('/admin/tool/installaddon/index.php', $params);
  57      }
  58  
  59      /**
  60       * Returns URL to the repository that addons can be searched in and installed from
  61       *
  62       * @return moodle_url
  63       */
  64      public function get_addons_repository_url() {
  65          global $CFG;
  66  
  67          if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) {
  68              $url = $CFG->config_php_settings['alternativeaddonsrepositoryurl'];
  69          } else {
  70              $url = 'https://moodle.org/plugins/get.php';
  71          }
  72  
  73          if (!$this->should_send_site_info()) {
  74              return new moodle_url($url);
  75          }
  76  
  77          // Append the basic information about our site.
  78          $site = array(
  79              'fullname' => $this->get_site_fullname(),
  80              'url' => $this->get_site_url(),
  81              'majorversion' => $this->get_site_major_version(),
  82          );
  83  
  84          $site = $this->encode_site_information($site);
  85  
  86          return new moodle_url($url, array('site' => $site));
  87      }
  88  
  89      /**
  90       * @return tool_installaddon_installfromzip_form
  91       */
  92      public function get_installfromzip_form() {
  93          if (!is_null($this->installfromzipform)) {
  94              return $this->installfromzipform;
  95          }
  96  
  97          $action = $this->index_url();
  98          $customdata = array('installer' => $this);
  99  
 100          $this->installfromzipform = new tool_installaddon_installfromzip_form($action, $customdata);
 101  
 102          return $this->installfromzipform;
 103      }
 104  
 105      /**
 106       * Makes a unique writable storage for uploaded ZIP packages.
 107       *
 108       * We need the saved ZIP to survive across multiple requests so that it can
 109       * be used by the plugin manager after the installation is confirmed. In
 110       * other words, we cannot use make_request_directory() here.
 111       *
 112       * @return string full path to the directory
 113       */
 114      public function make_installfromzip_storage() {
 115          return make_unique_writable_directory(make_temp_directory('tool_installaddon'));
 116      }
 117  
 118      /**
 119       * Returns localised list of available plugin types
 120       *
 121       * @return array (string)plugintype => (string)plugin name
 122       */
 123      public function get_plugin_types_menu() {
 124          global $CFG;
 125  
 126          $pluginman = core_plugin_manager::instance();
 127  
 128          $menu = array('' => get_string('choosedots'));
 129          foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
 130              $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
 131          }
 132  
 133          return $menu;
 134      }
 135  
 136      /**
 137       * Hook method to handle the remote request to install an add-on
 138       *
 139       * This is used as a callback when the admin picks a plugin version in the
 140       * Moodle Plugins directory and is redirected back to their site to install
 141       * it.
 142       *
 143       * This hook is called early from admin/tool/installaddon/index.php page so that
 144       * it has opportunity to take over the UI and display the first confirmation screen.
 145       *
 146       * @param tool_installaddon_renderer $output
 147       * @param string|null $request
 148       */
 149      public function handle_remote_request(tool_installaddon_renderer $output, $request) {
 150  
 151          if (is_null($request)) {
 152              return;
 153          }
 154  
 155          $data = $this->decode_remote_request($request);
 156  
 157          if ($data === false) {
 158              echo $output->remote_request_invalid_page($this->index_url());
 159              exit();
 160          }
 161  
 162          list($plugintype, $pluginname) = core_component::normalize_component($data->component);
 163          $pluginman = core_plugin_manager::instance();
 164  
 165          $plugintypepath = $pluginman->get_plugintype_root($plugintype);
 166  
 167          if (file_exists($plugintypepath.'/'.$pluginname)) {
 168              echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
 169              exit();
 170          }
 171  
 172          if (!$pluginman->is_plugintype_writable($plugintype)) {
 173              $continueurl = $this->index_url(array('installaddonrequest' => $request));
 174              echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
 175              exit();
 176          }
 177  
 178          if (!$pluginman->is_remote_plugin_installable($data->component, $data->version, $reason)) {
 179              $data->reason = $reason;
 180              echo $output->remote_request_non_installable_page($data, $this->index_url());
 181              exit();
 182          }
 183  
 184          $continueurl = $this->index_url(array(
 185              'installremote' => $data->component,
 186              'installremoteversion' => $data->version
 187          ));
 188  
 189          echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
 190          exit();
 191      }
 192  
 193      /**
 194       * Detect the given plugin's component name
 195       *
 196       * Only plugins that declare valid $plugin->component value in the version.php
 197       * are supported.
 198       *
 199       * @param string $zipfilepath full path to the saved ZIP file
 200       * @return string|bool declared component name or false if unable to detect
 201       */
 202      public function detect_plugin_component($zipfilepath) {
 203  
 204          $workdir = make_request_directory();
 205          $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir);
 206  
 207          if (empty($versionphp)) {
 208              return false;
 209          }
 210  
 211          return $this->detect_plugin_component_from_versionphp(file_get_contents($workdir.'/'.$versionphp));
 212      }
 213  
 214      //// End of external API ///////////////////////////////////////////////////
 215  
 216      /**
 217       * @see self::instance()
 218       */
 219      protected function __construct() {
 220      }
 221  
 222      /**
 223       * @return string this site full name
 224       */
 225      protected function get_site_fullname() {
 226          global $SITE;
 227  
 228          return strip_tags($SITE->fullname);
 229      }
 230  
 231      /**
 232       * @return string this site URL
 233       */
 234      protected function get_site_url() {
 235          global $CFG;
 236  
 237          return $CFG->wwwroot;
 238      }
 239  
 240      /**
 241       * @return string major version like 2.5, 2.6 etc.
 242       */
 243      protected function get_site_major_version() {
 244          return moodle_major_version();
 245      }
 246  
 247      /**
 248       * Encodes the given array in a way that can be safely appended as HTTP GET param
 249       *
 250       * Be ware! The recipient may rely on the exact way how the site information is encoded.
 251       * Do not change anything here unless you know what you are doing and understand all
 252       * consequences! (Don't you love warnings like that, too? :-p)
 253       *
 254       * @param array $info
 255       * @return string
 256       */
 257      protected function encode_site_information(array $info) {
 258          return base64_encode(json_encode($info));
 259      }
 260  
 261      /**
 262       * Decide if the encoded site information should be sent to the add-ons repository site
 263       *
 264       * For now, we just return true. In the future, we may want to implement some
 265       * privacy aware logic (based on site/user preferences for example).
 266       *
 267       * @return bool
 268       */
 269      protected function should_send_site_info() {
 270          return true;
 271      }
 272  
 273      /**
 274       * Decode the request from the Moodle Plugins directory
 275       *
 276       * @param string $request submitted via 'installaddonrequest' HTTP parameter
 277       * @return stdClass|bool false on error, object otherwise
 278       */
 279      protected function decode_remote_request($request) {
 280  
 281          $data = base64_decode($request, true);
 282  
 283          if ($data === false) {
 284              return false;
 285          }
 286  
 287          $data = json_decode($data);
 288  
 289          if (is_null($data)) {
 290              return false;
 291          }
 292  
 293          if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
 294              return false;
 295          }
 296  
 297          $data->name = s(strip_tags($data->name));
 298  
 299          if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
 300              return false;
 301          }
 302  
 303          list($plugintype, $pluginname) = core_component::normalize_component($data->component);
 304  
 305          if ($plugintype === 'core') {
 306              return false;
 307          }
 308  
 309          if ($data->component !== $plugintype.'_'.$pluginname) {
 310              return false;
 311          }
 312  
 313          if (!core_component::is_valid_plugin_name($plugintype, $pluginname)) {
 314              return false;
 315          }
 316  
 317          $plugintypes = core_component::get_plugin_types();
 318          if (!isset($plugintypes[$plugintype])) {
 319              return false;
 320          }
 321  
 322          // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
 323          if (!preg_match('/^[0-9]+$/', $data->version)) {
 324              return false;
 325          }
 326  
 327          return $data;
 328      }
 329  
 330      /**
 331       * Extracts the version.php from the given plugin ZIP file into the target directory
 332       *
 333       * @param string $zipfilepath full path to the saved ZIP file
 334       * @param string $targetdir full path to extract the file to
 335       * @return string|bool path to the version.php within the $targetpath; false on error (e.g. not found)
 336       */
 337      protected function extract_versionphp_file($zipfilepath, $targetdir) {
 338          global $CFG;
 339          require_once($CFG->libdir.'/filelib.php');
 340  
 341          $fp = get_file_packer('application/zip');
 342          $files = $fp->list_files($zipfilepath);
 343  
 344          if (empty($files)) {
 345              return false;
 346          }
 347  
 348          $rootdirname = null;
 349          $found = null;
 350  
 351          foreach ($files as $file) {
 352              // Valid plugin ZIP package has just one root directory with all
 353              // files in it.
 354              $pathnameitems = explode('/', $file->pathname);
 355  
 356              if (empty($pathnameitems)) {
 357                  return false;
 358              }
 359  
 360              // Set the expected name of the root directory in the first
 361              // iteration of the loop.
 362              if ($rootdirname === null) {
 363                  $rootdirname = $pathnameitems[0];
 364              }
 365  
 366              // Require the same root directory for all files in the ZIP
 367              // package.
 368              if ($rootdirname !== $pathnameitems[0]) {
 369                  return false;
 370              }
 371  
 372              // If we reached the valid version.php file, remember it.
 373              if ($pathnameitems[1] === 'version.php' and !$file->is_directory and $file->size > 0) {
 374                  $found = $file->pathname;
 375              }
 376          }
 377  
 378          if (empty($found)) {
 379              return false;
 380          }
 381  
 382          $extracted = $fp->extract_to_pathname($zipfilepath, $targetdir, array($found));
 383  
 384          if (empty($extracted)) {
 385              return false;
 386          }
 387  
 388          // The following syntax uses function array dereferencing, added in PHP 5.4.0.
 389          return array_keys($extracted)[0];
 390      }
 391  
 392      /**
 393       * Return the plugin component declared in its version.php file
 394       *
 395       * @param string $code the contents of the version.php file
 396       * @return string|bool declared plugin component or false if unable to detect
 397       */
 398      protected function detect_plugin_component_from_versionphp($code) {
 399  
 400          $result = preg_match_all('#^\s*\$plugin\->component\s*=\s*([\'"])(.+?_.+?)\1\s*;#m', $code, $matches);
 401  
 402          // Return if and only if the single match was detected.
 403          if ($result === 1 and !empty($matches[2][0])) {
 404              return $matches[2][0];
 405          }
 406  
 407          return false;
 408      }
 409  }