annotate core/lib/Drupal/Core/Updater/Updater.php @ 9:1fc0ff908d1f

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