2007-07-11 15:15:40 +00:00
< ? php
/**
* @ file
* Code required only when fetching information about available updates .
*/
2013-04-10 23:53:06 +00:00
use Guzzle\Http\Exception\RequestException ;
2007-07-11 15:15:40 +00:00
/**
2012-06-06 15:33:53 +00:00
* Page callback : Checks for updates and displays the update status report .
*
* Manually checks the update status without the use of cron .
*
* @ see update_menu ()
2007-07-11 15:15:40 +00:00
*/
function update_manual_status () {
2009-10-13 02:14:05 +00:00
_update_refresh ();
$batch = array (
'operations' => array (
array ( 'update_fetch_data_batch' , array ()),
),
'finished' => 'update_fetch_data_finished' ,
'title' => t ( 'Checking available update data' ),
'progress_message' => t ( 'Trying to check available update data ...' ),
'error_message' => t ( 'Error checking available update data.' ),
'file' => drupal_get_path ( 'module' , 'update' ) . '/update.fetch.inc' ,
);
batch_set ( $batch );
batch_process ( 'admin/reports/updates' );
}
/**
2012-06-06 15:33:53 +00:00
* Batch callback : Processes a step in batch for fetching available update data .
*
* @ param $context
* Reference to an array used for Batch API storage .
2009-10-13 02:14:05 +00:00
*/
function update_fetch_data_batch ( & $context ) {
2013-04-03 08:44:04 +00:00
$queue = Drupal :: queue ( 'update_fetch_tasks' );
2009-10-13 02:14:05 +00:00
if ( empty ( $context [ 'sandbox' ][ 'max' ])) {
$context [ 'finished' ] = 0 ;
$context [ 'sandbox' ][ 'max' ] = $queue -> numberOfItems ();
$context [ 'sandbox' ][ 'progress' ] = 0 ;
$context [ 'message' ] = t ( 'Checking available update data ...' );
$context [ 'results' ][ 'updated' ] = 0 ;
$context [ 'results' ][ 'failures' ] = 0 ;
$context [ 'results' ][ 'processed' ] = 0 ;
}
// Grab another item from the fetch queue.
for ( $i = 0 ; $i < 5 ; $i ++ ) {
if ( $item = $queue -> claimItem ()) {
if ( _update_process_fetch_task ( $item -> data )) {
$context [ 'results' ][ 'updated' ] ++ ;
$context [ 'message' ] = t ( 'Checked available update data for %title.' , array ( '%title' => $item -> data [ 'info' ][ 'name' ]));
}
else {
$context [ 'message' ] = t ( 'Failed to check available update data for %title.' , array ( '%title' => $item -> data [ 'info' ][ 'name' ]));
$context [ 'results' ][ 'failures' ] ++ ;
}
$context [ 'sandbox' ][ 'progress' ] ++ ;
$context [ 'results' ][ 'processed' ] ++ ;
$context [ 'finished' ] = $context [ 'sandbox' ][ 'progress' ] / $context [ 'sandbox' ][ 'max' ];
$queue -> deleteItem ( $item );
}
else {
// If the queue is currently empty, we're done. It's possible that
// another thread might have added new fetch tasks while we were
// processing this batch. In that case, the usual 'finished' math could
// get confused, since we'd end up processing more tasks that we thought
// we had when we started and initialized 'max' with numberOfItems(). By
// forcing 'finished' to be exactly 1 here, we ensure that batch
// processing is terminated.
$context [ 'finished' ] = 1 ;
return ;
}
}
}
/**
2012-06-06 15:33:53 +00:00
* Batch callback : Performs actions when all fetch tasks have been completed .
2009-10-13 02:14:05 +00:00
*
* @ param $success
2012-06-06 15:33:53 +00:00
* TRUE if the batch operation was successful ; FALSE if there were errors .
2009-10-13 02:14:05 +00:00
* @ param $results
2012-06-06 15:33:53 +00:00
* An associative array of results from the batch operation , including the key
2009-10-13 02:14:05 +00:00
* 'updated' which holds the total number of projects we fetched available
* update data for .
*/
function update_fetch_data_finished ( $success , $results ) {
if ( $success ) {
if ( ! empty ( $results )) {
if ( ! empty ( $results [ 'updated' ])) {
drupal_set_message ( format_plural ( $results [ 'updated' ], 'Checked available update data for one project.' , 'Checked available update data for @count projects.' ));
}
if ( ! empty ( $results [ 'failures' ])) {
drupal_set_message ( format_plural ( $results [ 'failures' ], 'Failed to get available update data for one project.' , 'Failed to get available update data for @count projects.' ), 'error' );
}
}
2007-07-11 15:15:40 +00:00
}
else {
2009-10-13 02:14:05 +00:00
drupal_set_message ( t ( 'An error occurred trying to get available update data.' ), 'error' );
2007-07-11 15:15:40 +00:00
}
}
/**
2012-06-06 15:33:53 +00:00
* Attempts to drain the queue of tasks for release history data to fetch .
2007-07-11 15:15:40 +00:00
*/
2009-10-13 02:14:05 +00:00
function _update_fetch_data () {
2013-04-03 08:44:04 +00:00
$queue = Drupal :: queue ( 'update_fetch_tasks' );
2012-08-03 16:52:07 +00:00
$end = time () + config ( 'update.settings' ) -> get ( 'fetch.timeout' );
2009-10-13 02:14:05 +00:00
while ( time () < $end && ( $item = $queue -> claimItem ())) {
_update_process_fetch_task ( $item -> data );
$queue -> deleteItem ( $item );
}
}
/**
2012-06-06 15:33:53 +00:00
* Processes a task to fetch available update data for a single project .
2009-10-13 02:14:05 +00:00
*
2013-03-20 11:51:03 +00:00
* Once the release history XML data is downloaded , it is parsed and saved in an
* entry just for that project .
2009-10-13 02:14:05 +00:00
*
* @ param $project
* Associative array of information about the project to fetch data for .
2012-06-06 15:33:53 +00:00
*
2009-10-13 02:14:05 +00:00
* @ return
* TRUE if we fetched parsable XML , otherwise FALSE .
*/
function _update_process_fetch_task ( $project ) {
2007-07-11 15:15:40 +00:00
global $base_url ;
2012-08-03 16:52:07 +00:00
$update_config = config ( 'update.settings' );
2009-06-06 06:26:13 +00:00
$fail = & drupal_static ( __FUNCTION__ , array ());
2009-10-13 02:14:05 +00:00
// This can be in the middle of a long-running batch, so REQUEST_TIME won't
// necessarily be valid.
2013-03-20 11:51:03 +00:00
$request_time_difference = time () - REQUEST_TIME ;
2009-10-13 02:14:05 +00:00
if ( empty ( $fail )) {
// If we have valid data about release history XML servers that we have
2013-03-20 11:51:03 +00:00
// failed to fetch from on previous attempts, load that.
$fail = Drupal :: keyValueExpirable ( 'update' ) -> get ( 'fetch_failures' );
2009-10-13 02:14:05 +00:00
}
2012-08-03 16:52:07 +00:00
$max_fetch_attempts = $update_config -> get ( 'fetch.max_attempts' );
2009-10-13 02:14:05 +00:00
$success = FALSE ;
$available = array ();
2010-05-01 08:12:23 +00:00
$site_key = drupal_hmac_base64 ( $base_url , drupal_get_private_key ());
2009-10-13 02:14:05 +00:00
$url = _update_build_fetch_url ( $project , $site_key );
$fetch_url_base = _update_get_fetch_url_base ( $project );
$project_name = $project [ 'name' ];
if ( empty ( $fail [ $fetch_url_base ]) || $fail [ $fetch_url_base ] < $max_fetch_attempts ) {
2013-04-10 23:53:06 +00:00
try {
$data = Drupal :: httpClient ()
-> get ( $url , array ( 'Accept' => 'text/xml' ))
-> send ()
-> getBody ( TRUE );
2012-05-27 15:19:58 +00:00
}
2013-04-10 23:53:06 +00:00
catch ( RequestException $exception ) {
watchdog_exception ( 'update' , $exception );
2009-10-13 02:14:05 +00:00
}
}
if ( ! empty ( $data )) {
$available = update_parse_xml ( $data );
// @todo: Purge release data we don't need (http://drupal.org/node/238950).
if ( ! empty ( $available )) {
// Only if we fetched and parsed something sane do we return success.
$success = TRUE ;
}
}
else {
$available [ 'project_status' ] = 'not-fetched' ;
if ( empty ( $fail [ $fetch_url_base ])) {
$fail [ $fetch_url_base ] = 1 ;
}
else {
$fail [ $fetch_url_base ] ++ ;
}
}
2012-08-03 16:52:07 +00:00
$frequency = $update_config -> get ( 'check.interval_days' );
2013-03-20 11:51:03 +00:00
$available [ 'last_fetch' ] = REQUEST_TIME + $request_time_difference ;
Drupal :: keyValueExpirable ( 'update_available_releases' ) -> setWithExpire ( $project_name , $available , $request_time_difference + ( 60 * 60 * 24 * $frequency ));
2009-10-13 02:14:05 +00:00
// Stash the $fail data back in the DB for the next 5 minutes.
2013-03-20 11:51:03 +00:00
Drupal :: keyValueExpirable ( 'update' ) -> setWithExpire ( 'fetch_failures' , $fail , $request_time_difference + ( 60 * 5 ));
2009-10-13 02:14:05 +00:00
// Whether this worked or not, we did just (try to) check for updates.
2013-03-20 11:51:03 +00:00
state () -> set ( 'update.last_check' , REQUEST_TIME + $request_time_difference );
2009-10-13 02:14:05 +00:00
// Now that we processed the fetch task for this project, clear out the
2013-03-20 11:51:03 +00:00
// record for this task so we're willing to fetch again.
drupal_container () -> get ( 'keyvalue' ) -> get ( 'update_fetch_task' ) -> delete ( $project_name );
2009-10-13 02:14:05 +00:00
return $success ;
}
/**
2013-03-20 11:51:03 +00:00
* Clears out all the available update data and initiates re - fetching .
2009-10-13 02:14:05 +00:00
*/
function _update_refresh () {
2008-01-30 10:14:42 +00:00
module_load_include ( 'inc' , 'update' , 'update.compare' );
2007-07-11 15:15:40 +00:00
2008-01-27 17:50:10 +00:00
// Since we're fetching new available update data, we want to clear
2013-03-20 11:51:03 +00:00
// of both the projects we care about, and the current update status of the
// site. We do *not* want to clear the cache of available releases just yet,
// since that data (even if it's stale) can be useful during
// update_get_projects(); for example, to modules that implement
2009-04-29 18:39:50 +00:00
// hook_system_info_alter() such as cvs_deploy.
2013-03-20 11:51:03 +00:00
Drupal :: keyValueExpirable ( 'update' ) -> delete ( 'update_project_projects' );
Drupal :: keyValueExpirable ( 'update' ) -> delete ( 'update_project_data' );
2008-01-27 17:50:10 +00:00
2007-07-11 15:15:40 +00:00
$projects = update_get_projects ();
2013-03-20 11:51:03 +00:00
// Now that we have the list of projects, we should also clear the available
// release data, since even if we fail to fetch new data, we need to clear
// out the stale data at this point.
Drupal :: keyValueExpirable ( 'update_available_releases' ) -> deleteAll ();
2009-06-06 06:26:13 +00:00
2007-07-11 15:15:40 +00:00
foreach ( $projects as $key => $project ) {
2009-10-13 02:14:05 +00:00
update_create_fetch_task ( $project );
2007-07-11 15:15:40 +00:00
}
2009-10-13 02:14:05 +00:00
}
2007-07-11 15:15:40 +00:00
2009-10-13 02:14:05 +00:00
/**
2012-06-06 15:33:53 +00:00
* Adds a task to the queue for fetching release history data for a project .
2009-10-13 02:14:05 +00:00
*
* We only create a new fetch task if there ' s no task already in the queue for
2013-03-20 11:51:03 +00:00
* this particular project ( based on 'update_fetch_task' key - value collection ) .
2009-10-13 02:14:05 +00:00
*
* @ param $project
* Associative array of information about a project as created by
2012-06-06 15:33:53 +00:00
* update_get_projects (), including keys such as 'name' ( short name ), and the
2013-03-06 22:51:39 +00:00
* 'info' array with data from a . info . yml file for the project .
2009-10-13 02:14:05 +00:00
*
* @ see update_get_projects ()
* @ see update_get_available ()
* @ see update_refresh ()
* @ see update_fetch_data ()
* @ see _update_process_fetch_task ()
*/
function _update_create_fetch_task ( $project ) {
$fetch_tasks = & drupal_static ( __FUNCTION__ , array ());
if ( empty ( $fetch_tasks )) {
2013-03-20 11:51:03 +00:00
$fetch_tasks = drupal_container () -> get ( 'keyvalue' ) -> get ( 'update_fetch_task' ) -> getAll ();
2007-07-11 15:15:40 +00:00
}
2013-03-20 11:51:03 +00:00
if ( empty ( $fetch_tasks [ $project [ 'name' ]])) {
2013-04-03 08:44:04 +00:00
$queue = Drupal :: queue ( 'update_fetch_tasks' );
2009-10-13 02:14:05 +00:00
$queue -> createItem ( $project );
2013-03-20 11:51:03 +00:00
drupal_container () -> get ( 'keyvalue' ) -> get ( 'update_fetch_task' ) -> set ( $project [ 'name' ], $project );
$fetch_tasks [ $project [ 'name' ]] = REQUEST_TIME ;
2007-07-11 15:15:40 +00:00
}
}
/**
* Generates the URL to fetch information about project updates .
*
2013-03-06 22:51:39 +00:00
* This figures out the right URL to use , based on the project ' s . info . yml file
* and the global defaults . Appends optional query arguments when the site is
2007-07-11 15:15:40 +00:00
* configured to report usage stats .
*
* @ param $project
* The array of project information from update_get_projects () .
* @ param $site_key
2012-06-06 15:33:53 +00:00
* ( optional ) The anonymous site key hash . Defaults to an empty string .
*
* @ return
* The URL for fetching information about updates to the specified project .
2007-07-11 15:15:40 +00:00
*
2009-10-13 02:14:05 +00:00
* @ see update_fetch_data ()
* @ see _update_process_fetch_task ()
2007-07-11 15:15:40 +00:00
* @ see update_get_projects ()
*/
function _update_build_fetch_url ( $project , $site_key = '' ) {
$name = $project [ 'name' ];
2009-06-06 06:26:13 +00:00
$url = _update_get_fetch_url_base ( $project );
2008-04-14 17:48:46 +00:00
$url .= '/' . $name . '/' . DRUPAL_CORE_COMPATIBILITY ;
2012-05-01 03:15:27 +00:00
// Only append usage infomation if we have a site key and the project is
// enabled. We do not want to record usage statistics for disabled projects.
2009-06-05 01:04:11 +00:00
if ( ! empty ( $site_key ) && ( strpos ( $project [ 'project_type' ], 'disabled' ) === FALSE )) {
2012-05-01 03:15:27 +00:00
// Append the site key.
2012-03-30 16:02:46 +00:00
$url .= ( strpos ( $url , '?' ) !== FALSE ) ? '&' : '?' ;
2007-07-11 15:15:40 +00:00
$url .= 'site_key=' ;
2009-07-03 19:21:55 +00:00
$url .= rawurlencode ( $site_key );
2012-05-01 03:15:27 +00:00
// Append the version.
2007-07-11 15:15:40 +00:00
if ( ! empty ( $project [ 'info' ][ 'version' ])) {
$url .= '&version=' ;
2009-07-03 19:21:55 +00:00
$url .= rawurlencode ( $project [ 'info' ][ 'version' ]);
2007-07-11 15:15:40 +00:00
}
2012-05-01 03:15:27 +00:00
// Append the list of modules or themes enabled.
$list = array_keys ( $project [ 'includes' ]);
$url .= '&list=' ;
$url .= rawurlencode ( implode ( ',' , $list ));
2007-07-11 15:15:40 +00:00
}
return $url ;
}
2009-06-06 06:26:13 +00:00
/**
2012-06-06 15:33:53 +00:00
* Returns the base of the URL to fetch available update data for a project .
2009-06-06 06:26:13 +00:00
*
* @ param $project
* The array of project information from update_get_projects () .
2012-06-06 15:33:53 +00:00
*
2009-06-06 06:26:13 +00:00
* @ return
* The base of the URL used for fetching available update data . This does
* not include the path elements to specify a particular project , version ,
* site_key , etc .
*
* @ see _update_build_fetch_url ()
*/
function _update_get_fetch_url_base ( $project ) {
2012-08-03 16:52:07 +00:00
if ( isset ( $project [ 'info' ][ 'project status url' ])) {
$url = $project [ 'info' ][ 'project status url' ];
}
else {
$url = config ( 'update.settings' ) -> get ( 'fetch.url' );
if ( empty ( $url )) {
$url = UPDATE_DEFAULT_URL ;
}
}
return $url ;
2009-06-06 06:26:13 +00:00
}
2007-07-11 15:15:40 +00:00
/**
2012-06-06 15:33:53 +00:00
* Performs any notifications that should be done once cron fetches new data .
2007-07-11 15:15:40 +00:00
*
2012-06-06 15:33:53 +00:00
* This method checks the status of the site using the new data and , depending
* on the configuration of the site , notifies administrators via e - mail if there
2007-07-11 15:15:40 +00:00
* are new releases or missing security updates .
*
* @ see update_requirements ()
*/
function _update_cron_notify () {
2012-08-03 16:52:07 +00:00
$update_config = config ( 'update.settings' );
2009-12-29 07:21:34 +00:00
module_load_install ( 'update' );
2007-07-11 15:15:40 +00:00
$status = update_requirements ( 'runtime' );
$params = array ();
2012-08-03 16:52:07 +00:00
$notify_all = ( $update_config -> get ( 'notification.threshold' ) == 'all' );
2007-07-11 15:15:40 +00:00
foreach ( array ( 'core' , 'contrib' ) as $report_type ) {
2008-04-14 17:48:46 +00:00
$type = 'update_' . $report_type ;
2007-07-11 15:15:40 +00:00
if ( isset ( $status [ $type ][ 'severity' ])
2009-04-29 03:57:21 +00:00
&& ( $status [ $type ][ 'severity' ] == REQUIREMENT_ERROR || ( $notify_all && $status [ $type ][ 'reason' ] == UPDATE_NOT_CURRENT ))) {
2007-07-11 15:15:40 +00:00
$params [ $report_type ] = $status [ $type ][ 'reason' ];
}
}
if ( ! empty ( $params )) {
2012-08-03 16:52:07 +00:00
$notify_list = $update_config -> get ( 'notification.emails' );
2007-07-11 15:15:40 +00:00
if ( ! empty ( $notify_list )) {
2012-09-13 08:48:24 +00:00
$default_langcode = language_default () -> langcode ;
2007-07-11 15:15:40 +00:00
foreach ( $notify_list as $target ) {
2009-03-14 23:01:38 +00:00
if ( $target_user = user_load_by_mail ( $target )) {
2012-09-13 08:48:24 +00:00
$target_langcode = user_preferred_langcode ( $target_user );
2007-07-11 15:15:40 +00:00
}
else {
2012-09-13 08:48:24 +00:00
$target_langcode = $default_langcode ;
2007-07-11 15:15:40 +00:00
}
2012-09-13 08:48:24 +00:00
$message = drupal_mail ( 'update' , 'status_notify' , $target , $target_langcode , $params );
2012-06-14 04:58:22 +00:00
// Track when the last mail was successfully sent to avoid sending
// too many e-mails.
if ( $message [ 'result' ]) {
2012-10-27 21:15:15 +00:00
state () -> set ( 'update.last_email_notification' , REQUEST_TIME );
2012-06-14 04:58:22 +00:00
}
2007-07-11 15:15:40 +00:00
}
}
}
}
/**
2012-06-06 15:33:53 +00:00
* Parses the XML of the Drupal release history info files .
2008-10-24 19:23:59 +00:00
*
2009-10-13 02:14:05 +00:00
* @ param $raw_xml
* A raw XML string of available release data for a given project .
2008-10-24 19:23:59 +00:00
*
* @ return
2009-10-13 02:14:05 +00:00
* Array of parsed data about releases for a given project , or NULL if there
* was an error parsing the string .
2007-07-11 15:15:40 +00:00
*/
2009-10-13 02:14:05 +00:00
function update_parse_xml ( $raw_xml ) {
try {
$xml = new SimpleXMLElement ( $raw_xml );
}
catch ( Exception $e ) {
// SimpleXMLElement::__construct produces an E_WARNING error message for
// each error found in the XML data and throws an exception if errors
// were detected. Catch any exception and return failure (NULL).
return ;
}
2009-10-13 08:02:49 +00:00
// If there is no valid project data, the XML is invalid, so return failure.
if ( ! isset ( $xml -> short_name )) {
return ;
}
2010-05-06 05:59:31 +00:00
$short_name = ( string ) $xml -> short_name ;
2008-10-24 19:23:59 +00:00
$data = array ();
2009-10-13 02:14:05 +00:00
foreach ( $xml as $k => $v ) {
2010-05-06 05:59:31 +00:00
$data [ $k ] = ( string ) $v ;
2009-10-13 02:14:05 +00:00
}
$data [ 'releases' ] = array ();
2009-10-13 08:02:49 +00:00
if ( isset ( $xml -> releases )) {
foreach ( $xml -> releases -> children () as $release ) {
2010-05-06 05:59:31 +00:00
$version = ( string ) $release -> version ;
2009-10-13 08:02:49 +00:00
$data [ 'releases' ][ $version ] = array ();
foreach ( $release -> children () as $k => $v ) {
2010-05-06 05:59:31 +00:00
$data [ 'releases' ][ $version ][ $k ] = ( string ) $v ;
2009-10-13 08:02:49 +00:00
}
$data [ 'releases' ][ $version ][ 'terms' ] = array ();
if ( $release -> terms ) {
foreach ( $release -> terms -> children () as $term ) {
2010-05-06 05:59:31 +00:00
if ( ! isset ( $data [ 'releases' ][ $version ][ 'terms' ][( string ) $term -> name ])) {
$data [ 'releases' ][ $version ][ 'terms' ][( string ) $term -> name ] = array ();
2009-10-13 08:02:49 +00:00
}
2010-05-06 05:59:31 +00:00
$data [ 'releases' ][ $version ][ 'terms' ][( string ) $term -> name ][] = ( string ) $term -> value ;
2008-10-24 19:23:59 +00:00
}
2007-07-11 15:15:40 +00:00
}
}
}
2008-10-24 19:23:59 +00:00
return $data ;
2007-07-11 15:15:40 +00:00
}