cms/drupal/includes/updater.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * @file
       
     5  * Classes used for updating various files in the Drupal webroot. These
       
     6  * classes use a FileTransfer object to actually perform the operations.
       
     7  * Normally, the FileTransfer is provided when the site owner is redirected to
       
     8  * authorize.php as part of a multistep process.
       
     9  */
       
    10 
       
    11 /**
       
    12  * Interface for a class which can update a Drupal project.
       
    13  *
       
    14  * An Updater currently serves the following purposes:
       
    15  *   - It can take a given directory, and determine if it can operate on it.
       
    16  *   - It can move the contents of that directory into the appropriate place
       
    17  *     on the system using FileTransfer classes.
       
    18  *   - It can return a list of "next steps" after an update or install.
       
    19  *   - In the future, it will most likely perform some of those steps as well.
       
    20  */
       
    21 interface DrupalUpdaterInterface {
       
    22 
       
    23   /**
       
    24    * Checks if the project is installed.
       
    25    *
       
    26    * @return bool
       
    27    */
       
    28   public function isInstalled();
       
    29 
       
    30   /**
       
    31    * Returns the system name of the project.
       
    32    *
       
    33    * @param string $directory
       
    34    *  A directory containing a project.
       
    35    */
       
    36   public static function getProjectName($directory);
       
    37 
       
    38   /**
       
    39    * @return string
       
    40    *   An absolute path to the default install location.
       
    41    */
       
    42   public function getInstallDirectory();
       
    43 
       
    44   /**
       
    45    * Determine if the Updater can handle the project provided in $directory.
       
    46    *
       
    47    * @todo: Provide something more rational here, like a project spec file.
       
    48    *
       
    49    * @param string $directory
       
    50    *
       
    51    * @return bool
       
    52    *   TRUE if the project is installed, FALSE if not.
       
    53    */
       
    54   public static function canUpdateDirectory($directory);
       
    55 
       
    56   /**
       
    57    * Actions to run after an install has occurred.
       
    58    */
       
    59   public function postInstall();
       
    60 
       
    61   /**
       
    62    * Actions to run after an update has occurred.
       
    63    */
       
    64   public function postUpdate();
       
    65 }
       
    66 
       
    67 /**
       
    68  * Base class for Updaters used in Drupal.
       
    69  */
       
    70 class Updater {
       
    71 
       
    72   /**
       
    73    * @var string $source Directory to install from.
       
    74    */
       
    75   public $source;
       
    76 
       
    77   public function __construct($source) {
       
    78     $this->source = $source;
       
    79     $this->name = self::getProjectName($source);
       
    80     $this->title = self::getProjectTitle($source);
       
    81   }
       
    82 
       
    83   /**
       
    84    * Return an Updater of the appropriate type depending on the source.
       
    85    *
       
    86    * If a directory is provided which contains a module, will return a
       
    87    * ModuleUpdater.
       
    88    *
       
    89    * @param string $source
       
    90    *   Directory of a Drupal project.
       
    91    *
       
    92    * @return Updater
       
    93    */
       
    94   public static function factory($source) {
       
    95     if (is_dir($source)) {
       
    96       $updater = self::getUpdaterFromDirectory($source);
       
    97     }
       
    98     else {
       
    99       throw new UpdaterException(t('Unable to determine the type of the source directory.'));
       
   100     }
       
   101     return new $updater($source);
       
   102   }
       
   103 
       
   104   /**
       
   105    * Determine which Updater class can operate on the given directory.
       
   106    *
       
   107    * @param string $directory
       
   108    *   Extracted Drupal project.
       
   109    *
       
   110    * @return string
       
   111    *   The class name which can work with this project type.
       
   112    */
       
   113   public static function getUpdaterFromDirectory($directory) {
       
   114     // Gets a list of possible implementing classes.
       
   115     $updaters = drupal_get_updaters();
       
   116     foreach ($updaters as $updater) {
       
   117       $class = $updater['class'];
       
   118       if (call_user_func(array($class, 'canUpdateDirectory'), $directory)) {
       
   119         return $class;
       
   120       }
       
   121     }
       
   122     throw new UpdaterException(t('Cannot determine the type of project.'));
       
   123   }
       
   124 
       
   125   /**
       
   126    * Figure out what the most important (or only) info file is in a directory.
       
   127    *
       
   128    * Since there is no enforcement of which info file is the project's "main"
       
   129    * info file, this will get one with the same name as the directory, or the
       
   130    * first one it finds.  Not ideal, but needs a larger solution.
       
   131    *
       
   132    * @param string $directory
       
   133    *   Directory to search in.
       
   134    *
       
   135    * @return string
       
   136    *   Path to the info file.
       
   137    */
       
   138   public static function findInfoFile($directory) {
       
   139     $info_files = file_scan_directory($directory, '/.*\.info$/');
       
   140     if (!$info_files) {
       
   141       return FALSE;
       
   142     }
       
   143     foreach ($info_files as $info_file) {
       
   144       if (drupal_substr($info_file->filename, 0, -5) == drupal_basename($directory)) {
       
   145         // Info file Has the same name as the directory, return it.
       
   146         return $info_file->uri;
       
   147       }
       
   148     }
       
   149     // Otherwise, return the first one.
       
   150     $info_file = array_shift($info_files);
       
   151     return $info_file->uri;
       
   152   }
       
   153 
       
   154   /**
       
   155    * Get the name of the project directory (basename).
       
   156    *
       
   157    * @todo: It would be nice, if projects contained an info file which could
       
   158    *        provide their canonical name.
       
   159    *
       
   160    * @param string $directory
       
   161    *
       
   162    * @return string
       
   163    *   The name of the project.
       
   164    */
       
   165   public static function getProjectName($directory) {
       
   166     return drupal_basename($directory);
       
   167   }
       
   168 
       
   169   /**
       
   170    * Return the project name from a Drupal info file.
       
   171    *
       
   172    * @param string $directory
       
   173    *   Directory to search for the info file.
       
   174    *
       
   175    * @return string
       
   176    *   The title of the project.
       
   177    */
       
   178   public static function getProjectTitle($directory) {
       
   179     $info_file = self::findInfoFile($directory);
       
   180     $info = drupal_parse_info_file($info_file);
       
   181     if (empty($info)) {
       
   182       throw new UpdaterException(t('Unable to parse info file: %info_file.', array('%info_file' => $info_file)));
       
   183     }
       
   184     if (empty($info['name'])) {
       
   185       throw new UpdaterException(t("The info file (%info_file) does not define a 'name' attribute.", array('%info_file' => $info_file)));
       
   186     }
       
   187     return $info['name'];
       
   188   }
       
   189 
       
   190   /**
       
   191    * Store the default parameters for the Updater.
       
   192    *
       
   193    * @param array $overrides
       
   194    *   An array of overrides for the default parameters.
       
   195    *
       
   196    * @return array
       
   197    *   An array of configuration parameters for an update or install operation.
       
   198    */
       
   199   protected function getInstallArgs($overrides = array()) {
       
   200     $args = array(
       
   201       'make_backup' => FALSE,
       
   202       'install_dir' => $this->getInstallDirectory(),
       
   203       'backup_dir'  => $this->getBackupDir(),
       
   204     );
       
   205     return array_merge($args, $overrides);
       
   206   }
       
   207 
       
   208   /**
       
   209    * Updates a Drupal project, returns a list of next actions.
       
   210    *
       
   211    * @param FileTransfer $filetransfer
       
   212    *   Object that is a child of FileTransfer. Used for moving files
       
   213    *   to the server.
       
   214    * @param array $overrides
       
   215    *   An array of settings to override defaults; see self::getInstallArgs().
       
   216    *
       
   217    * @return array
       
   218    *   An array of links which the user may need to complete the update
       
   219    */
       
   220   public function update(&$filetransfer, $overrides = array()) {
       
   221     try {
       
   222       // Establish arguments with possible overrides.
       
   223       $args = $this->getInstallArgs($overrides);
       
   224 
       
   225       // Take a Backup.
       
   226       if ($args['make_backup']) {
       
   227         $this->makeBackup($args['install_dir'], $args['backup_dir']);
       
   228       }
       
   229 
       
   230       if (!$this->name) {
       
   231         // This is bad, don't want to delete the install directory.
       
   232         throw new UpdaterException(t('Fatal error in update, cowardly refusing to wipe out the install directory.'));
       
   233       }
       
   234 
       
   235       // Make sure the installation parent directory exists and is writable.
       
   236       $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
       
   237 
       
   238       // Note: If the project is installed in sites/all, it will not be
       
   239       // deleted. It will be installed in sites/default as that will override
       
   240       // the sites/all reference and not break other sites which are using it.
       
   241       if (is_dir($args['install_dir'] . '/' . $this->name)) {
       
   242         // Remove the existing installed file.
       
   243         $filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name);
       
   244       }
       
   245 
       
   246       // Copy the directory in place.
       
   247       $filetransfer->copyDirectory($this->source, $args['install_dir']);
       
   248 
       
   249       // Make sure what we just installed is readable by the web server.
       
   250       $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
       
   251 
       
   252       // Run the updates.
       
   253       // @TODO: decide if we want to implement this.
       
   254       $this->postUpdate();
       
   255 
       
   256       // For now, just return a list of links of things to do.
       
   257       return $this->postUpdateTasks();
       
   258     }
       
   259     catch (FileTransferException $e) {
       
   260       throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
       
   261     }
       
   262   }
       
   263 
       
   264   /**
       
   265    * Installs a Drupal project, returns a list of next actions.
       
   266    *
       
   267    * @param FileTransfer $filetransfer
       
   268    *   Object that is a child of FileTransfer.
       
   269    * @param array $overrides
       
   270    *   An array of settings to override defaults; see self::getInstallArgs().
       
   271    *
       
   272    * @return array
       
   273    *   An array of links which the user may need to complete the install.
       
   274    */
       
   275   public function install(&$filetransfer, $overrides = array()) {
       
   276     try {
       
   277       // Establish arguments with possible overrides.
       
   278       $args = $this->getInstallArgs($overrides);
       
   279 
       
   280       // Make sure the installation parent directory exists and is writable.
       
   281       $this->prepareInstallDirectory($filetransfer, $args['install_dir']);
       
   282 
       
   283       // Copy the directory in place.
       
   284       $filetransfer->copyDirectory($this->source, $args['install_dir']);
       
   285 
       
   286       // Make sure what we just installed is readable by the web server.
       
   287       $this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
       
   288 
       
   289       // Potentially enable something?
       
   290       // @TODO: decide if we want to implement this.
       
   291       $this->postInstall();
       
   292       // For now, just return a list of links of things to do.
       
   293       return $this->postInstallTasks();
       
   294     }
       
   295     catch (FileTransferException $e) {
       
   296       throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
       
   297     }
       
   298   }
       
   299 
       
   300   /**
       
   301    * Make sure the installation parent directory exists and is writable.
       
   302    *
       
   303    * @param FileTransfer $filetransfer
       
   304    *   Object which is a child of FileTransfer.
       
   305    * @param string $directory
       
   306    *   The installation directory to prepare.
       
   307    */
       
   308   public function prepareInstallDirectory(&$filetransfer, $directory) {
       
   309     // Make the parent dir writable if need be and create the dir.
       
   310     if (!is_dir($directory)) {
       
   311       $parent_dir = dirname($directory);
       
   312       if (!is_writable($parent_dir)) {
       
   313         @chmod($parent_dir, 0755);
       
   314         // It is expected that this will fail if the directory is owned by the
       
   315         // FTP user. If the FTP user == web server, it will succeed.
       
   316         try {
       
   317           $filetransfer->createDirectory($directory);
       
   318           $this->makeWorldReadable($filetransfer, $directory);
       
   319         }
       
   320         catch (FileTransferException $e) {
       
   321           // Probably still not writable. Try to chmod and do it again.
       
   322           // @todo: Make a new exception class so we can catch it differently.
       
   323           try {
       
   324             $old_perms = substr(sprintf('%o', fileperms($parent_dir)), -4);
       
   325             $filetransfer->chmod($parent_dir, 0755);
       
   326             $filetransfer->createDirectory($directory);
       
   327             $this->makeWorldReadable($filetransfer, $directory);
       
   328             // Put the permissions back.
       
   329             $filetransfer->chmod($parent_dir, intval($old_perms, 8));
       
   330           }
       
   331           catch (FileTransferException $e) {
       
   332             $message = t($e->getMessage(), $e->arguments);
       
   333             $throw_message = t('Unable to create %directory due to the following: %reason', array('%directory' => $directory, '%reason' => $message));
       
   334             throw new UpdaterException($throw_message);
       
   335           }
       
   336         }
       
   337         // Put the parent directory back.
       
   338         @chmod($parent_dir, 0555);
       
   339       }
       
   340     }
       
   341   }
       
   342 
       
   343   /**
       
   344    * Ensure that a given directory is world readable.
       
   345    *
       
   346    * @param FileTransfer $filetransfer
       
   347    *   Object which is a child of FileTransfer.
       
   348    * @param string $path
       
   349    *   The file path to make world readable.
       
   350    * @param bool $recursive
       
   351    *   If the chmod should be applied recursively.
       
   352    */
       
   353   public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) {
       
   354     if (!is_executable($path)) {
       
   355       // Set it to read + execute.
       
   356       $new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5";
       
   357       $filetransfer->chmod($path, intval($new_perms, 8), $recursive);
       
   358     }
       
   359   }
       
   360 
       
   361   /**
       
   362    * Perform a backup.
       
   363    *
       
   364    * @todo Not implemented.
       
   365    */
       
   366   public function makeBackup(&$filetransfer, $from, $to) {
       
   367   }
       
   368 
       
   369   /**
       
   370    * Return the full path to a directory where backups should be written.
       
   371    */
       
   372   public function getBackupDir() {
       
   373     return file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath();
       
   374   }
       
   375 
       
   376   /**
       
   377    * Perform actions after new code is updated.
       
   378    */
       
   379   public function postUpdate() {
       
   380   }
       
   381 
       
   382   /**
       
   383    * Perform actions after installation.
       
   384    */
       
   385   public function postInstall() {
       
   386   }
       
   387 
       
   388   /**
       
   389    * Return an array of links to pages that should be visited post operation.
       
   390    *
       
   391    * @return array
       
   392    *   Links which provide actions to take after the install is finished.
       
   393    */
       
   394   public function postInstallTasks() {
       
   395     return array();
       
   396   }
       
   397 
       
   398   /**
       
   399    * Return an array of links to pages that should be visited post operation.
       
   400    *
       
   401    * @return array
       
   402    *   Links which provide actions to take after the update is finished.
       
   403    */
       
   404   public function postUpdateTasks() {
       
   405     return array();
       
   406   }
       
   407 }
       
   408 
       
   409 /**
       
   410  * Exception class for the Updater class hierarchy.
       
   411  *
       
   412  * This is identical to the base Exception class, we just give it a more
       
   413  * specific name so that call sites that want to tell the difference can
       
   414  * specifically catch these exceptions and treat them differently.
       
   415  */
       
   416 class UpdaterException extends Exception {
       
   417 }
       
   418 
       
   419 /**
       
   420  * Child class of UpdaterException that indicates a FileTransfer exception.
       
   421  *
       
   422  * We have to catch FileTransfer exceptions and wrap those in t(), since
       
   423  * FileTransfer is so low-level that it doesn't use any Drupal APIs and none
       
   424  * of the strings are translated.
       
   425  */
       
   426 class UpdaterFileTransferException extends UpdaterException {
       
   427 }