[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/admin/tool/installaddon/classes/ -> installer.php (source)

   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 related classes
  20   *
  21   * @package     tool_installaddon
  22   * @subpackage  classes
  23   * @copyright   2013 David Mudrak <[email protected]>
  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 <[email protected]>
  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       * Saves the ZIP file from the {@link tool_installaddon_installfromzip_form} form
 107       *
 108       * The file is saved into the given temporary location for inspection and eventual
 109       * deployment. The form is expected to be submitted and validated.
 110       *
 111       * @param tool_installaddon_installfromzip_form $form
 112       * @param string $targetdir full path to the directory where the ZIP should be stored to
 113       * @return string filename of the saved file relative to the given target
 114       */
 115      public function save_installfromzip_file(tool_installaddon_installfromzip_form $form, $targetdir) {
 116  
 117          $filename = clean_param($form->get_new_filename('zipfile'), PARAM_FILE);
 118          $form->save_file('zipfile', $targetdir.'/'.$filename);
 119  
 120          return $filename;
 121      }
 122  
 123      /**
 124       * Extracts the saved file previously saved by {self::save_installfromzip_file()}
 125       *
 126       * The list of files found in the ZIP is returned via $zipcontentfiles parameter
 127       * by reference. The format of that list is array of (string)filerelpath => (bool|string)
 128       * where the array value is either true or a string describing the problematic file.
 129       *
 130       * @see zip_packer::extract_to_pathname()
 131       * @param string $zipfilepath full path to the saved ZIP file
 132       * @param string $targetdir full path to the directory to extract the ZIP file to
 133       * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
 134       * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
 135       */
 136      public function extract_installfromzip_file($zipfilepath, $targetdir, $rootdir = '') {
 137          global $CFG;
 138          require_once($CFG->libdir.'/filelib.php');
 139  
 140          $fp = get_file_packer('application/zip');
 141          $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
 142  
 143          if (!$files) {
 144              return array();
 145          }
 146  
 147          if (!empty($rootdir)) {
 148              $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
 149          }
 150  
 151          // Sometimes zip may not contain all parent directories, add them to make it consistent.
 152          foreach ($files as $path => $status) {
 153              if ($status !== true) {
 154                  continue;
 155              }
 156              $parts = explode('/', trim($path, '/'));
 157              while (array_pop($parts)) {
 158                  if (empty($parts)) {
 159                      break;
 160                  }
 161                  $dir = implode('/', $parts).'/';
 162                  if (!isset($files[$dir])) {
 163                      $files[$dir] = true;
 164                  }
 165              }
 166          }
 167  
 168          return $files;
 169      }
 170  
 171      /**
 172       * Returns localised list of available plugin types
 173       *
 174       * @return array (string)plugintype => (string)plugin name
 175       */
 176      public function get_plugin_types_menu() {
 177          global $CFG;
 178  
 179          $pluginman = core_plugin_manager::instance();
 180  
 181          $menu = array('' => get_string('choosedots'));
 182          foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
 183              $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
 184          }
 185  
 186          return $menu;
 187      }
 188  
 189      /**
 190       * Returns the full path of the root of the given plugin type
 191       *
 192       * Null is returned if the plugin type is not known. False is returned if the plugin type
 193       * root is expected but not found. Otherwise, string is returned.
 194       *
 195       * @param string $plugintype
 196       * @return string|bool|null
 197       */
 198      public function get_plugintype_root($plugintype) {
 199  
 200          $plugintypepath = null;
 201          foreach (core_component::get_plugin_types() as $type => $fullpath) {
 202              if ($type === $plugintype) {
 203                  $plugintypepath = $fullpath;
 204                  break;
 205              }
 206          }
 207          if (is_null($plugintypepath)) {
 208              return null;
 209          }
 210  
 211          if (!is_dir($plugintypepath)) {
 212              return false;
 213          }
 214  
 215          return $plugintypepath;
 216      }
 217  
 218      /**
 219       * Is it possible to create a new plugin directory for the given plugin type?
 220       *
 221       * @throws coding_exception for invalid plugin types or non-existing plugin type locations
 222       * @param string $plugintype
 223       * @return boolean
 224       */
 225      public function is_plugintype_writable($plugintype) {
 226  
 227          $plugintypepath = $this->get_plugintype_root($plugintype);
 228  
 229          if (is_null($plugintypepath)) {
 230              throw new coding_exception('Unknown plugin type!');
 231          }
 232  
 233          if ($plugintypepath === false) {
 234              throw new coding_exception('Plugin type location does not exist!');
 235          }
 236  
 237          return is_writable($plugintypepath);
 238      }
 239  
 240      /**
 241       * Hook method to handle the remote request to install an add-on
 242       *
 243       * This is used as a callback when the admin picks a plugin version in the
 244       * Moodle Plugins directory and is redirected back to their site to install
 245       * it.
 246       *
 247       * This hook is called early from admin/tool/installaddon/index.php page so that
 248       * it has opportunity to take over the UI.
 249       *
 250       * @param tool_installaddon_renderer $output
 251       * @param string|null $request
 252       * @param bool $confirmed
 253       */
 254      public function handle_remote_request(tool_installaddon_renderer $output, $request, $confirmed = false) {
 255          global $CFG;
 256          require_once(dirname(__FILE__).'/pluginfo_client.php');
 257  
 258          if (is_null($request)) {
 259              return;
 260          }
 261  
 262          $data = $this->decode_remote_request($request);
 263  
 264          if ($data === false) {
 265              echo $output->remote_request_invalid_page($this->index_url());
 266              exit();
 267          }
 268  
 269          list($plugintype, $pluginname) = core_component::normalize_component($data->component);
 270  
 271          $plugintypepath = $this->get_plugintype_root($plugintype);
 272  
 273          if (file_exists($plugintypepath.'/'.$pluginname)) {
 274              echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
 275              exit();
 276          }
 277  
 278          if (!$this->is_plugintype_writable($plugintype)) {
 279              $continueurl = $this->index_url(array('installaddonrequest' => $request));
 280              echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
 281              exit();
 282          }
 283  
 284          $continueurl = $this->index_url(array(
 285              'installaddonrequest' => $request,
 286              'confirm' => 1,
 287              'sesskey' => sesskey()));
 288  
 289          if (!$confirmed) {
 290              echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
 291              exit();
 292          }
 293  
 294          // The admin has confirmed their intention to install the add-on.
 295          require_sesskey();
 296  
 297          // Fetch the plugin info. The essential information is the URL to download the ZIP
 298          // and the MD5 hash of the ZIP, obtained via HTTPS.
 299          $client = tool_installaddon_pluginfo_client::instance();
 300  
 301          try {
 302              $pluginfo = $client->get_pluginfo($data->component, $data->version);
 303  
 304          } catch (tool_installaddon_pluginfo_exception $e) {
 305              if (debugging()) {
 306                  throw $e;
 307              } else {
 308                  echo $output->remote_request_pluginfo_exception($data, $e, $this->index_url());
 309                  exit();
 310              }
 311          }
 312  
 313          // Fetch the ZIP with the plugin version
 314          $jobid = md5(rand().uniqid('', true));
 315          $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
 316          $zipfilename = 'downloaded.zip';
 317  
 318          try {
 319              $this->download_file($pluginfo->downloadurl, $sourcedir.'/'.$zipfilename);
 320  
 321          } catch (tool_installaddon_installer_exception $e) {
 322              if (debugging()) {
 323                  throw $e;
 324              } else {
 325                  echo $output->installer_exception($e, $this->index_url());
 326                  exit();
 327              }
 328          }
 329  
 330          // Check the MD5 checksum
 331          $md5expected = $pluginfo->downloadmd5;
 332          $md5actual = md5_file($sourcedir.'/'.$zipfilename);
 333          if ($md5expected !== $md5actual) {
 334              $e = new tool_installaddon_installer_exception('err_zip_md5', array('expected' => $md5expected, 'actual' => $md5actual));
 335              if (debugging()) {
 336                  throw $e;
 337              } else {
 338                  echo $output->installer_exception($e, $this->index_url());
 339                  exit();
 340              }
 341          }
 342  
 343          // Redirect to the validation page.
 344          $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
 345              'sesskey' => sesskey(),
 346              'jobid' => $jobid,
 347              'zip' => $zipfilename,
 348              'type' => $plugintype));
 349          redirect($nexturl);
 350      }
 351  
 352      /**
 353       * Download the given file into the given destination.
 354       *
 355       * This is basically a simplified version of {@link download_file_content()} from
 356       * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used
 357       * in mdeploy.php for fetching available updates.
 358       *
 359       * @param string $source file url starting with http(s)://
 360       * @param string $target store the downloaded content to this file (full path)
 361       * @throws tool_installaddon_installer_exception
 362       */
 363      public function download_file($source, $target) {
 364          global $CFG;
 365          require_once($CFG->libdir.'/filelib.php');
 366  
 367          $targetfile = fopen($target, 'w');
 368  
 369          if (!$targetfile) {
 370              throw new tool_installaddon_installer_exception('err_download_write_file', $target);
 371          }
 372  
 373          $options = array(
 374              'file' => $targetfile,
 375              'timeout' => 300,
 376              'followlocation' => true,
 377              'maxredirs' => 3,
 378              'ssl_verifypeer' => true,
 379              'ssl_verifyhost' => 2,
 380          );
 381  
 382          $curl = new curl(array('proxy' => true));
 383  
 384          $result = $curl->download_one($source, null, $options);
 385  
 386          $curlinfo = $curl->get_info();
 387  
 388          fclose($targetfile);
 389  
 390          if ($result !== true) {
 391              throw new tool_installaddon_installer_exception('err_curl_exec', array(
 392                  'url' => $source, 'errorno' => $curl->get_errno(), 'error' => $result));
 393  
 394          } else if (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200) {
 395              throw new tool_installaddon_installer_exception('err_curl_http_code', array(
 396                  'url' => $source, 'http_code' => $curlinfo['http_code']));
 397  
 398          } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
 399              throw new tool_installaddon_installer_exception('err_curl_ssl_verify', array(
 400                  'url' => $source, 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
 401          }
 402      }
 403  
 404      /**
 405       * Moves the given source into a new location recursively
 406       *
 407       * This is cross-device safe implementation to be used instead of the native rename() function.
 408       * See https://bugs.php.net/bug.php?id=54097 for more details.
 409       *
 410       * @param string $source full path to the existing directory
 411       * @param string $target full path to the new location of the directory
 412       * @param int $dirpermissions
 413       * @param int $filepermissions
 414       */
 415      public function move_directory($source, $target, $dirpermissions, $filepermissions) {
 416  
 417          if (file_exists($target)) {
 418              throw new tool_installaddon_installer_exception('err_folder_already_exists', array('path' => $target));
 419          }
 420  
 421          if (is_dir($source)) {
 422              $handle = opendir($source);
 423          } else {
 424              throw new tool_installaddon_installer_exception('err_no_such_folder', array('path' => $source));
 425          }
 426  
 427          if (!file_exists($target)) {
 428              // Do not use make_writable_directory() here - it is intended for dataroot only.
 429              mkdir($target, true);
 430              @chmod($target, $dirpermissions);
 431          }
 432  
 433          if (!is_writable($target)) {
 434              closedir($handle);
 435              throw new tool_installaddon_installer_exception('err_folder_not_writable', array('path' => $target));
 436          }
 437  
 438          while ($filename = readdir($handle)) {
 439              $sourcepath = $source.'/'.$filename;
 440              $targetpath = $target.'/'.$filename;
 441  
 442              if ($filename === '.' or $filename === '..') {
 443                  continue;
 444              }
 445  
 446              if (is_dir($sourcepath)) {
 447                  $this->move_directory($sourcepath, $targetpath, $dirpermissions, $filepermissions);
 448  
 449              } else {
 450                  rename($sourcepath, $targetpath);
 451                  @chmod($targetpath, $filepermissions);
 452              }
 453          }
 454  
 455          closedir($handle);
 456  
 457          rmdir($source);
 458  
 459          clearstatcache();
 460      }
 461  
 462      //// End of external API ///////////////////////////////////////////////////
 463  
 464      /**
 465       * @see self::instance()
 466       */
 467      protected function __construct() {
 468      }
 469  
 470      /**
 471       * @return string this site full name
 472       */
 473      protected function get_site_fullname() {
 474          global $SITE;
 475  
 476          return strip_tags($SITE->fullname);
 477      }
 478  
 479      /**
 480       * @return string this site URL
 481       */
 482      protected function get_site_url() {
 483          global $CFG;
 484  
 485          return $CFG->wwwroot;
 486      }
 487  
 488      /**
 489       * @return string major version like 2.5, 2.6 etc.
 490       */
 491      protected function get_site_major_version() {
 492          return moodle_major_version();
 493      }
 494  
 495      /**
 496       * Encodes the given array in a way that can be safely appended as HTTP GET param
 497       *
 498       * Be ware! The recipient may rely on the exact way how the site information is encoded.
 499       * Do not change anything here unless you know what you are doing and understand all
 500       * consequences! (Don't you love warnings like that, too? :-p)
 501       *
 502       * @param array $info
 503       * @return string
 504       */
 505      protected function encode_site_information(array $info) {
 506          return base64_encode(json_encode($info));
 507      }
 508  
 509      /**
 510       * Decide if the encoded site information should be sent to the add-ons repository site
 511       *
 512       * For now, we just return true. In the future, we may want to implement some
 513       * privacy aware logic (based on site/user preferences for example).
 514       *
 515       * @return bool
 516       */
 517      protected function should_send_site_info() {
 518          return true;
 519      }
 520  
 521      /**
 522       * Renames the root directory of the extracted ZIP package.
 523       *
 524       * This method does not validate the presence of the single root directory
 525       * (the validator does it later). It just searches for the first directory
 526       * under the given location and renames it.
 527       *
 528       * The method will not rename the root if the requested location already
 529       * exists.
 530       *
 531       * @param string $dirname the location of the extracted ZIP package
 532       * @param string $rootdir the requested name of the root directory
 533       * @param array $files list of extracted files
 534       * @return array eventually amended list of extracted files
 535       */
 536      protected function rename_extracted_rootdir($dirname, $rootdir, array $files) {
 537  
 538          if (!is_dir($dirname)) {
 539              debugging('Unable to rename rootdir of non-existing content', DEBUG_DEVELOPER);
 540              return $files;
 541          }
 542  
 543          if (file_exists($dirname.'/'.$rootdir)) {
 544              debugging('Unable to rename rootdir to already existing folder', DEBUG_DEVELOPER);
 545              return $files;
 546          }
 547  
 548          $found = null; // The name of the first subdirectory under the $dirname.
 549          foreach (scandir($dirname) as $item) {
 550              if (substr($item, 0, 1) === '.') {
 551                  continue;
 552              }
 553              if (is_dir($dirname.'/'.$item)) {
 554                  $found = $item;
 555                  break;
 556              }
 557          }
 558  
 559          if (!is_null($found)) {
 560              if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) {
 561                  $newfiles = array();
 562                  foreach ($files as $filepath => $status) {
 563                      $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath);
 564                      $newfiles[$newpath] = $status;
 565                  }
 566                  return $newfiles;
 567              }
 568          }
 569  
 570          return $files;
 571      }
 572  
 573      /**
 574       * Decode the request from the Moodle Plugins directory
 575       *
 576       * @param string $request submitted via 'installaddonrequest' HTTP parameter
 577       * @return stdClass|bool false on error, object otherwise
 578       */
 579      protected function decode_remote_request($request) {
 580  
 581          $data = base64_decode($request, true);
 582  
 583          if ($data === false) {
 584              return false;
 585          }
 586  
 587          $data = json_decode($data);
 588  
 589          if (is_null($data)) {
 590              return false;
 591          }
 592  
 593          if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
 594              return false;
 595          }
 596  
 597          $data->name = s(strip_tags($data->name));
 598  
 599          if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
 600              return false;
 601          }
 602  
 603          list($plugintype, $pluginname) = core_component::normalize_component($data->component);
 604  
 605          if ($plugintype === 'core') {
 606              return false;
 607          }
 608  
 609          if ($data->component !== $plugintype.'_'.$pluginname) {
 610              return false;
 611          }
 612  
 613          if (!core_component::is_valid_plugin_name($plugintype, $pluginname)) {
 614              return false;
 615          }
 616  
 617          $plugintypes = core_component::get_plugin_types();
 618          if (!isset($plugintypes[$plugintype])) {
 619              return false;
 620          }
 621  
 622          // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
 623          if (!preg_match('/^[0-9]+$/', $data->version)) {
 624              return false;
 625          }
 626  
 627          return $data;
 628      }
 629  }
 630  
 631  
 632  /**
 633   * General exception thrown by {@link tool_installaddon_installer} class
 634   *
 635   * @copyright 2013 David Mudrak <[email protected]>
 636   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 637   */
 638  class tool_installaddon_installer_exception extends moodle_exception {
 639  
 640      /**
 641       * @param string $errorcode exception description identifier
 642       * @param mixed $debuginfo debugging data to display
 643       */
 644      public function __construct($errorcode, $a=null, $debuginfo=null) {
 645          parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
 646      }
 647  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1