Code coverage for /20081101/modules/update/update.module

Line #Times calledCode
1
<?php
2
// $Id: update.module,v 1.26 2008/09/23 19:07:58 webchick Exp $
3
4
/**
5
 * @file
6
 * The "Update status" module checks for available updates of Drupal core
and
7
 * any installed contributed modules and themes. It warns site
administrators
8
 * if newer releases are available via the system status report
9
 * (admin/reports/status), the module and theme pages, and optionally via
email.
10
 */
11
12
/**
13
 * URL to check for updates, if a given project doesn't define its own.
14
 */
15
define('UPDATE_DEFAULT_URL', 'http://updates.drupal.org/release-history');
16
17
// These are internally used constants for this code, do not modify.
18
19
/**
20
 * Project is missing security update(s).
21
 */
22
define('UPDATE_NOT_SECURE', 1);
23
24
/**
25
 * Current release has been unpublished and is no longer available.
26
 */
27
define('UPDATE_REVOKED', 2);
28
29
/**
30
 * Current release is no longer supported by the project maintainer.
31
 */
32
define('UPDATE_NOT_SUPPORTED', 3);
33
34
/**
35
 * Project has a new release available, but it is not a security release.
36
 */
37
define('UPDATE_NOT_CURRENT', 4);
38
39
/**
40
 * Project is up to date.
41
 */
42
define('UPDATE_CURRENT', 5);
43
44
/**
45
 * Project's status cannot be checked.
46
 */
47
define('UPDATE_NOT_CHECKED', -1);
48
49
/**
50
 * No available update data was found for project.
51
 */
52
define('UPDATE_UNKNOWN', -2);
53
54
55
/**
56
 * Implementation of hook_help().
57
 */
58
function update_help($path, $arg) {
59
  switch ($path) {
600
    case 'admin/reports/updates':
610
      $output = '<p>' . t('Here you can find information about available
updates for your installed modules and themes. Note that each module or
theme is part of a "project", which may or may not have the same name, and
might include multiple modules or themes within it.') . '</p>';
620
      $output .= '<p>' . t('To extend the functionality or to change the
look of your site, a number of contributed <a href="@modules">modules</a>
and <a href="@themes">themes</a> are available.', array('@modules' =>
'http://drupal.org/project/modules', '@themes' =>
'http://drupal.org/project/themes')) . '</p>';
630
      return $output;
640
    case 'admin/build/themes':
650
    case 'admin/build/modules':
660
      include_once DRUPAL_ROOT . '/includes/install.inc';
670
      $status = update_requirements('runtime');
680
      foreach (array('core', 'contrib') as $report_type) {
690
        $type = 'update_' . $report_type;
700
        if (isset($status[$type]['severity'])) {
710
          if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
720
            drupal_set_message($status[$type]['description'], 'error');
730
          }
740
          elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
750
            drupal_set_message($status[$type]['description'], 'warning');
760
          }
770
        }
780
      }
790
      return '<p>' . t('See the <a href="@available_updates">available
updates</a> page for information on installed modules and themes with new
versions released.', array('@available_updates' =>
url('admin/reports/updates'))) . '</p>';
80
810
    case 'admin/reports/updates/settings':
820
    case 'admin/reports/status':
83
      // These two pages don't need additional nagging.
840
      break;
85
860
    case 'admin/help#update':
870
      $output = '<p>' . t("The Update status module periodically checks for
new versions of your site's software (including contributed modules and
themes), and alerts you to available updates.") . '</p>';
880
      $output .= '<p>' . t('The <a href="@update-report">report of
available updates</a> will alert you when new releases are available for
download. You may configure options for update checking frequency and
notifications at the <a href="@update-settings">Update status module
settings page</a>.', array('@update-report' =>
url('admin/reports/updates'), '@update-settings' =>
url('admin/reports/updates/settings'))) . '</p>';
890
      $output .= '<p>' . t('Please note that in order to provide this
information, anonymous usage statistics are sent to drupal.org. If desired,
you may disable the Update status module from the <a href="@modules">module
administration page</a>.', array('@modules' => url('admin/build/modules')))
. '</p>';
900
      $output .= '<p>' . t('For more information, see the online handbook
entry for <a href="@update">Update status module</a>.', array('@update' =>
'http://drupal.org/handbook/modules/update')) . '</p>';
910
      return $output;
92
930
    default:
