Chris@0: 'item_list', Chris@0: '#title' => t('Downloading updates failed:'), Chris@0: '#items' => $results['errors'], Chris@0: ]; Chris@17: \Drupal::messenger()->addError(\Drupal::service('renderer')->render($item_list)); Chris@0: } Chris@0: elseif ($success) { Chris@17: \Drupal::messenger()->addStatus(t('Updates downloaded successfully.')); Chris@0: $_SESSION['update_manager_update_projects'] = $results['projects']; Chris@18: return new RedirectResponse(Url::fromRoute('update.confirmation_page', [], ['absolute' => TRUE])->toString()); Chris@0: } Chris@0: else { Chris@0: // Ideally we're catching all Exceptions, so they should never see this, Chris@0: // but just in case, we have to tell them something. Chris@17: \Drupal::messenger()->addError(t('Fatal error trying to download.')); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks for file transfer backends and prepares a form fragment about them. Chris@0: * Chris@0: * @param array $form Chris@0: * Reference to the form array we're building. Chris@0: * @param string $operation Chris@0: * The update manager operation we're in the middle of. Can be either 'update' Chris@0: * or 'install'. Use to provide operation-specific interface text. Chris@0: * Chris@0: * @return Chris@0: * TRUE if the update manager should continue to the next step in the Chris@0: * workflow, or FALSE if we've hit a fatal configuration and must halt the Chris@0: * workflow. Chris@0: */ Chris@0: function _update_manager_check_backends(&$form, $operation) { Chris@0: // If file transfers will be performed locally, we do not need to display any Chris@0: // warnings or notices to the user and should automatically continue the Chris@0: // workflow, since we won't be using a FileTransfer backend that requires Chris@0: // user input or a specific server configuration. Chris@0: if (update_manager_local_transfers_allowed()) { Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: // Otherwise, show the available backends. Chris@0: $form['available_backends'] = [ Chris@0: '#prefix' => '
', Chris@0: '#suffix' => '
', Chris@0: ]; Chris@0: Chris@0: $available_backends = drupal_get_filetransfer_info(); Chris@0: if (empty($available_backends)) { Chris@0: if ($operation == 'update') { Chris@0: $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 Extending Drupal 8.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']); Chris@0: } Chris@0: else { Chris@0: $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 Extending Drupal 8.', [':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview']); Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $backend_names = []; Chris@0: foreach ($available_backends as $backend) { Chris@0: $backend_names[] = $backend['title']; Chris@0: } Chris@0: if ($operation == 'update') { Chris@0: $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural( Chris@0: count($available_backends), Chris@0: 'Updating modules and themes requires @backends access to your server. See Extending Drupal 8 for other update methods.', Chris@0: 'Updating modules and themes requires access to your server via one of the following methods: @backends. See Extending Drupal 8 for other update methods.', Chris@0: [ Chris@0: '@backends' => implode(', ', $backend_names), Chris@0: ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview', Chris@0: ]); Chris@0: } Chris@0: else { Chris@0: $form['available_backends']['#markup'] = \Drupal::translation()->formatPlural( Chris@0: count($available_backends), Chris@0: 'Installing modules and themes requires @backends access to your server. See Extending Drupal 8 for other installation methods.', Chris@0: 'Installing modules and themes requires access to your server via one of the following methods: @backends. See Extending Drupal 8 for other installation methods.', Chris@0: [ Chris@0: '@backends' => implode(', ', $backend_names), Chris@0: ':doc_url' => 'https://www.drupal.org/docs/8/extending-drupal-8/overview', Chris@0: ]); Chris@0: } Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Unpacks a downloaded archive file. Chris@0: * Chris@0: * @param string $file Chris@0: * The filename of the archive you wish to extract. Chris@0: * @param string $directory Chris@0: * The directory you wish to extract the archive into. Chris@0: * Chris@0: * @return Archiver Chris@0: * The Archiver object used to extract the archive. Chris@0: * Chris@0: * @throws Exception Chris@0: */ Chris@0: function update_manager_archive_extract($file, $directory) { Chris@0: $archiver = archiver_get_archiver($file); Chris@0: if (!$archiver) { Chris@0: throw new Exception(t('Cannot extract %file, not a valid archive.', ['%file' => $file])); Chris@0: } Chris@0: Chris@0: // Remove the directory if it exists, otherwise it might contain a mixture of Chris@0: // old files mixed with the new files (e.g. in cases where files were removed Chris@0: // from a later release). Chris@0: $files = $archiver->listContents(); Chris@0: Chris@0: // Unfortunately, we can only use the directory name to determine the project Chris@0: // name. Some archivers list the first file as the directory (i.e., MODULE/) Chris@0: // and others list an actual file (i.e., MODULE/README.TXT). Chris@0: $project = strtok($files[0], '/\\'); Chris@0: Chris@0: $extract_location = $directory . '/' . $project; Chris@0: if (file_exists($extract_location)) { Chris@18: try { Chris@18: \Drupal::service('file_system')->deleteRecursive($extract_location); Chris@18: } Chris@18: catch (FileException $e) { Chris@18: // Ignore failed deletes. Chris@18: } Chris@0: } Chris@0: Chris@0: $archiver->extract($directory); Chris@0: return $archiver; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Verifies an archive after it has been downloaded and extracted. Chris@0: * Chris@0: * This function is responsible for invoking hook_verify_update_archive(). Chris@0: * Chris@0: * @param string $project Chris@0: * The short name of the project to download. Chris@0: * @param string $archive_file Chris@0: * The filename of the unextracted archive. Chris@0: * @param string $directory Chris@0: * The directory that the archive was extracted into. Chris@0: * Chris@0: * @return array Chris@0: * An array of error messages to display if the archive was invalid. If there Chris@0: * are no errors, it will be an empty array. Chris@0: */ Chris@0: function update_manager_archive_verify($project, $archive_file, $directory) { Chris@0: return \Drupal::moduleHandler()->invokeAll('verify_update_archive', [$project, $archive_file, $directory]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Copies a file from the specified URL to the temporary directory for updates. Chris@0: * Chris@0: * Returns the local path if the file has already been downloaded. Chris@0: * Chris@0: * @param $url Chris@0: * The URL of the file on the server. Chris@0: * Chris@0: * @return string Chris@0: * Path to local file. Chris@0: */ Chris@0: function update_manager_file_get($url) { Chris@0: $parsed_url = parse_url($url); Chris@0: $remote_schemes = ['http', 'https', 'ftp', 'ftps', 'smb', 'nfs']; Chris@0: if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], $remote_schemes)) { Chris@0: // This is a local file, just return the path. Chris@14: return \Drupal::service('file_system')->realpath($url); Chris@0: } Chris@0: Chris@0: // Check the cache and download the file if needed. Chris@0: $cache_directory = _update_manager_cache_directory(); Chris@18: $local = $cache_directory . '/' . \Drupal::service('file_system')->basename($parsed_url['path']); Chris@0: Chris@0: if (!file_exists($local) || update_delete_file_if_stale($local)) { Chris@0: return system_retrieve_file($url, $local, FALSE, FILE_EXISTS_REPLACE); Chris@0: } Chris@0: else { Chris@0: return $local; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements callback_batch_operation(). Chris@0: * Chris@0: * Downloads, unpacks, and verifies a project. Chris@0: * Chris@0: * This function assumes that the provided URL points to a file archive of some Chris@0: * sort. The URL can have any scheme that we have a file stream wrapper to Chris@0: * support. The file is downloaded to a local cache. Chris@0: * Chris@0: * @param string $project Chris@0: * The short name of the project to download. Chris@0: * @param string $url Chris@0: * The URL to download a specific project release archive file. Chris@0: * @param array $context Chris@0: * Reference to an array used for Batch API storage. Chris@0: * Chris@0: * @see update_manager_download_page() Chris@0: */ Chris@0: function update_manager_batch_project_get($project, $url, &$context) { Chris@0: // This is here to show the user that we are in the process of downloading. Chris@0: if (!isset($context['sandbox']['started'])) { Chris@0: $context['sandbox']['started'] = TRUE; Chris@0: $context['message'] = t('Downloading %project', ['%project' => $project]); Chris@0: $context['finished'] = 0; Chris@0: return; Chris@0: } Chris@0: Chris@0: // Actually try to download the file. Chris@0: if (!($local_cache = update_manager_file_get($url))) { Chris@0: $context['results']['errors'][$project] = t('Failed to download %project from %url', ['%project' => $project, '%url' => $url]); Chris@0: return; Chris@0: } Chris@0: Chris@0: // Extract it. Chris@0: $extract_directory = _update_manager_extract_directory(); Chris@0: try { Chris@0: update_manager_archive_extract($local_cache, $extract_directory); Chris@0: } Chris@0: catch (Exception $e) { Chris@0: $context['results']['errors'][$project] = $e->getMessage(); Chris@0: return; Chris@0: } Chris@0: Chris@0: // Verify it. Chris@0: $archive_errors = update_manager_archive_verify($project, $local_cache, $extract_directory); Chris@0: if (!empty($archive_errors)) { Chris@0: // We just need to make sure our array keys don't collide, so use the Chris@0: // numeric keys from the $archive_errors array. Chris@0: foreach ($archive_errors as $key => $error) { Chris@0: $context['results']['errors']["$project-$key"] = $error; Chris@0: } Chris@0: return; Chris@0: } Chris@0: Chris@0: // Yay, success. Chris@0: $context['results']['projects'][$project] = $url; Chris@0: $context['finished'] = 1; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines if file transfers will be performed locally. Chris@0: * Chris@0: * If the server is configured such that webserver-created files have the same Chris@0: * owner as the configuration directory (e.g., sites/default) where new code Chris@0: * will eventually be installed, the update manager can transfer files entirely Chris@0: * locally, without changing their ownership (in other words, without prompting Chris@0: * the user for FTP, SSH or other credentials). Chris@0: * Chris@0: * This server configuration is an inherent security weakness because it allows Chris@0: * a malicious webserver process to append arbitrary PHP code and then execute Chris@0: * it. However, it is supported here because it is a common configuration on Chris@0: * shared hosting, and there is nothing Drupal can do to prevent it. Chris@0: * Chris@0: * @return Chris@0: * TRUE if local file transfers are allowed on this server, or FALSE if not. Chris@0: * Chris@0: * @see install_check_requirements() Chris@0: */ Chris@0: function update_manager_local_transfers_allowed() { Chris@0: // Compare the owner of a webserver-created temporary file to the owner of Chris@0: // the configuration directory to determine if local transfers will be Chris@0: // allowed. Chris@18: $temporary_file = \Drupal::service('file_system')->tempnam('temporary://', 'update_'); Chris@0: $site_path = \Drupal::service('site.path'); Chris@0: $local_transfers_allowed = fileowner($temporary_file) === fileowner($site_path); Chris@0: Chris@0: // Clean up. If this fails, we can ignore it (since this is just a temporary Chris@0: // file anyway). Chris@0: @drupal_unlink($temporary_file); Chris@0: Chris@0: return $local_transfers_allowed; Chris@0: }