| Drupal | PHP Cross Reference | Content Management Systems |
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title