94
      // Otherwise, if we're on *any* admin page and there's a security
95
      // update missing, print an error message about it.
960
      if (arg(0) == 'admin' && strpos($path, '#') === FALSE
970
          && user_access('administer site configuration')) {
980
        include_once DRUPAL_ROOT . '/includes/install.inc';
990
        $status = update_requirements('runtime');
1000
        foreach (array('core', 'contrib') as $report_type) {
1010
          $type = 'update_' . $report_type;
1020
          if (isset($status[$type])
1030
              && isset($status[$type]['reason'])
1040
              && $status[$type]['reason'] === UPDATE_NOT_SECURE) {
1050
            drupal_set_message($status[$type]['description'], 'error');
1060
          }
1070
        }
1080
      }
109
1100
  }
1110
}
112
113
/**
114
 * Implementation of hook_menu().
115
 */
116
function update_menu() {
1170
  $items = array();
118
1190
  $items['admin/reports/updates'] = array(
1200
    'title' => 'Available updates',
1210
    'description' => 'Get a status report about available updates for your
installed modules and themes.',
1220
    'page callback' => 'update_status',
1230
    'access arguments' => array('administer site configuration'),
1240
    'weight' => 10,
125
  );
1260
  $items['admin/settings/updates'] = array(
1270
    'title' => 'Updates',
1280
    'description' => 'Change frequency of checks for available updates to
your installed modules and themes, and how you would like to be notified.',
1290
    'page callback' => 'drupal_get_form',
1300
    'page arguments' => array('update_settings'),
1310
    'access arguments' => array('administer site configuration'),
132
  );
1330
  $items['admin/reports/updates/check'] = array(
1340
    'title' => 'Manual update check',
1350
    'page callback' => 'update_manual_status',
1360
    'access arguments' => array('administer site configuration'),
1370
    'type' => MENU_CALLBACK,
138
  );
139
1400
  return $items;
1410
}
142
143
/**
144
 * Implementation of the hook_theme() registry.
145
 */
146
function update_theme() {
147
  return array(
148
    'update_settings' => array(
1490
      'arguments' => array('form' => NULL),
1500
    ),
151
    'update_report' => array(
1520
      'arguments' => array('data' => NULL),
1530
    ),
154
    'update_version' => array(
1550
      'arguments' => array('version' => NULL, 'tag' => NULL, 'class' =>
NULL),
1560
    ),
1570
  );
1580
}
159
160
/**
161
 * Implementation of hook_requirements().
162
 *
163
 * @return
164
 *   An array describing the status of the site regarding available
updates.
165
 *   If there is no update data, only one record will be returned,
indicating
166
 *   that the status of core can't be determined. If data is available,
there
167
 *   will be two records: one for core, and another for all of contrib
168
 *   (assuming there are any contributed modules or themes enabled on the
169
 *   site). In addition to the fields expected by hook_requirements
('value',
170
 *   'severity', and optionally 'description'), this array will contain a
171
 *   'reason' attribute, which is an integer constant to indicate why the
172
 *   given status is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT,
or
173
 *   UPDATE_UNKNOWN). This is used for generating the appropriate e-mail
174
 *   notification messages during update_cron(), and might be useful for
other
175
 *   modules that invoke update_requirements() to find out if the site is
up
176
 *   to date or not.
177
 *
178
 * @see _update_message_text()
179
 * @see _update_cron_notify()
180
 */
