cms/drupal/includes/filetransfer/filetransfer.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /*
       
     4  * Base FileTransfer class.
       
     5  *
       
     6  * Classes extending this class perform file operations on directories not
       
     7  * writable by the webserver. To achieve this, the class should connect back
       
     8  * to the server using some backend (for example FTP or SSH). To keep security,
       
     9  * the password should always be asked from the user and never stored. For
       
    10  * safety, all methods operate only inside a "jail", by default the Drupal root.
       
    11  */
       
    12 abstract class FileTransfer {
       
    13   protected $username;
       
    14   protected $password;
       
    15   protected $hostname = 'localhost';
       
    16   protected $port;
       
    17 
       
    18   /**
       
    19    * The constructor for the UpdateConnection class. This method is also called
       
    20    * from the classes that extend this class and override this method.
       
    21    */
       
    22   function __construct($jail) {
       
    23     $this->jail = $jail;
       
    24   }
       
    25 
       
    26   /**
       
    27    * Classes that extend this class must override the factory() static method.
       
    28    *
       
    29    * @param string $jail
       
    30    *   The full path where all file operations performed by this object will
       
    31    *   be restricted to. This prevents the FileTransfer classes from being
       
    32    *   able to touch other parts of the filesystem.
       
    33    * @param array $settings
       
    34    *   An array of connection settings for the FileTransfer subclass. If the
       
    35    *   getSettingsForm() method uses any nested settings, the same structure
       
    36    *   will be assumed here.
       
    37    * @return object
       
    38    *   New instance of the appropriate FileTransfer subclass.
       
    39    */
       
    40   static function factory($jail, $settings) {
       
    41     throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
       
    42   }
       
    43 
       
    44   /**
       
    45    * Implementation of the magic __get() method.
       
    46    *
       
    47    * If the connection isn't set to anything, this will call the connect() method
       
    48    * and set it to and return the result; afterwards, the connection will be
       
    49    * returned directly without using this method.
       
    50    */
       
    51   function __get($name) {
       
    52     if ($name == 'connection') {
       
    53       $this->connect();
       
    54       return $this->connection;
       
    55     }
       
    56 
       
    57     if ($name == 'chroot') {
       
    58       $this->setChroot();
       
    59       return $this->chroot;
       
    60     }
       
    61   }
       
    62 
       
    63   /**
       
    64    * Connect to the server.
       
    65    */
       
    66   abstract protected function connect();
       
    67 
       
    68   /**
       
    69    * Copies a directory.
       
    70    *
       
    71    * @param $source
       
    72    *   The source path.
       
    73    * @param $destination
       
    74    *   The destination path.
       
    75    */
       
    76   public final function copyDirectory($source, $destination) {
       
    77     $source = $this->sanitizePath($source);
       
    78     $destination = $this->fixRemotePath($destination);
       
    79     $this->checkPath($destination);
       
    80     $this->copyDirectoryJailed($source, $destination);
       
    81   }
       
    82 
       
    83   /**
       
    84    * @see http://php.net/chmod
       
    85    *
       
    86    * @param string $path
       
    87    * @param long $mode
       
    88    * @param bool $recursive
       
    89    */
       
    90   public final function chmod($path, $mode, $recursive = FALSE) {
       
    91     if (!in_array('FileTransferChmodInterface', class_implements(get_class($this)))) {
       
    92       throw new FileTransferException('Unable to change file permissions');
       
    93     }
       
    94     $path = $this->sanitizePath($path);
       
    95     $path = $this->fixRemotePath($path);
       
    96     $this->checkPath($path);
       
    97     $this->chmodJailed($path, $mode, $recursive);
       
    98   }
       
    99 
       
   100   /**
       
   101    * Creates a directory.
       
   102    *
       
   103    * @param $directory
       
   104    *   The directory to be created.
       
   105    */
       
   106   public final function createDirectory($directory) {
       
   107     $directory = $this->fixRemotePath($directory);
       
   108     $this->checkPath($directory);
       
   109     $this->createDirectoryJailed($directory);
       
   110   }
       
   111 
       
   112   /**
       
   113    * Removes a directory.
       
   114    *
       
   115    * @param $directory
       
   116    *   The directory to be removed.
       
   117    */
       
   118   public final function removeDirectory($directory) {
       
   119     $directory = $this->fixRemotePath($directory);
       
   120     $this->checkPath($directory);
       
   121     $this->removeDirectoryJailed($directory);
       
   122   }
       
   123 
       
   124   /**
       
   125    * Copies a file.
       
   126    *
       
   127    * @param $source
       
   128    *   The source file.
       
   129    * @param $destination
       
   130    *   The destination file.
       
   131    */
       
   132   public final function copyFile($source, $destination) {
       
   133     $source = $this->sanitizePath($source);
       
   134     $destination = $this->fixRemotePath($destination);
       
   135     $this->checkPath($destination);
       
   136     $this->copyFileJailed($source, $destination);
       
   137   }
       
   138 
       
   139   /**
       
   140    * Removes a file.
       
   141    *
       
   142    * @param $destination
       
   143    *   The destination file to be removed.
       
   144    */
       
   145   public final function removeFile($destination) {
       
   146     $destination = $this->fixRemotePath($destination);
       
   147     $this->checkPath($destination);
       
   148     $this->removeFileJailed($destination);
       
   149   }
       
   150 
       
   151   /**
       
   152    * Checks that the path is inside the jail and throws an exception if not.
       
   153    *
       
   154    * @param $path
       
   155    *   A path to check against the jail.
       
   156    */
       
   157   protected final function checkPath($path) {
       
   158     $full_jail = $this->chroot . $this->jail;
       
   159     $full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
       
   160     $full_path = $this->fixRemotePath($full_path, FALSE);
       
   161     if ($full_jail !== $full_path) {
       
   162       throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
       
   163     }
       
   164   }
       
   165 
       
   166   /**
       
   167    * Returns a modified path suitable for passing to the server.
       
   168    * If a path is a windows path, makes it POSIX compliant by removing the drive letter.
       
   169    * If $this->chroot has a value, it is stripped from the path to allow for
       
   170    * chroot'd filetransfer systems.
       
   171    *
       
   172    * @param $path
       
   173    * @param $strip_chroot
       
   174    *
       
   175    * @return string
       
   176    */
       
   177   protected final function fixRemotePath($path, $strip_chroot = TRUE) {
       
   178     $path = $this->sanitizePath($path);
       
   179     $path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
       
   180     if ($strip_chroot) {
       
   181       if ($this->chroot && strpos($path, $this->chroot) === 0) {
       
   182         $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
       
   183       }
       
   184     }
       
   185     return $path;
       
   186   }
       
   187 
       
   188   /**
       
   189   * Changes backslashes to slashes, also removes a trailing slash.
       
   190   *
       
   191   * @param string $path
       
   192   * @return string
       
   193   */
       
   194   function sanitizePath($path) {
       
   195     $path = str_replace('\\', '/', $path); // Windows path sanitization.
       
   196     if (substr($path, -1) == '/') {
       
   197       $path = substr($path, 0, -1);
       
   198     }
       
   199     return $path;
       
   200   }
       
   201 
       
   202   /**
       
   203    * Copies a directory.
       
   204    *
       
   205    * We need a separate method to make the $destination is in the jail.
       
   206    *
       
   207    * @param $source
       
   208    *   The source path.
       
   209    * @param $destination
       
   210    *   The destination path.
       
   211    */
       
   212   protected function copyDirectoryJailed($source, $destination) {
       
   213     if ($this->isDirectory($destination)) {
       
   214       $destination = $destination . '/' . drupal_basename($source);
       
   215     }
       
   216     $this->createDirectory($destination);
       
   217     foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
       
   218       $relative_path = substr($filename, strlen($source));
       
   219       if ($file->isDir()) {
       
   220         $this->createDirectory($destination . $relative_path);
       
   221       }
       
   222       else {
       
   223         $this->copyFile($file->getPathName(), $destination . $relative_path);
       
   224       }
       
   225     }
       
   226   }
       
   227 
       
   228   /**
       
   229    * Creates a directory.
       
   230    *
       
   231    * @param $directory
       
   232    *   The directory to be created.
       
   233    */
       
   234   abstract protected function createDirectoryJailed($directory);
       
   235 
       
   236   /**
       
   237    * Removes a directory.
       
   238    *
       
   239    * @param $directory
       
   240    *   The directory to be removed.
       
   241    */
       
   242   abstract protected function removeDirectoryJailed($directory);
       
   243 
       
   244   /**
       
   245    * Copies a file.
       
   246    *
       
   247    * @param $source
       
   248    *   The source file.
       
   249    * @param $destination
       
   250    *   The destination file.
       
   251    */
       
   252   abstract protected function copyFileJailed($source, $destination);
       
   253 
       
   254   /**
       
   255    * Removes a file.
       
   256    *
       
   257    * @param $destination
       
   258    *   The destination file to be removed.
       
   259    */
       
   260   abstract protected function removeFileJailed($destination);
       
   261 
       
   262   /**
       
   263    * Checks if a particular path is a directory
       
   264    *
       
   265    * @param $path
       
   266    *   The path to check
       
   267    *
       
   268    * @return boolean
       
   269    */
       
   270   abstract public function isDirectory($path);
       
   271 
       
   272   /**
       
   273    * Checks if a particular path is a file (not a directory).
       
   274    *
       
   275    * @param $path
       
   276    *   The path to check
       
   277    *
       
   278    * @return boolean
       
   279    */
       
   280   abstract public function isFile($path);
       
   281 
       
   282   /**
       
   283    * Return the chroot property for this connection.
       
   284    *
       
   285    * It does this by moving up the tree until it finds itself. If successful,
       
   286    * it will return the chroot, otherwise FALSE.
       
   287    *
       
   288    * @return
       
   289    *   The chroot path for this connection or FALSE.
       
   290    */
       
   291   function findChroot() {
       
   292     // If the file exists as is, there is no chroot.
       
   293     $path = __FILE__;
       
   294     $path = $this->fixRemotePath($path, FALSE);
       
   295     if ($this->isFile($path)) {
       
   296       return FALSE;
       
   297     }
       
   298 
       
   299     $path = dirname(__FILE__);
       
   300     $path = $this->fixRemotePath($path, FALSE);
       
   301     $parts = explode('/', $path);
       
   302     $chroot = '';
       
   303     while (count($parts)) {
       
   304       $check = implode($parts, '/');
       
   305       if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
       
   306         // Remove the trailing slash.
       
   307         return substr($chroot, 0, -1);
       
   308       }
       
   309       $chroot .= array_shift($parts) . '/';
       
   310     }
       
   311     return FALSE;
       
   312   }
       
   313 
       
   314   /**
       
   315    * Sets the chroot and changes the jail to match the correct path scheme
       
   316    *
       
   317    */
       
   318   function setChroot() {
       
   319     $this->chroot = $this->findChroot();
       
   320     $this->jail = $this->fixRemotePath($this->jail);
       
   321   }
       
   322 
       
   323   /**
       
   324    * Returns a form to collect connection settings credentials.
       
   325    *
       
   326    * Implementing classes can either extend this form with fields collecting the
       
   327    * specific information they need, or override it entirely.
       
   328    */
       
   329   public function getSettingsForm() {
       
   330     $form['username'] = array(
       
   331       '#type' => 'textfield',
       
   332       '#title' => t('Username'),
       
   333     );
       
   334     $form['password'] = array(
       
   335       '#type' => 'password',
       
   336       '#title' => t('Password'),
       
   337       '#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
       
   338     );
       
   339     $form['advanced'] = array(
       
   340       '#type' => 'fieldset',
       
   341       '#title' => t('Advanced settings'),
       
   342       '#collapsible' => TRUE,
       
   343       '#collapsed' => TRUE,
       
   344     );
       
   345     $form['advanced']['hostname'] = array(
       
   346       '#type' => 'textfield',
       
   347       '#title' => t('Host'),
       
   348       '#default_value' => 'localhost',
       
   349       '#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
       
   350     );
       
   351     $form['advanced']['port'] = array(
       
   352       '#type' => 'textfield',
       
   353       '#title' => t('Port'),
       
   354       '#default_value' => NULL,
       
   355     );
       
   356     return $form;
       
   357   }
       
   358 }
       
   359 
       
   360 /**
       
   361  * FileTransferException class.
       
   362  */
       
   363 class FileTransferException extends Exception {
       
   364   public $arguments;
       
   365 
       
   366   function __construct($message, $code = 0, $arguments = array()) {
       
   367     parent::__construct($message, $code);
       
   368     $this->arguments = $arguments;
       
   369   }
       
   370 }
       
   371 
       
   372 
       
   373 /**
       
   374  * A FileTransfer Class implementing this interface can be used to chmod files.
       
   375  */
       
   376 interface FileTransferChmodInterface {
       
   377 
       
   378   /**
       
   379    * Changes the permissions of the file / directory specified in $path
       
   380    *
       
   381    * @param string $path
       
   382    *   Path to change permissions of.
       
   383    * @param long $mode
       
   384    *   The new file permission mode to be passed to chmod().
       
   385    * @param boolean $recursive
       
   386    *   Pass TRUE to recursively chmod the entire directory specified in $path.
       
   387    */
       
   388   function chmodJailed($path, $mode, $recursive);
       
   389 }
       
   390 
       
   391 /**
       
   392  * Provides an interface for iterating recursively over filesystem directories.
       
   393  *
       
   394  * Manually skips '.' and '..' directories, since no existing method is
       
   395  * available in PHP 5.2.
       
   396  *
       
   397  * @todo Depreciate in favor of RecursiveDirectoryIterator::SKIP_DOTS once PHP
       
   398  *   5.3 or later is required.
       
   399  */
       
   400 class SkipDotsRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
       
   401   /**
       
   402    * Constructs a SkipDotsRecursiveDirectoryIterator
       
   403    *
       
   404    * @param $path
       
   405    *   The path of the directory to be iterated over.
       
   406    */
       
   407   function __construct($path) {
       
   408     parent::__construct($path);
       
   409     $this->skipdots();
       
   410   }
       
   411 
       
   412   function rewind() {
       
   413     parent::rewind();
       
   414     $this->skipdots();
       
   415   }
       
   416 
       
   417   function next() {
       
   418     parent::next();
       
   419     $this->skipdots();
       
   420   }
       
   421 
       
   422   protected function skipdots() {
       
   423     while ($this->isDot()) {
       
   424       parent::next();
       
   425     }
       
   426   }
       
   427 }