annotate core/modules/update/update.manager.inc @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * Administrative screens and processing functions of the Update Manager module.
Chris@0 6 *
Chris@0 7 * This allows site administrators with the 'administer software updates'
Chris@0 8 * permission to either upgrade existing projects, or download and install new
Chris@0 9 * ones, so long as the killswitch setting ('allow_authorize_operations') is
Chris@0 10 * not FALSE.
Chris@0 11 *
Chris@0 12 * To install new code, the administrator is prompted for either the URL of an
Chris@0 13 * archive file, or to directly upload the archive file. The archive is loaded
Chris@0 14 * into a temporary location, extracted, and verified. If everything is
Chris@0 15 * successful, the user is redirected to authorize.php to type in file transfer
Chris@0 16 * credentials and authorize the installation to proceed with elevated
Chris@0 17 * privileges, such that the extracted files can be copied out of the temporary
Chris@0 18 * location and into the live web root.
Chris@0 19 *
Chris@0 20 * Updating existing code is a more elaborate process. The first step is a
Chris@0 21 * selection form where the user is presented with a table of installed projects
Chris@0 22 * that are missing newer releases. The user selects which projects they wish to
Chris@0 23 * update, and presses the "Download updates" button to continue. This sets up a
Chris@0 24 * batch to fetch all the selected releases, and redirects to
Chris@0 25 * admin/update/download to display the batch progress bar as it runs. Each
Chris@0 26 * batch operation is responsible for downloading a single file, extracting the
Chris@0 27 * archive, and verifying the contents. If there are any errors, the user is
Chris@0 28 * redirected back to the first page with the error messages. If all downloads
Chris@0 29 * were extracted and verified, the user is instead redirected to
Chris@0 30 * admin/update/ready, a landing page which reminds them to backup their
Chris@0 31 * database and asks if they want to put the site offline during the update.
Chris@0 32 * Once the user presses the "Install updates" button, they are redirected to
Chris@0 33 * authorize.php to supply their web root file access credentials. The
Chris@0 34 * authorized operation (which lives in update.authorize.inc) sets up a batch to
Chris@0 35 * copy each extracted update from the temporary location into the live web
Chris@0 36 * root.
Chris@0 37 */
Chris@0 38
Chris@18 39 use Drupal\Core\Url;
Chris@18 40 use Drupal\Core\File\Exception\FileException;
Chris@0 41 use Symfony\Component\HttpFoundation\RedirectResponse;
Chris@0 42
Chris@0 43 /**
Chris@0 44 * Batch callback: Performs actions when the download batch is completed.
Chris@0 45 *
Chris@0 46 * @param $success
Chris@0 47 * TRUE if the batch operation was successful, FALSE if there were errors.
Chris@0 48 * @param $results
Chris@0 49 * An associative array of results from the batch operation.
Chris@0 50 */
Chris@0 51 function update_manager_download_batch_finished($success, $results) {
Chris@0 52 if (!empty($results['errors'])) {
Chris@0 53 $item_list = [
Chris@0 54 '#theme' => 'item_list',
Chris@0 55 '#title' => t('Downloading updates failed:'),
Chris@0 56 '#items' => $results['errors'],
Chris@0 57 ];
Chris@17 58 \Drupal::messenger()->addError(\Drupal::service('renderer')->render($item_list));
Chris@0 59 }
Chris@0 60 elseif ($success) {
Chris@17 61 \Drupal::messenger()->addStatus(t('Updates downloaded successfully.'));
Chris@0 62 $_SESSION['update_manager_update_projects'] = $results['projects'];
Chris@18 63 return new RedirectResponse(Url::fromRoute('update.confirmation_page', [], ['absolute' => TRUE])->toString());
Chris@0 64 }
Chris@0 65 else {
Chris@0 66 // Ideally we're catching all Exceptions, so they should never see this,
Chris@0 67 // but just in case, we have to tell them something.
Chris@17 68 \Drupal::messenger()->addError(t('Fatal error trying to download.'));
Chris@0 69 }
Chris@0 70 }
Chris@0 71
Chris@0 72 /**
Chris@0 73 * Checks for file transfer backends and prepares a form fragment about them.
Chris@0 74 *
Chris@0 75 * @param array $form
Chris@0 76 * Reference to the form array we're building.
Chris@0 77 * @param string $operation
Chris@0 78 * The update manager operation we're in the middle of. Can be either 'update'
Chris@0 79 * or 'install'. Use to provide operation-specific interface text.
Chris@0 80 *
Chris@0 81 * @return
Chris@0 82 * TRUE if the update manager should continue to the next step in the
Chris@0 83 * workflow, or FALSE if we've hit a fatal configuration and must halt the
Chris@0 84 * workflow.
Chris@0 85 */
Chris@0 86 function _update_manager_check_backends(&$form, $operation) {
Chris@0 87 // If file transfers will be performed locally, we do not need to display any
Chris@0 88 // warnings or notices to the user and should automatically continue the
Chris@0 89 // workflow, since we won't be using a FileTransfer backend that requires
Chris@0 90 // user input or a specific server configuration.
Chris@0 91 if (update_manager_local_transfers_allowed()) {
Chris@0 92 return TRUE;
Chris@0 93 }
Chris@0 94
Chris@0 95 // Otherwise, show the available backends.
Chris@0 96 $form['available_backends'] = [
Chris@0 97 '#prefix' => '<p>',
Chris@0 98 '#suffix' => '</p>',
Chris@0 99 ];
Chris@0 100
Chris@0 101 $available_backends = drupal_get_filetransfer_info();
Chris@0 102 if (empty($available_backends)) {
Chris@0 103 if ($operation == 'update') {
Chris@0 104 $form['available_backends']['#markup'] = t('Your server does not support updating modules and themes from this interface. Instead, update modules and themes by uploading the new versions directly to the server, as documented in <a href=":doc_url">Extending Drupal 8</a>.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']);
Chris@0 105 }
Chris@0 106 else {
Chris@0 107 $form['available_backends']['#markup'] = t('Your server does not support installing modules and themes from this interface. Instead, install modules and themes by uploading them directly to the server, as documented in <a href=":doc_url">Extending Drupal 8</a>.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']);
Chris@0 108 }
Chris@0 109 return FALSE;
Chris@0 110 }
Chris@0 111
Chris@0 112 $backend_names = [];
Chris@0 113 foreach ($available_backends as $backend) {
Chris@0 114 $backend_names[] = $backend['title'];
Chris@0 115 }
Chris@0 116 if ($operation == 'update') {
Chris@0 117 $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural(
Chris@0 118 count($available_backends),
Chris@0 119 'Updating modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal 8</a> for other update methods.',
Chris@0 120 'Updating modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See <a href=":doc_url">Extending Drupal 8</a> for other update methods.',
Chris@0 121 [
Chris@0 122 '@backends' => implode(', ', $backend_names),
Chris@0 123 ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview',
Chris@0 124 ]);
Chris@0 125 }
Chris@0 126 else {
Chris@0 127 $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural(
Chris@0 128 count($available_backends),
Chris@0 129 'Installing modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal 8</a> for other installation methods.',
Chris@0 130 'Installing modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See <a href=":doc_url">Extending Drupal 8</a> for other installation methods.',
Chris@0 131 [
Chris@0 132 '@backends' => implode(', ', $backend_names),
Chris@0 133 ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview',
Chris@0 134 ]);
Chris@0 135 }
Chris@0 136 return TRUE;
Chris@0 137 }
Chris@0 138
Chris@0 139 /**
Chris@0 140 * Unpacks a downloaded archive file.
Chris@0 141 *
Chris@0 142 * @param string $file
Chris@0 143 * The filename of the archive you wish to extract.
Chris@0 144 * @param string $directory
Chris@0 145 * The directory you wish to extract the archive into.
Chris@0 146 *
Chris@0 147 * @return Archiver
Chris@0 148 * The Archiver object used to extract the archive.
Chris@0 149 *
Chris@0 150 * @throws Exception
Chris@0 151 */
Chris@0 152 function update_manager_archive_extract($file, $directory) {
Chris@0 153 $archiver = archiver_get_archiver($file);
Chris@0 154 if (!$archiver) {
Chris@0 155 throw new Exception(t('Cannot extract %file, not a valid archive.', ['%file' => $file]));
Chris@0 156 }
Chris@0 157
Chris@0 158 // Remove the directory if it exists, otherwise it might contain a mixture of
Chris@0 159 // old files mixed with the new files (e.g. in cases where files were removed
Chris@0 160 // from a later release).
Chris@0 161 $files = $archiver->listContents();
Chris@0 162
Chris@0 163 // Unfortunately, we can only use the directory name to determine the project
Chris@0 164 // name. Some archivers list the first file as the directory (i.e., MODULE/)
Chris@0 165 // and others list an actual file (i.e., MODULE/README.TXT).
Chris@0 166 $project = strtok($files[0], '/\\');
Chris@0 167
Chris@0 168 $extract_location = $directory . '/' . $project;
Chris@0 169 if (file_exists($extract_location)) {
Chris@18 170 try {
Chris@18 171 \Drupal::service('file_system')->deleteRecursive($extract_location);
Chris@18 172 }
Chris@18 173 catch (FileException $e) {
Chris@18 174 // Ignore failed deletes.
Chris@18 175 }
Chris@0 176 }
Chris@0 177
Chris@0 178 $archiver->extract($directory);
Chris@0 179 return $archiver;
Chris@0 180 }
Chris@0 181
Chris@0 182 /**
Chris@0 183 * Verifies an archive after it has been downloaded and extracted.
Chris@0 184 *
Chris@0 185 * This function is responsible for invoking hook_verify_update_archive().
Chris@0 186 *
Chris@0 187 * @param string $project
Chris@0 188 * The short name of the project to download.
Chris@0 189 * @param string $archive_file
Chris@0 190 * The filename of the unextracted archive.
Chris@0 191 * @param string $directory
Chris@0 192 * The directory that the archive was extracted into.
Chris@0 193 *
Chris@0 194 * @return array
Chris@0 195 * An array of error messages to display if the archive was invalid. If there
Chris@0 196 * are no errors, it will be an empty array.
Chris@0 197 */
Chris@0 198 function update_manager_archive_verify($project, $archive_file, $directory) {
Chris@0 199 return \Drupal::moduleHandler()->invokeAll('verify_update_archive', [$project, $archive_file, $directory]);
Chris@0 200 }
Chris@0 201
Chris@0 202 /**
Chris@0 203 * Copies a file from the specified URL to the temporary directory for updates.
Chris@0 204 *
Chris@0 205 * Returns the local path if the file has already been downloaded.
Chris@0 206 *
Chris@0 207 * @param $url
Chris@0 208 * The URL of the file on the server.
Chris@0 209 *
Chris@0 210 * @return string
Chris@0 211 * Path to local file.
Chris@0 212 */
Chris@0 213 function update_manager_file_get($url) {
Chris@0 214 $parsed_url = parse_url($url);
Chris@0 215 $remote_schemes = ['http', 'https', 'ftp', 'ftps', 'smb', 'nfs'];
Chris@0 216 if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], $remote_schemes)) {
Chris@0 217 // This is a local file, just return the path.
Chris@14 218 return \Drupal::service('file_system')->realpath($url);
Chris@0 219 }
Chris@0 220
Chris@0 221 // Check the cache and download the file if needed.
Chris@0 222 $cache_directory = _update_manager_cache_directory();
Chris@18 223 $local = $cache_directory . '/' . \Drupal::service('file_system')->basename($parsed_url['path']);
Chris@0 224
Chris@0 225 if (!file_exists($local) || update_delete_file_if_stale($local)) {
Chris@0 226 return system_retrieve_file($url, $local, FALSE, FILE_EXISTS_REPLACE);
Chris@0 227 }
Chris@0 228 else {
Chris@0 229 return $local;
Chris@0 230 }
Chris@0 231 }
Chris@0 232
Chris@0 233 /**
Chris@0 234 * Implements callback_batch_operation().
Chris@0 235 *
Chris@0 236 * Downloads, unpacks, and verifies a project.
Chris@0 237 *
Chris@0 238 * This function assumes that the provided URL points to a file archive of some
Chris@0 239 * sort. The URL can have any scheme that we have a file stream wrapper to
Chris@0 240 * support. The file is downloaded to a local cache.
Chris@0 241 *
Chris@0 242 * @param string $project
Chris@0 243 * The short name of the project to download.
Chris@0 244 * @param string $url
Chris@0 245 * The URL to download a specific project release archive file.
Chris@0 246 * @param array $context
Chris@0 247 * Reference to an array used for Batch API storage.
Chris@0 248 *
Chris@0 249 * @see update_manager_download_page()
Chris@0 250 */
Chris@0 251 function update_manager_batch_project_get($project, $url, &$context) {
Chris@0 252 // This is here to show the user that we are in the process of downloading.
Chris@0 253 if (!isset($context['sandbox']['started'])) {
Chris@0 254 $context['sandbox']['started'] = TRUE;
Chris@0 255 $context['message'] = t('Downloading %project', ['%project' => $project]);
Chris@0 256 $context['finished'] = 0;
Chris@0 257 return;
Chris@0 258 }
Chris@0 259
Chris@0 260 // Actually try to download the file.
Chris@0 261 if (!($local_cache = update_manager_file_get($url))) {
Chris@0 262 $context['results']['errors'][$project] = t('Failed to download %project from %url', ['%project' => $project, '%url' => $url]);
Chris@0 263 return;
Chris@0 264 }
Chris@0 265
Chris@0 266 // Extract it.
Chris@0 267 $extract_directory = _update_manager_extract_directory();
Chris@0 268 try {
Chris@0 269 update_manager_archive_extract($local_cache, $extract_directory);
Chris@0 270 }
Chris@0 271 catch (Exception $e) {
Chris@0 272 $context['results']['errors'][$project] = $e->getMessage();
Chris@0 273 return;
Chris@0 274 }
Chris@0 275
Chris@0 276 // Verify it.
Chris@0 277 $archive_errors = update_manager_archive_verify($project, $local_cache, $extract_directory);
Chris@0 278 if (!empty($archive_errors)) {
Chris@0 279 // We just need to make sure our array keys don't collide, so use the
Chris@0 280 // numeric keys from the $archive_errors array.
Chris@0 281 foreach ($archive_errors as $key => $error) {
Chris@0 282 $context['results']['errors']["$project-$key"] = $error;
Chris@0 283 }
Chris@0 284 return;
Chris@0 285 }
Chris@0 286
Chris@0 287 // Yay, success.
Chris@0 288 $context['results']['projects'][$project] = $url;
Chris@0 289 $context['finished'] = 1;
Chris@0 290 }
Chris@0 291
Chris@0 292 /**
Chris@0 293 * Determines if file transfers will be performed locally.
Chris@0 294 *
Chris@0 295 * If the server is configured such that webserver-created files have the same
Chris@0 296 * owner as the configuration directory (e.g., sites/default) where new code
Chris@0 297 * will eventually be installed, the update manager can transfer files entirely
Chris@0 298 * locally, without changing their ownership (in other words, without prompting
Chris@0 299 * the user for FTP, SSH or other credentials).
Chris@0 300 *
Chris@0 301 * This server configuration is an inherent security weakness because it allows
Chris@0 302 * a malicious webserver process to append arbitrary PHP code and then execute
Chris@0 303 * it. However, it is supported here because it is a common configuration on
Chris@0 304 * shared hosting, and there is nothing Drupal can do to prevent it.
Chris@0 305 *
Chris@0 306 * @return
Chris@0 307 * TRUE if local file transfers are allowed on this server, or FALSE if not.
Chris@0 308 *
Chris@0 309 * @see install_check_requirements()
Chris@0 310 */
Chris@0 311 function update_manager_local_transfers_allowed() {
Chris@0 312 // Compare the owner of a webserver-created temporary file to the owner of
Chris@0 313 // the configuration directory to determine if local transfers will be
Chris@0 314 // allowed.
Chris@18 315 $temporary_file = \Drupal::service('file_system')->tempnam('temporary://', 'update_');
Chris@0 316 $site_path = \Drupal::service('site.path');
Chris@0 317 $local_transfers_allowed = fileowner($temporary_file) === fileowner($site_path);
Chris@0 318
Chris@0 319 // Clean up. If this fails, we can ignore it (since this is just a temporary
Chris@0 320 // file anyway).
Chris@0 321 @drupal_unlink($temporary_file);
Chris@0 322
Chris@0 323 return $local_transfers_allowed;
Chris@0 324 }