181
function update_requirements($phase) {
1820
  if ($phase == 'runtime') {
1830
    if ($available = update_get_available(FALSE)) {
1840
      module_load_include('inc', 'update', 'update.compare');
1850
      $data = update_calculate_project_data($available);
186
      // First, populate the requirements for core:
1870
      $requirements['update_core'] =
_update_requirement_check($data['drupal'], 'core');
188
      // We don't want to check drupal a second time.
1890
      unset($data['drupal']);
1900
      if (!empty($data)) {
191
        // Now, sort our $data array based on each project's status. The
192
        // status constants are numbered in the right order of precedence,
so
193
        // we just need to make sure the projects are sorted in ascending
194
        // order of status, and we can look at the first project we find.
1950
        uasort($data, '_update_project_status_sort');
1960
        $first_project = reset($data);
1970
        $requirements['update_contrib'] =
_update_requirement_check($first_project, 'contrib');
1980
      }
1990
    }
200
    else {
2010
      $requirements['update_core']['title'] = t('Drupal core update
status');
2020
      $requirements['update_core']['value'] = t('No update data
available');
2030
      $requirements['update_core']['severity'] = REQUIREMENT_WARNING;
2040
      $requirements['update_core']['reason'] = UPDATE_UNKNOWN;
2050
      $requirements['update_core']['description'] = _update_no_data();
206
    }
2070
    return $requirements;
2080
  }
2090
}
210
211
/**
212
 * Private helper method to fill in the requirements array.
213
 *
214
 * This is shared for both core and contrib to generate the right elements
in
215
 * the array for hook_requirements().
216
 *
217
 * @param $project
218
 *  Array of information about the project we're testing as returned by
219
 *  update_calculate_project_data().
220
 * @param $type
221
 *  What kind of project is this ('core' or 'contrib').
222
 *
223
 * @return
224
 *  An array to be included in the nested $requirements array.
225
 *
226
 * @see hook_requirements()
227
 * @see update_requirements()
228
 * @see update_calculate_project_data()
229
 */
230
function _update_requirement_check($project, $type) {
2310
  $requirement = array();
2320
  if ($type == 'core') {
2330
    $requirement['title'] = t('Drupal core update status');
2340
  }
235
  else {
2360
    $requirement['title'] = t('Module and theme update status');
237
  }
2380
  $status = $project['status'];
2390
  if ($status != UPDATE_CURRENT) {
2400
    $requirement['reason'] = $status;
2410
    $requirement['description'] = _update_message_text($type, $status,
TRUE);
2420
    $requirement['severity'] = REQUIREMENT_ERROR;
2430
  }
244
  switch ($status) {
2450
    case UPDATE_NOT_SECURE:
2460
      $requirement_label = t('Not secure!');
2470
      break;
2480
    case UPDATE_REVOKED:
2490
      $requirement_label = t('Revoked!');
2500
      break;
2510
    case UPDATE_NOT_SUPPORTED:
2520
      $requirement_label = t('Unsupported release');
2530
      break;
2540
    case UPDATE_NOT_CURRENT:
2550
      $requirement_label = t('Out of date');
2560
      $requirement['severity'] =
variable_get('update_notification_threshold', 'all') == 'all' ?
REQUIREMENT_ERROR : REQUIREMENT_WARNING;
2570
      break;
2580
    case UPDATE_UNKNOWN:
2590
    case UPDATE_NOT_CHECKED:
2600
      $requirement_label = isset($project['reason']) ? $project['reason'] :
t('Can not determine status');
2610
      $requirement['severity'] = REQUIREMENT_WARNING;
2620
      break;
2630
    default:
2640
      $requirement_label = t('Up to date');
2650
  }
2660
  if ($status != UPDATE_CURRENT && $type == 'core' &&
isset($project['recommended'])) {
2670
    $requirement_label .= ' ' . t('(version @version available)',
array('@version' => $project['recommended']));
2680
  }
2690
  $requirement['value'] = l($requirement_label, 'admin/reports/updates');
2700
  return $requirement;
2710
}
272
273
/**
274
 * Implementation of hook_cron().
275
 */
276
function update_cron() {
2770
  $frequency = variable_get('update_check_frequency', 1);
2780
  $interval = 60 * 60 * 24 * $frequency;
279
  // Cron should check for updates if there is no update data cached or if
the configured
280
  // update interval has elapsed.
2810
  if (!cache_get('update_info', 'cache_update') || ((REQUEST_TIME -
variable_get('update_last_check', 0)) > $interval)) {
2820
    update_refresh();
2830
    _update_cron_notify();
2840
  }
2850
}
286
287
/**
288
 * Implementation of hook_form_alter().
289
 *
290
 * Adds a submit handler to the system modules and themes forms, so that if
a
291
 * site admin saves either form, we invalidate the cache of available
updates.
292
 *
293
 * @see update_invalidate_cache()
294
 */
295
function update_form_alter(&$form, $form_state, $form_id) {
2960
  if ($form_id == 'system_modules' || $form_id == 'system_themes' ) {
2970
    $form['#submit'][] = 'update_invalidate_cache';
2980
  }
2990
}
300
301
/**
302
 * Prints a warning message when there is no data about available updates.
303
 */
304
function _update_no_data() {
3050
  $destination = drupal_get_destination();
3060
  return t('No information is available about potential new releases for
currently installed modules and themes. To check for updates, you may need
to <a href="@run_cron">run cron</a> or you can <a
href="@check_manually">check manually</a>. Please note that checking for
available updates can take a long time, so please be patient.', array(
3070
    '@run_cron' => url('admin/reports/status/run-cron', array('query' =>
$destination)),
3080
    '@check_manually' => url('admin/reports/updates/check', array('query'
=> $destination)),
3090
  ));
3100
}
311
312
/**
313
 * Internal helper to try to get the update information from the cache
314
 * if possible, and to refresh the cache when necessary.
315
 *
316
 * In addition to checking the cache lifetime, this function also ensures
that
317
 * there are no .info files for enabled modules or themes that have a newer
318
 * modification timestamp than the last time we checked for available
update
319
 * data. If any .info file was modified, it almost certainly means a new
320
 * version of something was installed. Without fresh available update data,
321
 * the logic in update_calculate_project_data() will be wrong and produce
322
 * confusing, bogus results.
323
 *
324
 * @param $refresh
325
 *   Boolean to indicate if this method should refresh the cache
automatically
326
 *   if there's no data.
327
 *
328
 * @see update_refresh()
329
 * @see update_get_projects()
330
 */
331
function update_get_available($refresh = FALSE) {
3320
  module_load_include('inc', 'update', 'update.compare');
3330
  $available = array();
334
335
  // First, make sure that none of the .info files have a change time
336
  // newer than the last time we checked for available updates.
3370
  $needs_refresh = FALSE;
3380
  $last_check = variable_get('update_last_check', 0);
3390
  $projects = update_get_projects();
3400
  foreach ($projects as $key => $project) {
3410
    if ($project['info']['_info_file_ctime'] > $last_check) {
3420
      $needs_refresh = TRUE;
3430
      break;
3440
    }
3450
  }
3460
  if (!$needs_refresh && ($cache = cache_get('update_info',
'cache_update'))
3470
       && $cache->expire > REQUEST_TIME) {
3480
    $available = $cache->data;
3490
  }
3500
  elseif ($needs_refresh || $refresh) {
351
    // If we need to refresh due to a newer .info file, ignore the argument
352
    // and force the refresh (e.g., even for update_requirements()) to
prevent
353
    // bogus results.
3540
    $available = update_refresh();
3550
  }
3560
  return $available;
3570
}
358
359
/**
360
 * Implementation of hook_flush_caches().
361
 *
362
 * The function update.php (among others) calls this hook to flush the
caches.
363
 * Since we're running update.php, we are likely to install a new version
of
364
 * something, in which case, we want to check for available update data
again.
365
 */
366
function update_flush_caches() {
3670
  return array('cache_update');
3680
}
369
370
/**
371
 * Invalidates any cached data relating to update status.
372
 */
373
function update_invalidate_cache() {
3740
  cache_clear_all('*', 'cache_update', TRUE);
3750
}
376
377
/**
378
 * Wrapper to load the include file and then refresh the release data.
379
 */
380
function update_refresh() {
3810
  module_load_include('inc', 'update', 'update.fetch');
3820
  return _update_refresh();
3830
}
384
385
/**
386
 * Implementation of hook_mail().
387
 *
388
 * Constructs the email notification message when the site is out of date.
389
 *
390
 * @param $key
391
 *   Unique key to indicate what message to build, always 'status_notify'.
392
 * @param $message
393
 *   Reference to the message array being built.
394
 * @param $params
395
 *   Array of parameters to indicate what kind of text to include in the
396
 *   message body. This is a keyed array of message type ('core' or
'contrib')
397
 *   as the keys, and the status reason constant (UPDATE_NOT_SECURE, etc)
for
398
 *   the values.
399
 *
400
 * @see drupal_mail()
401
 * @see _update_cron_notify()
402
 * @see _update_message_text()
403
 */
404
function update_mail($key, &$message, $params) {
4050
  $language = $message['language'];
4060
  $langcode = $language->language;
4070
  $message['subject'] .= t('New release(s) available for !site_name',
array('!site_name' => variable_get('site_name', 'Drupal')), $langcode);
4080
  foreach ($params as $msg_type => $msg_reason) {
4090
    $message['body'][] = _update_message_text($msg_type, $msg_reason,
FALSE, $language);
4100
  }
4110
  $message['body'][] = t('See the available updates page for more
information:', array(), $langcode) . "\n" . url('admin/reports/updates',
array('absolute' => TRUE, 'language' => $language));
4120
}
413
414
/**
415
 * Helper function to return the appropriate message text when the site is
out
416
 * of date or missing a security update.
417
 *
418
 * These error messages are shared by both update_requirements() for the
419
 * site-wide status report at admin/reports/status and in the body of the
420
 * notification emails generated by update_cron().
421
 *
422
 * @param $msg_type
423
 *   String to indicate what kind of message to generate. Can be either
424
 *   'core' or 'contrib'.
425
 * @param $msg_reason
426
 *   Integer constant specifying why message is generated.
427
 * @param $report_link
428
 *   Boolean that controls if a link to the updates report should be added.
429
 * @param $language
430
 *   An optional language object to use.
431
 * @return
432
 *   The properly translated error message for the given key.
433
 */
434
function _update_message_text($msg_type, $msg_reason, $report_link = FALSE,
$language = NULL) {
4350
  $langcode = isset($language) ? $language->language : NULL;
4360
  $text = '';
437
  switch ($msg_reason) {
4380
    case UPDATE_NOT_SECURE:
4390
      if ($msg_type == 'core') {
4400
        $text = t('There is a security update available for your version of
Drupal. To ensure the security of your server, you should update
immediately!', array(), $langcode);
4410
      }
442
      else {
4430
        $text = t('There are security updates available for one or more of
your modules or themes. To ensure the security of your server, you should
update immediately!', array(), $langcode);
444
      }
4450
      break;
446
4470
    case UPDATE_REVOKED:
4480
      if ($msg_type == 'core') {
4490
        $text = t('Your version of Drupal has been revoked and is no longer
available for download. Upgrading is strongly recommended!', array(),
$langcode);
4500
      }
451
      else {
4520
        $text = t('The installed version of at least one of your modules or
themes has been revoked and is no longer available for download. Upgrading
or disabling is strongly recommended!', array(), $langcode);
453
      }
4540
      break;
455
4560
    case UPDATE_NOT_SUPPORTED:
4570
      if ($msg_type == 'core') {
4580
        $text = t('Your version of Drupal is no longer supported. Upgrading
is strongly recommended!', array(), $langcode);
4590
      }
460
      else {
4610
        $text = t('The installed version of at least one of your modules or
themes is no longer supported. Upgrading or disabling is strongly
recommended! Please see the project homepage for more details.', array(),
$langcode);
462
      }
4630
      break;
464
4650
    case UPDATE_NOT_CURRENT:
4660
      if ($msg_type == 'core') {
4670
        $text = t('There are updates available for your version of Drupal.
To ensure the proper functioning of your site, you should update as soon as
possible.', array(), $langcode);
4680
      }
469
      else {
4700
        $text = t('There are updates available for one or more of your
modules or themes. To ensure the proper functioning of your site, you
should update as soon as possible.', array(), $langcode);
471
      }
4720
      break;
473
4740
    case UPDATE_UNKNOWN:
4750
    case UPDATE_NOT_CHECKED:
4760
      if ($msg_type == 'core') {
4770
        $text = t('There was a problem determining the status of available
updates for your version of Drupal.', array(), $langcode);
4780
      }
479
      else {
4800
        $text = t('There was a problem determining the status of available
updates for one or more of your modules or themes.', array(), $langcode);
481
      }
4820
      break;
4830
  }
484
4850
  if ($report_link) {
4860
    $text .= ' ' . t('See the <a href="@available_updates">available
updates</a> page for more information.', array('@available_updates' =>
url('admin/reports/updates', array('language' => $language))), $langcode);
4870
  }
488
4890
  return $text;
4900
}
491
492
/**
493
 * Private sort function to order projects based on their status.
494
 *
495
 * @see update_requirements()
496
 * @see uasort()
497
 */
498
function _update_project_status_sort($a, $b) {
499
  // The status constants are numerically in the right order, so we can
500
  // usually subtract the two to compare in the order we want. However,
501
  // negative status values should be treated as if they are huge, since we
502
  // always want them at the bottom of the list.
5030
  $a_status = $a['status'] > 0 ? $a['status'] : (-10 * $a['status']);
5040
  $b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
5050
  return $a_status - $b_status;
5060
}
507