[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/classes/update/ -> deployer.php (source)

   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 updates.
  19   *
  20   * @package    core
  21   * @copyright  2011 David Mudrak <[email protected]>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core\update;
  25  
  26  use coding_exception, core_component, moodle_url;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * Implements a communication bridge to the mdeploy.php utility
  32   */
  33  class deployer {
  34  
  35      /** @var \core\update\deployer holds the singleton instance */
  36      protected static $singletoninstance;
  37      /** @var moodle_url URL of a page that includes the deployer UI */
  38      protected $callerurl;
  39      /** @var moodle_url URL to return after the deployment */
  40      protected $returnurl;
  41  
  42      /**
  43       * Direct instantiation not allowed, use the factory method {@link self::instance()}
  44       */
  45      protected function __construct() {
  46      }
  47  
  48      /**
  49       * Sorry, this is singleton
  50       */
  51      protected function __clone() {
  52      }
  53  
  54      /**
  55       * Factory method for this class
  56       *
  57       * @return \core\update\deployer the singleton instance
  58       */
  59      public static function instance() {
  60          if (is_null(self::$singletoninstance)) {
  61              self::$singletoninstance = new self();
  62          }
  63          return self::$singletoninstance;
  64      }
  65  
  66      /**
  67       * Reset caches used by this script
  68       *
  69       * @param bool $phpunitreset is this called as a part of PHPUnit reset?
  70       */
  71      public static function reset_caches($phpunitreset = false) {
  72          if ($phpunitreset) {
  73              self::$singletoninstance = null;
  74          }
  75      }
  76  
  77      /**
  78       * Is automatic deployment enabled?
  79       *
  80       * @return bool
  81       */
  82      public function enabled() {
  83          global $CFG;
  84  
  85          if (!empty($CFG->disableupdateautodeploy)) {
  86              // The feature is prohibited via config.php.
  87              return false;
  88          }
  89  
  90          return get_config('updateautodeploy');
  91      }
  92  
  93      /**
  94       * Sets some base properties of the class to make it usable.
  95       *
  96       * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
  97       * @param moodle_url $returnurl the final URL to return to when the deployment is finished
  98       */
  99      public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
 100  
 101          if (!$this->enabled()) {
 102              throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
 103          }
 104  
 105          $this->callerurl = $callerurl;
 106          $this->returnurl = $returnurl;
 107      }
 108  
 109      /**
 110       * Has the deployer been initialized?
 111       *
 112       * Initialized deployer means that the following properties were set:
 113       * callerurl, returnurl
 114       *
 115       * @return bool
 116       */
 117      public function initialized() {
 118  
 119          if (!$this->enabled()) {
 120              return false;
 121          }
 122  
 123          if (empty($this->callerurl)) {
 124              return false;
 125          }
 126  
 127          if (empty($this->returnurl)) {
 128              return false;
 129          }
 130  
 131          return true;
 132      }
 133  
 134      /**
 135       * Returns a list of reasons why the deployment can not happen
 136       *
 137       * If the returned array is empty, the deployment seems to be possible. The returned
 138       * structure is an associative array with keys representing individual impediments.
 139       * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
 140       *
 141       * @param \core\update\info $info
 142       * @return array
 143       */
 144      public function deployment_impediments(info $info) {
 145  
 146          $impediments = array();
 147  
 148          if (empty($info->download)) {
 149              $impediments['missingdownloadurl'] = true;
 150          }
 151  
 152          if (empty($info->downloadmd5)) {
 153              $impediments['missingdownloadmd5'] = true;
 154          }
 155  
 156          if (!empty($info->download) and !$this->update_downloadable($info->download)) {
 157              $impediments['notdownloadable'] = true;
 158          }
 159  
 160          if (!$this->component_writable($info->component)) {
 161              $impediments['notwritable'] = true;
 162          }
 163  
 164          return $impediments;
 165      }
 166  
 167      /**
 168       * Check to see if the current version of the plugin seems to be a checkout of an external repository.
 169       *
 170       * @see core_plugin_manager::plugin_external_source()
 171       * @param \core\update\info $info
 172       * @return false|string
 173       */
 174      public function plugin_external_source(info $info) {
 175  
 176          $paths = core_component::get_plugin_types();
 177          list($plugintype, $pluginname) = core_component::normalize_component($info->component);
 178          $pluginroot = $paths[$plugintype].'/'.$pluginname;
 179  
 180          if (is_dir($pluginroot.'/.git')) {
 181              return 'git';
 182          }
 183  
 184          if (is_file($pluginroot.'/.git')) {
 185              return 'git-submodule';
 186          }
 187  
 188          if (is_dir($pluginroot.'/CVS')) {
 189              return 'cvs';
 190          }
 191  
 192          if (is_dir($pluginroot.'/.svn')) {
 193              return 'svn';
 194          }
 195  
 196          if (is_dir($pluginroot.'/.hg')) {
 197              return 'mercurial';
 198          }
 199  
 200          return false;
 201      }
 202  
 203      /**
 204       * Prepares a renderable widget to confirm installation of an available update.
 205       *
 206       * @param \core\update\info $info component version to deploy
 207       * @return \renderable
 208       */
 209      public function make_confirm_widget(info $info) {
 210  
 211          if (!$this->initialized()) {
 212              throw new coding_exception('Illegal method call - deployer not initialized.');
 213          }
 214  
 215          $params = array(
 216              'updateaddon' => $info->component,
 217              'version' =>$info->version,
 218              'sesskey' => sesskey(),
 219          );
 220  
 221          // Append some our own data.
 222          if (!empty($this->callerurl)) {
 223              $params['callerurl'] = $this->callerurl->out(false);
 224          }
 225          if (!empty($this->returnurl)) {
 226              $params['returnurl'] = $this->returnurl->out(false);
 227          }
 228  
 229          $widget = new \single_button(
 230              new moodle_url($this->callerurl, $params),
 231              get_string('updateavailableinstall', 'core_admin'),
 232              'post'
 233          );
 234  
 235          return $widget;
 236      }
 237  
 238      /**
 239       * Prepares a renderable widget to execute installation of an available update.
 240       *
 241       * @param \core\update\info $info component version to deploy
 242       * @param moodle_url $returnurl URL to return after the installation execution
 243       * @return \renderable
 244       */
 245      public function make_execution_widget(info $info, moodle_url $returnurl = null) {
 246          global $CFG;
 247  
 248          if (!$this->initialized()) {
 249              throw new coding_exception('Illegal method call - deployer not initialized.');
 250          }
 251  
 252          $pluginrootpaths = core_component::get_plugin_types();
 253  
 254          list($plugintype, $pluginname) = core_component::normalize_component($info->component);
 255  
 256          if (empty($pluginrootpaths[$plugintype])) {
 257              throw new coding_exception('Unknown plugin type root location', $plugintype);
 258          }
 259  
 260          list($passfile, $password) = $this->prepare_authorization();
 261  
 262          if (is_null($returnurl)) {
 263              $returnurl = new moodle_url('/admin');
 264          } else {
 265              $returnurl = $returnurl;
 266          }
 267  
 268          $params = array(
 269              'upgrade' => true,
 270              'type' => $plugintype,
 271              'name' => $pluginname,
 272              'typeroot' => $pluginrootpaths[$plugintype],
 273              'package' => $info->download,
 274              'md5' => $info->downloadmd5,
 275              'dataroot' => $CFG->dataroot,
 276              'dirroot' => $CFG->dirroot,
 277              'passfile' => $passfile,
 278              'password' => $password,
 279              'returnurl' => $returnurl->out(false),
 280          );
 281  
 282          if (!empty($CFG->proxyhost)) {
 283              // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
 284              // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
 285              // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
 286              // fixed, the condition should be amended.
 287              if (true or !is_proxybypass($info->download)) {
 288                  if (empty($CFG->proxyport)) {
 289                      $params['proxy'] = $CFG->proxyhost;
 290                  } else {
 291                      $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
 292                  }
 293  
 294                  if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
 295                      $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
 296                  }
 297  
 298                  if (!empty($CFG->proxytype)) {
 299                      $params['proxytype'] = $CFG->proxytype;
 300                  }
 301              }
 302          }
 303  
 304          $widget = new \single_button(
 305              new moodle_url('/mdeploy.php', $params),
 306              get_string('updateavailableinstall', 'core_admin'),
 307              'post'
 308          );
 309  
 310          return $widget;
 311      }
 312  
 313      /**
 314       * Returns array of data objects passed to this tool.
 315       *
 316       * @return array
 317       */
 318      public function submitted_data() {
 319          $component = optional_param('updateaddon', '', PARAM_COMPONENT);
 320          $version = optional_param('version', '', PARAM_RAW);
 321          if (!$component or !$version) {
 322              return false;
 323          }
 324  
 325          $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component);
 326          if (!$plugininfo) {
 327              return false;
 328          }
 329  
 330          if ($plugininfo->is_standard()) {
 331              return false;
 332          }
 333  
 334          if (!$updates = $plugininfo->available_updates()) {
 335              return false;
 336          }
 337  
 338          $info = null;
 339          foreach ($updates as $update) {
 340              if ($update->version == $version) {
 341                  $info = $update;
 342                  break;
 343              }
 344          }
 345          if (!$info) {
 346              return false;
 347          }
 348  
 349          $data = array(
 350              'updateaddon' => $component,
 351              'updateinfo'  => $info,
 352              'callerurl'   => optional_param('callerurl', null, PARAM_URL),
 353              'returnurl'   => optional_param('returnurl', null, PARAM_URL),
 354          );
 355          if ($data['callerurl']) {
 356              $data['callerurl'] = new moodle_url($data['callerurl']);
 357          }
 358          if ($data['callerurl']) {
 359              $data['returnurl'] = new moodle_url($data['returnurl']);
 360          }
 361  
 362          return $data;
 363      }
 364  
 365      /**
 366       * Handles magic getters and setters for protected properties.
 367       *
 368       * @param string $name method name, e.g. set_returnurl()
 369       * @param array $arguments arguments to be passed to the array
 370       */
 371      public function __call($name, array $arguments = array()) {
 372  
 373          if (substr($name, 0, 4) === 'set_') {
 374              $property = substr($name, 4);
 375              if (empty($property)) {
 376                  throw new coding_exception('Invalid property name (empty)');
 377              }
 378              if (empty($arguments)) {
 379                  $arguments = array(true); // Default value for flag-like properties.
 380              }
 381              // Make sure it is a protected property.
 382              $isprotected = false;
 383              $reflection = new \ReflectionObject($this);
 384              foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
 385                  if ($reflectionproperty->getName() === $property) {
 386                      $isprotected = true;
 387                      break;
 388                  }
 389              }
 390              if (!$isprotected) {
 391                  throw new coding_exception('Unable to set property - it does not exist or it is not protected');
 392              }
 393              $value = reset($arguments);
 394              $this->$property = $value;
 395              return;
 396          }
 397  
 398          if (substr($name, 0, 4) === 'get_') {
 399              $property = substr($name, 4);
 400              if (empty($property)) {
 401                  throw new coding_exception('Invalid property name (empty)');
 402              }
 403              if (!empty($arguments)) {
 404                  throw new coding_exception('No parameter expected');
 405              }
 406              // Make sure it is a protected property.
 407              $isprotected = false;
 408              $reflection = new \ReflectionObject($this);
 409              foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
 410                  if ($reflectionproperty->getName() === $property) {
 411                      $isprotected = true;
 412                      break;
 413                  }
 414              }
 415              if (!$isprotected) {
 416                  throw new coding_exception('Unable to get property - it does not exist or it is not protected');
 417              }
 418              return $this->$property;
 419          }
 420      }
 421  
 422      /**
 423       * Generates a random token and stores it in a file in moodledata directory.
 424       *
 425       * @return array of the (string)filename and (string)password in this order
 426       */
 427      public function prepare_authorization() {
 428          global $CFG;
 429  
 430          make_upload_directory('mdeploy/auth/');
 431  
 432          $attempts = 0;
 433          $success = false;
 434  
 435          while (!$success and $attempts < 5) {
 436              $attempts++;
 437  
 438              $passfile = $this->generate_passfile();
 439              $password = $this->generate_password();
 440              $now = time();
 441  
 442              $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
 443  
 444              if (!file_exists($filepath)) {
 445                  $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
 446                  chmod($filepath, $CFG->filepermissions);
 447              }
 448          }
 449  
 450          if ($success) {
 451              return array($passfile, $password);
 452  
 453          } else {
 454              throw new \moodle_exception('unable_prepare_authorization', 'core_plugin');
 455          }
 456      }
 457  
 458      /* === End of external API === */
 459  
 460      /**
 461       * Returns a random string to be used as a filename of the password storage.
 462       *
 463       * @return string
 464       */
 465      protected function generate_passfile() {
 466          return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
 467      }
 468  
 469      /**
 470       * Returns a random string to be used as the authorization token
 471       *
 472       * @return string
 473       */
 474      protected function generate_password() {
 475          return complex_random_string();
 476      }
 477  
 478      /**
 479       * Checks if the given component's directory is writable
 480       *
 481       * For the purpose of the deployment, the web server process has to have
 482       * write access to all files in the component's directory (recursively) and for the
 483       * directory itself.
 484       *
 485       * @see worker::move_directory_source_precheck()
 486       * @param string $component normalized component name
 487       * @return boolean
 488       */
 489      protected function component_writable($component) {
 490  
 491          list($plugintype, $pluginname) = core_component::normalize_component($component);
 492  
 493          $directory = core_component::get_plugin_directory($plugintype, $pluginname);
 494  
 495          if (is_null($directory)) {
 496              // Plugin unknown, most probably deleted or missing during upgrade,
 497              // look at the parent directory instead because they might want to install it.
 498              $plugintypes = core_component::get_plugin_types();
 499              if (!isset($plugintypes[$plugintype])) {
 500                  throw new coding_exception('Unknown component location', $component);
 501              }
 502              $directory = $plugintypes[$plugintype];
 503          }
 504  
 505          return $this->directory_writable($directory);
 506      }
 507  
 508      /**
 509       * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
 510       *
 511       * This is mainly supposed to check if the transmission over HTTPS would
 512       * work. That is, if the CA certificates are present at the server.
 513       *
 514       * @param string $downloadurl the URL of the ZIP package to download
 515       * @return bool
 516       */
 517      protected function update_downloadable($downloadurl) {
 518          global $CFG;
 519  
 520          $curloptions = array(
 521              'CURLOPT_SSL_VERIFYHOST' => 2,      // This is the default in {@link curl} class but just in case.
 522              'CURLOPT_SSL_VERIFYPEER' => true,
 523          );
 524  
 525          $curl = new \curl(array('proxy' => true));
 526          $result = $curl->head($downloadurl, $curloptions);
 527          $errno = $curl->get_errno();
 528          if (empty($errno)) {
 529              return true;
 530          } else {
 531              return false;
 532          }
 533      }
 534  
 535      /**
 536       * Checks if the directory and all its contents (recursively) is writable
 537       *
 538       * @param string $path full path to a directory
 539       * @return boolean
 540       */
 541      private function directory_writable($path) {
 542  
 543          if (!is_writable($path)) {
 544              return false;
 545          }
 546  
 547          if (is_dir($path)) {
 548              $handle = opendir($path);
 549          } else {
 550              return false;
 551          }
 552  
 553          $result = true;
 554  
 555          while ($filename = readdir($handle)) {
 556              $filepath = $path.'/'.$filename;
 557  
 558              if ($filename === '.' or $filename === '..') {
 559                  continue;
 560              }
 561  
 562              if (is_dir($filepath)) {
 563                  $result = $result && $this->directory_writable($filepath);
 564  
 565              } else {
 566                  $result = $result && is_writable($filepath);
 567              }
 568          }
 569  
 570          closedir($handle);
 571  
 572          return $result;
 573      }
 574  }


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