2009-10-15 21:19:31 +00:00
< ? php
/**
* @ file
2012-06-06 15:33:53 +00:00
* Administrative screens and processing functions of the Update Manager module .
*
2009-10-15 21:19:31 +00:00
* This allows site administrators with the 'administer software updates'
* permission to either upgrade existing projects , or download and install new
2024-01-11 14:36:13 +00:00
* ones , so long as the kill switch setting ( 'allow_authorize_operations' ) is
2013-01-07 11:45:26 +00:00
* not FALSE .
2009-10-15 21:19:31 +00:00
*
* To install new code , the administrator is prompted for either the URL of an
* archive file , or to directly upload the archive file . The archive is loaded
* into a temporary location , extracted , and verified . If everything is
2012-06-06 15:33:53 +00:00
* successful , the user is redirected to authorize . php to type in file transfer
* credentials and authorize the installation to proceed with elevated
* privileges , such that the extracted files can be copied out of the temporary
* location and into the live web root .
2009-10-15 21:19:31 +00:00
*
* Updating existing code is a more elaborate process . The first step is a
2012-06-06 15:33:53 +00:00
* selection form where the user is presented with a table of installed projects
* that are missing newer releases . The user selects which projects they wish to
* update , and presses the " Download updates " button to continue . This sets up a
* batch to fetch all the selected releases , and redirects to
* admin / update / download to display the batch progress bar as it runs . Each
* batch operation is responsible for downloading a single file , extracting the
* archive , and verifying the contents . If there are any errors , the user is
* redirected back to the first page with the error messages . If all downloads
2015-04-02 10:59:11 +00:00
* were extracted and verified , the user is instead redirected to
2012-06-06 15:33:53 +00:00
* admin / update / ready , a landing page which reminds them to backup their
* database and asks if they want to put the site offline during the update .
* Once the user presses the " Install updates " button , they are redirected to
* authorize . php to supply their web root file access credentials . The
* authorized operation ( which lives in update . authorize . inc ) sets up a batch to
* copy each extracted update from the temporary location into the live web
* root .
2009-10-15 21:19:31 +00:00
*/
2010-01-30 07:59:26 +00:00
Issue #2244513 by kim.pepper, phenaproxima, 20th, andrei.dincu, beejeebus, Berdir, alexpott, jibran, andypost, larowlan, Chadwick Wood, acbramley, Wim Leers, sun, xjm, YesCT, chx, tim.plunkett: Move the unmanaged file APIs to the file_system service (file.inc)
2019-02-23 22:35:15 +00:00
use Drupal\Core\File\Exception\FileException ;
2023-06-23 00:28:41 +00:00
use Drupal\Core\File\Exception\InvalidStreamWrapperException ;
2019-10-08 20:49:15 +00:00
use Drupal\Core\File\FileSystemInterface ;
2023-06-23 00:28:41 +00:00
use Drupal\Core\Url ;
use GuzzleHttp\Exception\TransferException ;
Issue #1668866 by ParisLiakos, aspilicious, tim.plunkett, pdrake, g.oechsler, dawehner, Berdir, corvus_ch, damiankloip, disasm, marcingy, neclimdul: Replace drupal_goto() with RedirectResponse.
2013-06-19 16:07:30 +00:00
use Symfony\Component\HttpFoundation\RedirectResponse ;
2012-03-11 00:23:05 +00:00
2009-10-15 21:19:31 +00:00
/**
2012-06-06 15:33:53 +00:00
* Batch callback : Performs actions when the download batch is completed .
*
* @ param $success
* TRUE if the batch operation was successful , FALSE if there were errors .
* @ param $results
* An associative array of results from the batch operation .
2009-10-15 21:19:31 +00:00
*/
function update_manager_download_batch_finished ( $success , $results ) {
2009-10-29 07:08:38 +00:00
if ( ! empty ( $results [ 'errors' ])) {
2017-03-04 01:20:24 +00:00
$item_list = [
2013-08-13 10:53:04 +00:00
'#theme' => 'item_list' ,
'#title' => t ( 'Downloading updates failed:' ),
'#items' => $results [ 'errors' ],
2017-03-04 01:20:24 +00:00
];
2018-05-01 09:15:07 +00:00
\Drupal :: messenger () -> addError ( \Drupal :: service ( 'renderer' ) -> render ( $item_list ));
2009-10-29 07:08:38 +00:00
}
elseif ( $success ) {
2018-05-01 09:15:07 +00:00
\Drupal :: messenger () -> addStatus ( t ( 'Updates downloaded successfully.' ));
2020-02-20 21:38:33 +00:00
\Drupal :: request () -> getSession () -> set ( 'update_manager_update_projects' , $results [ 'projects' ]);
2019-04-16 05:38:27 +00:00
return new RedirectResponse ( Url :: fromRoute ( 'update.confirmation_page' , [], [ 'absolute' => TRUE ]) -> toString ());
2009-10-15 21:19:31 +00:00
}
else {
2009-10-29 07:08:38 +00:00
// Ideally we're catching all Exceptions, so they should never see this,
// but just in case, we have to tell them something.
2018-05-01 09:15:07 +00:00
\Drupal :: messenger () -> addError ( t ( 'Fatal error trying to download.' ));
2009-10-15 21:19:31 +00:00
}
}
2011-01-03 02:41:33 +00:00
/**
* Checks for file transfer backends and prepares a form fragment about them .
*
* @ param array $form
* Reference to the form array we ' re building .
* @ param string $operation
2012-06-06 15:33:53 +00:00
* The update manager operation we 're in the middle of. Can be either ' update '
* or 'install' . Use to provide operation - specific interface text .
2011-01-03 02:41:33 +00:00
*
Issue #2941148 by quietone, bruno.bicudo, ravi.shankar, Sweetchuck, beatrizrodrigues, lucienchalom, VitaliyB98, WagnerMelo, sophiavs, ankitjain28may, daffie, longwave, Sutharsan, borisson_, cosmicdreams, heykarthikwithu, catch: Fix Drupal.Commenting.FunctionComment.MissingReturnType
2022-09-27 09:58:26 +00:00
* @ return bool
2012-06-06 15:33:53 +00:00
* TRUE if the update manager should continue to the next step in the
2011-01-03 02:41:33 +00:00
* workflow , or FALSE if we ' ve hit a fatal configuration and must halt the
* workflow .
*/
function _update_manager_check_backends ( & $form , $operation ) {
// If file transfers will be performed locally, we do not need to display any
// warnings or notices to the user and should automatically continue the
// workflow, since we won't be using a FileTransfer backend that requires
// user input or a specific server configuration.
if ( update_manager_local_transfers_allowed ()) {
return TRUE ;
}
// Otherwise, show the available backends.
2017-03-04 01:20:24 +00:00
$form [ 'available_backends' ] = [
2011-01-03 02:41:33 +00:00
'#prefix' => '<p>' ,
'#suffix' => '</p>' ,
2017-03-04 01:20:24 +00:00
];
2011-01-03 02:41:33 +00:00
$available_backends = drupal_get_filetransfer_info ();
if ( empty ( $available_backends )) {
if ( $operation == 'update' ) {
2021-06-15 08:46:42 +00:00
$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</a>.' , [ ':doc_url' => 'https://www.drupal.org/docs/extending-drupal/overview' ]);
2011-01-03 02:41:33 +00:00
}
else {
2021-06-15 08:46:42 +00:00
$form [ 'available_backends' ][ '#markup' ] = t ( 'Your server does not support adding modules and themes from this interface. Instead, add modules and themes by uploading them directly to the server, as documented in <a href=":doc_url">Extending Drupal</a>.' , [ ':doc_url' => 'https://www.drupal.org/docs/extending-drupal/overview' ]);
2011-01-03 02:41:33 +00:00
}
return FALSE ;
}
2017-03-04 01:20:24 +00:00
$backend_names = [];
2011-01-03 02:41:33 +00:00
foreach ( $available_backends as $backend ) {
$backend_names [] = $backend [ 'title' ];
}
if ( $operation == 'update' ) {
2015-01-10 13:56:47 +00:00
$form [ 'available_backends' ][ '#markup' ] = \Drupal :: translation () -> formatPlural (
2011-01-03 02:41:33 +00:00
count ( $available_backends ),
2021-06-15 08:46:42 +00:00
'Updating modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal</a> for other update methods.' ,
'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</a> for other update methods.' ,
2017-03-04 01:20:24 +00:00
[
2011-01-03 02:41:33 +00:00
'@backends' => implode ( ', ' , $backend_names ),
2021-06-15 08:46:42 +00:00
':doc_url' => 'https://www.drupal.org/docs/extending-drupal/overview' ,
2017-03-04 01:20:24 +00:00
]);
2011-01-03 02:41:33 +00:00
}
else {
2015-01-10 13:56:47 +00:00
$form [ 'available_backends' ][ '#markup' ] = \Drupal :: translation () -> formatPlural (
2011-01-03 02:41:33 +00:00
count ( $available_backends ),
2021-06-15 08:46:42 +00:00
'Adding modules and themes requires <strong>@backends access</strong> to your server. See <a href=":doc_url">Extending Drupal</a> for other methods.' ,
'Adding modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See <a href=":doc_url">Extending Drupal</a> for other methods.' ,
2017-03-04 01:20:24 +00:00
[
2011-01-03 02:41:33 +00:00
'@backends' => implode ( ', ' , $backend_names ),
2021-06-15 08:46:42 +00:00
':doc_url' => 'https://www.drupal.org/docs/extending-drupal/overview' ,
2017-03-04 01:20:24 +00:00
]);
2011-01-03 02:41:33 +00:00
}
return TRUE ;
}
2009-10-15 21:19:31 +00:00
/**
2012-06-06 15:33:53 +00:00
* Unpacks a downloaded archive file .
2009-10-15 21:19:31 +00:00
*
* @ param string $file
* The filename of the archive you wish to extract .
* @ param string $directory
2009-10-24 11:43:42 +00:00
* The directory you wish to extract the archive into .
2012-06-06 15:33:53 +00:00
*
2019-05-17 01:04:57 +00:00
* @ return \Drupal\Core\Archiver\ArchiverInterface
2009-10-24 11:43:42 +00:00
* The Archiver object used to extract the archive .
2012-06-06 15:33:53 +00:00
*
* @ throws Exception
2009-10-15 21:19:31 +00:00
*/
function update_manager_archive_extract ( $file , $directory ) {
2019-05-17 01:04:57 +00:00
/** @var \Drupal\Core\Archiver\ArchiverInterface $archiver */
$archiver = \Drupal :: service ( 'plugin.manager.archiver' ) -> getInstance ([
'filepath' => $file ,
]);
2009-10-24 11:43:42 +00:00
if ( ! $archiver ) {
Issue #2055851 by andypost, jungle, dawehner, Mac_Weber, sja112, borisson_, fietserwin, init90, Gábor Hojtsy, xjm, effulgentsia, tim.plunkett: Remove translation of exception messages
2020-05-19 00:21:21 +00:00
throw new Exception ( " Cannot extract ' $file ', not a valid archive " );
2009-10-15 21:19:31 +00:00
}
2010-12-01 00:23:36 +00:00
// Remove the directory if it exists, otherwise it might contain a mixture of
// old files mixed with the new files (e.g. in cases where files were removed
// from a later release).
$files = $archiver -> listContents ();
2011-01-28 07:20:50 +00:00
// Unfortunately, we can only use the directory name to determine the project
// name. Some archivers list the first file as the directory (i.e., MODULE/)
// and others list an actual file (i.e., MODULE/README.TXT).
$project = strtok ( $files [ 0 ], '/\\' );
2010-12-01 00:23:36 +00:00
$extract_location = $directory . '/' . $project ;
if ( file_exists ( $extract_location )) {
Issue #2244513 by kim.pepper, phenaproxima, 20th, andrei.dincu, beejeebus, Berdir, alexpott, jibran, andypost, larowlan, Chadwick Wood, acbramley, Wim Leers, sun, xjm, YesCT, chx, tim.plunkett: Move the unmanaged file APIs to the file_system service (file.inc)
2019-02-23 22:35:15 +00:00
try {
\Drupal :: service ( 'file_system' ) -> deleteRecursive ( $extract_location );
}
catch ( FileException $e ) {
// Ignore failed deletes.
}
2010-12-01 00:23:36 +00:00
}
2009-10-24 11:43:42 +00:00
$archiver -> extract ( $directory );
return $archiver ;
2009-10-15 21:19:31 +00:00
}
/**
2012-06-06 15:33:53 +00:00
* Verifies an archive after it has been downloaded and extracted .
2009-10-15 21:19:31 +00:00
*
* This function is responsible for invoking hook_verify_update_archive () .
*
* @ param string $project
* The short name of the project to download .
* @ param string $archive_file
* The filename of the unextracted archive .
* @ param string $directory
* The directory that the archive was extracted into .
*
2010-12-06 06:50:08 +00:00
* @ return array
2012-06-06 15:33:53 +00:00
* An array of error messages to display if the archive was invalid . If there
* are no errors , it will be an empty array .
2009-10-15 21:19:31 +00:00
*/
function update_manager_archive_verify ( $project , $archive_file , $directory ) {
2017-03-04 01:20:24 +00:00
return \Drupal :: moduleHandler () -> invokeAll ( 'verify_update_archive' , [ $project , $archive_file , $directory ]);
2009-10-15 21:19:31 +00:00
}
/**
2012-06-06 15:33:53 +00:00
* Copies a file from the specified URL to the temporary directory for updates .
2009-10-15 21:19:31 +00:00
*
2012-06-06 15:33:53 +00:00
* Returns the local path if the file has already been downloaded .
2009-10-15 21:19:31 +00:00
*
* @ param $url
* The URL of the file on the server .
*
2023-06-23 00:28:41 +00:00
* @ return string | false
* Path to local file , or FALSE if it could not be retrieved .
2009-10-15 21:19:31 +00:00
*/
function update_manager_file_get ( $url ) {
$parsed_url = parse_url ( $url );
2017-03-04 01:20:24 +00:00
$remote_schemes = [ 'http' , 'https' , 'ftp' , 'ftps' , 'smb' , 'nfs' ];
2015-08-29 06:01:22 +00:00
if ( ! isset ( $parsed_url [ 'scheme' ]) || ! in_array ( $parsed_url [ 'scheme' ], $remote_schemes )) {
2009-10-15 21:19:31 +00:00
// This is a local file, just return the path.
2017-11-14 15:53:29 +00:00
return \Drupal :: service ( 'file_system' ) -> realpath ( $url );
2009-10-15 21:19:31 +00:00
}
// Check the cache and download the file if needed.
2011-05-14 02:06:54 +00:00
$cache_directory = _update_manager_cache_directory ();
2019-03-07 09:12:01 +00:00
$local = $cache_directory . '/' . \Drupal :: service ( 'file_system' ) -> basename ( $parsed_url [ 'path' ]);
2009-10-15 21:19:31 +00:00
2010-12-15 03:52:05 +00:00
if ( ! file_exists ( $local ) || update_delete_file_if_stale ( $local )) {
2023-06-23 00:28:41 +00:00
try {
$data = ( string ) \Drupal :: httpClient () -> get ( $url ) -> getBody ();
return \Drupal :: service ( 'file_system' ) -> saveData ( $data , $local , FileSystemInterface :: EXISTS_REPLACE );
}
catch ( TransferException $exception ) {
\Drupal :: messenger () -> addError ( t ( 'Failed to fetch file due to error "%error"' , [ '%error' => $exception -> getMessage ()]));
}
catch ( FileException | InvalidStreamWrapperException $e ) {
\Drupal :: messenger () -> addError ( t ( 'Failed to save file due to error "%error"' , [ '%error' => $e -> getMessage ()]));
}
return FALSE ;
2009-10-15 21:19:31 +00:00
}
else {
return $local ;
}
}
/**
2015-02-13 19:51:15 +00:00
* Implements callback_batch_operation () .
*
* Downloads , unpacks , and verifies a project .
2009-10-15 21:19:31 +00:00
*
2012-06-06 15:33:53 +00:00
* This function assumes that the provided URL points to a file archive of some
* sort . The URL can have any scheme that we have a file stream wrapper to
* support . The file is downloaded to a local cache .
2009-10-15 21:19:31 +00:00
*
* @ param string $project
* The short name of the project to download .
* @ param string $url
* The URL to download a specific project release archive file .
2011-05-08 19:50:38 +00:00
* @ param array $context
2012-06-06 15:33:53 +00:00
* Reference to an array used for Batch API storage .
2009-10-15 21:19:31 +00:00
*
* @ see update_manager_download_page ()
*/
function update_manager_batch_project_get ( $project , $url , & $context ) {
// This is here to show the user that we are in the process of downloading.
if ( ! isset ( $context [ 'sandbox' ][ 'started' ])) {
$context [ 'sandbox' ][ 'started' ] = TRUE ;
2017-03-04 01:20:24 +00:00
$context [ 'message' ] = t ( 'Downloading %project' , [ '%project' => $project ]);
2009-10-15 21:19:31 +00:00
$context [ 'finished' ] = 0 ;
return ;
}
// Actually try to download the file.
if ( ! ( $local_cache = update_manager_file_get ( $url ))) {
2017-03-04 01:20:24 +00:00
$context [ 'results' ][ 'errors' ][ $project ] = t ( 'Failed to download %project from %url' , [ '%project' => $project , '%url' => $url ]);
2009-10-15 21:19:31 +00:00
return ;
}
// Extract it.
$extract_directory = _update_manager_extract_directory ();
try {
update_manager_archive_extract ( $local_cache , $extract_directory );
}
catch ( Exception $e ) {
2009-10-29 07:08:38 +00:00
$context [ 'results' ][ 'errors' ][ $project ] = $e -> getMessage ();
2009-10-15 21:19:31 +00:00
return ;
}
// Verify it.
2010-12-06 06:50:08 +00:00
$archive_errors = update_manager_archive_verify ( $project , $local_cache , $extract_directory );
if ( ! empty ( $archive_errors )) {
// We just need to make sure our array keys don't collide, so use the
// numeric keys from the $archive_errors array.
foreach ( $archive_errors as $key => $error ) {
$context [ 'results' ][ 'errors' ][ " $project - $key " ] = $error ;
}
2009-10-15 21:19:31 +00:00
return ;
}
// Yay, success.
2009-10-29 07:08:38 +00:00
$context [ 'results' ][ 'projects' ][ $project ] = $url ;
2009-10-15 21:19:31 +00:00
$context [ 'finished' ] = 1 ;
}
2011-01-03 02:41:33 +00:00
/**
* Determines if file transfers will be performed locally .
*
* If the server is configured such that webserver - created files have the same
2012-06-06 15:33:53 +00:00
* owner as the configuration directory ( e . g . , sites / default ) where new code
* will eventually be installed , the update manager can transfer files entirely
2011-01-03 02:41:33 +00:00
* locally , without changing their ownership ( in other words , without prompting
* the user for FTP , SSH or other credentials ) .
*
* This server configuration is an inherent security weakness because it allows
* a malicious webserver process to append arbitrary PHP code and then execute
* it . However , it is supported here because it is a common configuration on
* shared hosting , and there is nothing Drupal can do to prevent it .
*
Issue #2941148 by quietone, bruno.bicudo, ravi.shankar, Sweetchuck, beatrizrodrigues, lucienchalom, VitaliyB98, WagnerMelo, sophiavs, ankitjain28may, daffie, longwave, Sutharsan, borisson_, cosmicdreams, heykarthikwithu, catch: Fix Drupal.Commenting.FunctionComment.MissingReturnType
2022-09-27 09:58:26 +00:00
* @ return bool
2011-01-03 02:41:33 +00:00
* TRUE if local file transfers are allowed on this server , or FALSE if not .
*
* @ see install_check_requirements ()
*/
function update_manager_local_transfers_allowed () {
2019-04-26 02:44:21 +00:00
$file_system = \Drupal :: service ( 'file_system' );
2011-01-03 02:41:33 +00:00
// Compare the owner of a webserver-created temporary file to the owner of
// the configuration directory to determine if local transfers will be
// allowed.
2019-03-07 09:12:01 +00:00
$temporary_file = \Drupal :: service ( 'file_system' ) -> tempnam ( 'temporary://' , 'update_' );
2020-03-05 11:22:39 +00:00
$site_path = \Drupal :: getContainer () -> getParameter ( 'site.path' );
2015-06-09 12:15:19 +00:00
$local_transfers_allowed = fileowner ( $temporary_file ) === fileowner ( $site_path );
2011-01-03 02:41:33 +00:00
// Clean up. If this fails, we can ignore it (since this is just a temporary
// file anyway).
2019-04-26 02:44:21 +00:00
@ $file_system -> unlink ( $temporary_file );
2011-01-03 02:41:33 +00:00
return $local_transfers_allowed ;
}