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

Line #Times calledCode
1
<?php
2
// $Id: update.module,v 1.20 2008/07/24 16:25:19 dries 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 './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 './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/reports/updates/list'] = array(
1270
    'title' => 'List',
1280
    'page callback' => 'update_status',
1290
    'access arguments' => array('administer site configuration'),
1300
    'type' => MENU_DEFAULT_LOCAL_TASK,
131
  );
1320
  $items['admin/reports/updates/settings'] = array(
1330
    'title' => 'Settings',
1340
    'page callback' => 'drupal_get_form',
1350
    'page arguments' => array('update_settings'),
1360
    'access arguments' => array('administer site configuration'),
1370
    'type' => MENU_LOCAL_TASK,
138
  );
1390
  $items['admin/reports/updates/check'] = array(
1400
    'title' => 'Manual update check',
1410
    'page callback' => 'update_manual_status',
1420
    'access arguments' => array('administer site configuration'),
1430
    'type' => MENU_CALLBACK,
144
  );
145
1460
  return $items;
1470
}
148
149
/**
150
 * Implementation of the hook_theme() registry.
151
 */
152
function update_theme() {
153
  return array(
154
    'update_settings' => array(
1550
      'arguments' => array('form' => NULL),
1560
    ),
157
    'update_report' => array(
1580
      'arguments' => array('data' => NULL),
1590
    ),
160
    'update_version' => array(
1610
      'arguments' => array('version' => NULL, 'tag' => NULL, 'class' =>
NULL),
1620
    ),
1630
  );
1640
}
165
166
/**
167
 * Implementation of hook_requirements().
168
 *
169
 * @return
170
 *   An array describing the status of the site regarding available
updates.
171
 *   If there is no update data, only one record will be returned,
indicating
172
 *   that the status of core can't be determined. If data is available,
there
173
 *   will be two records: one for core, and another for all of contrib
174
 *   (assuming there are any contributed modules or themes enabled on the
175
 *   site). In addition to the fields expected by hook_requirements
('value',
176
 *   'severity', and optionally 'description'), this array will contain a
177
 *   'reason' attribute, which is an integer constant to indicate why the
178
 *   given status is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT,
or
179
 *   UPDATE_UNKNOWN). This is used for generating the appropriate e-mail
180
 *   notification messages during update_cron(), and might be useful for
other
181
 *   modules that invoke update_requirements() to find out if the site is
up
182
 *   to date or not.
183
 *
184
 * @see _update_message_text()
185
 * @see _update_cron_notify()
186
 */
187
function update_requirements($phase) {
1880
  if ($phase == 'runtime') {
1890
    if ($available = update_get_available(FALSE)) {
1900
      module_load_include('inc', 'update', 'update.compare');
1910
      $data = update_calculate_project_data($available);
192
      // First, populate the requirements for core:
1930
      $requirements['update_core'] =
_update_requirement_check($data['drupal'], 'core');
194
      // We don't want to check drupal a second time.
1950
      unset($data['drupal']);
1960
      if (!empty($data)) {
197
        // Now, sort our $data array based on each project's status. The
198
        // status constants are numbered in the right order of precedence,
so
199
        // we just need to make sure the projects are sorted in ascending
200
        // order of status, and we can look at the first project we find.
2010
        uasort($data, '_update_project_status_sort');
2020
        $first_project = reset($data);
2030
        $requirements['update_contrib'] =
_update_requirement_check($first_project, 'contrib');
2040
      }
2050
    }
206
    else {
2070
      $requirements['update_core']['title'] = t('Drupal core update
status');
2080
      $requirements['update_core']['value'] = t('No update data
available');
2090
      $requirements['update_core']['severity'] = REQUIREMENT_WARNING;
2100
      $requirements['update_core']['reason'] = UPDATE_UNKNOWN;
2110
      $requirements['update_core']['description'] = _update_no_data();
212
    }
2130
    return $requirements;
2140
  }
2150
}
216
217
/**
218
 * Private helper method to fill in the requirements array.
219
 *
220
 * This is shared for both core and contrib to generate the right elements
in
221
 * the array for hook_requirements().
222
 *
223
 * @param $project
224
 *  Array of information about the project we're testing as returned by
225
 *  update_calculate_project_data().
226
 * @param $type
227
 *  What kind of project is this ('core' or 'contrib').
228
 *
229
 * @return
230
 *  An array to be included in the nested $requirements array.
231
 *
232
 * @see hook_requirements()
233
 * @see update_requirements()
234
 * @see update_calculate_project_data()
235
 */
236
function _update_requirement_check($project, $type) {
2370
  $requirement = array();
2380
  if ($type == 'core') {
2390
    $requirement['title'] = t('Drupal core update status');
2400
  }
241
  else {
2420
    $requirement['title'] = t('Module and theme update status');
243
  }
2440
  $status = $project['status'];
2450
  if ($status != UPDATE_CURRENT) {
2460
    $requirement['reason'] = $status;
2470
    $requirement['description'] = _update_message_text($type, $status,
TRUE);
2480
    $requirement['severity'] = REQUIREMENT_ERROR;
2490
  }
250
  switch ($status) {
2510
    case UPDATE_NOT_SECURE:
2520
      $requirement_label = t('Not secure!');
2530
      break;
2540
    case UPDATE_REVOKED:
2550
      $requirement_label = t('Revoked!');
2560
      break;
2570
    case UPDATE_NOT_SUPPORTED:
2580
      $requirement_label = t('Unsupported release');
2590
      break;
2600
    case UPDATE_NOT_CURRENT:
2610
      $requirement_label = t('Out of date');
2620
      $requirement['severity'] =
variable_get('update_notification_threshold', 'all') == 'all' ?
REQUIREMENT_ERROR : REQUIREMENT_WARNING;
2630
      break;
2640
    case UPDATE_UNKNOWN:
2650
    case UPDATE_NOT_CHECKED:
2660
      $requirement_label = isset($project['reason']) ? $project['reason'] :
t('Can not determine status');
2670
      $requirement['severity'] = REQUIREMENT_WARNING;
2680
      break;
2690
    default:
2700
      $requirement_label = t('Up to date');
2710
  }
2720
  if ($status != UPDATE_CURRENT && $type == 'core' &&
isset($project['recommended'])) {
2730
    $requirement_label .= ' ' . t('(version @version available)',
array('@version' => $project['recommended']));
2740
  }
2750
  $requirement['value'] = l($requirement_label, 'admin/reports/updates');
2760
  return $requirement;
2770
}
278
279
/**
280
 * Implementation of hook_cron().
281
 */
282
function update_cron() {
2830
  $frequency = variable_get('update_check_frequency', 1);
2840
  $interval = 60 * 60 * 24 * $frequency;
2850
  if (time() - variable_get('update_last_check', 0) > $interval) {
2860
    update_refresh();
2870
    _update_cron_notify();
2880
  }
2890
}
290
291
/**
292
 * Implementation of hook_form_alter().
293
 *
294
 * Adds a submit handler to the system modules and themes forms, so that if
a
295
 * site admin saves either form, we invalidate the cache of available
updates.
296
 *
297
 * @see update_invalidate_cache()
298
 */
299
function update_form_alter(&$form, $form_state, $form_id) {
3000
  if ($form_id == 'system_modules' || $form_id == 'system_themes' ) {
3010
    $form['#submit'][] = 'update_invalidate_cache';
3020
  }
3030
}
304
305
/**
306
 * Prints a warning message when there is no data about available updates.
307
 */
308
function _update_no_data() {
3090
  $destination = drupal_get_destination();
3100
  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(
3110
    '@run_cron' => url('admin/reports/status/run-cron', array('query' =>
$destination)),
3120
    '@check_manually' => url('admin/reports/updates/check', array('query'
=> $destination)),
3130
  ));
3140
}
315
316
/**
317
 * Internal helper to try to get the update information from the cache
318
 * if possible, and to refresh the cache when necessary.
319
 *
320
 * In addition to checking the cache lifetime, this function also ensures
that
321
 * there are no .info files for enabled modules or themes that have a
newer
322
 * modification timestamp than the last time we checked for available
update
323
 * data. If any .info file was modified, it almost certainly means a new
324
 * version of something was installed. Without fresh available update
data,
325
 * the logic in update_calculate_project_data() will be wrong and produce
326
 * confusing, bogus results.
327
 *
328
 * @param $refresh
329
 *   Boolean to indicate if this method should refresh the cache
automatically
330
 *   if there's no data.
331
 *
332
 * @see update_refresh()
333
 * @see update_get_projects()
334
 */
335
function update_get_available($refresh = FALSE) {
3360
  module_load_include('inc', 'update', 'update.compare');
3370
  $available = array();
338
339
  // First, make sure that none of the .info files have a change time
340
  // newer than the last time we checked for available updates.
3410
  $needs_refresh = FALSE;
3420
  $last_check = variable_get('update_last_check', 0);
3430
  $projects = update_get_projects();
3440
  foreach ($projects as $key => $project) {
3450
    if ($project['info']['_info_file_ctime'] > $last_check) {
3460
      $needs_refresh = TRUE;
3470
      break;
3480
    }
3490
  }
3500
  if (!$needs_refresh && ($cache = cache_get('update_info',
'cache_update'))
3510
       && $cache->expire > time()) {
3520
    $available = $cache->data;
3530
  }
3540
  elseif ($needs_refresh || $refresh) {
355
    // If we need to refresh due to a newer .info file, ignore the
argument
356
    // and force the refresh (e.g., even for update_requirements()) to
prevent
357
    // bogus results.
3580
    $available = update_refresh();
3590
  }
3600
  return $available;
3610
}
362
363
/**
364
 * Implementation of hook_flush_caches().
365
 *
366
 * The function update.php (among others) calls this hook to flush the
caches.
367
 * Since we're running update.php, we are likely to install a new version
of
368
 * something, in which case, we want to check for available update data
again.
369
 */
370
function update_flush_caches() {
3710
  return array('cache_update');
3720
}
373
374
/**
375
 * Invalidates any cached data relating to update status.
376
 */
377
function update_invalidate_cache() {
3780
  cache_clear_all('*', 'cache_update', TRUE);
3790
}
380
381
/**
382
 * Wrapper to load the include file and then refresh the release data.
383
 */
384
function update_refresh() {
3850
  module_load_include('inc', 'update', 'update.fetch');
3860
  return _update_refresh();
3870
}
388
389
/**
390
 * Implementation of hook_mail().
391
 *
392
 * Constructs the email notification message when the site is out of date.
393
 *
394
 * @param $key
395
 *   Unique key to indicate what message to build, always 'status_notify'.
396
 * @param $message
397
 *   Reference to the message array being built.
398
 * @param $params
399
 *   Array of parameters to indicate what kind of text to include in the
400
 *   message body. This is a keyed array of message type ('core' or
'contrib')
401
 *   as the keys, and the status reason constant (UPDATE_NOT_SECURE, etc)
for
402
 *   the values.
403
 *
404
 * @see drupal_mail()
405
 * @see _update_cron_notify()
406
 * @see _update_message_text()
407
 */
408
function update_mail($key, &$message, $params) {
4090
  $language = $message['language'];
4100
  $langcode = $language->language;
4110
  $message['subject'] .= t('New release(s) available for !site_name',
array('!site_name' => variable_get('site_name', 'Drupal')), $langcode);
4120
  foreach ($params as $msg_type => $msg_reason) {
4130
    $message['body'][] = _update_message_text($msg_type, $msg_reason,
FALSE, $language);
4140
  }
4150
  $message['body'][] = t('See the available updates page for more
information:', array(), $langcode) . "\n" . url('admin/reports/updates',
array('absolute' => TRUE, 'language' => $language));
4160
}
417
418
/**
419
 * Helper function to return the appropriate message text when the site is
out
420
 * of date or missing a security update.
421
 *
422
 * These error messages are shared by both update_requirements() for the
423
 * site-wide status report at admin/reports/status and in the body of the
424
 * notification emails generated by update_cron().
425
 *
426
 * @param $msg_type
427
 *   String to indicate what kind of message to generate. Can be either
428
 *   'core' or 'contrib'.
429
 * @param $msg_reason
430
 *   Integer constant specifying why message is generated.
431
 * @param $report_link
432
 *   Boolean that controls if a link to the updates report should be
added.
433
 * @param $language
434
 *   An optional language object to use.
435
 * @return
436
 *   The properly translated error message for the given key.
437
 */
438
function _update_message_text($msg_type, $msg_reason, $report_link = FALSE,
$language = NULL) {
4390
  $langcode = isset($language) ? $language->language : NULL;
4400
  $text = '';
441
  switch ($msg_reason) {
4420
    case UPDATE_NOT_SECURE:
4430
      if ($msg_type == 'core') {
4440
        $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);
4450
      }
446
      else {
4470
        $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);
448
      }
4490
      break;
450
4510
    case UPDATE_REVOKED:
4520
      if ($msg_type == 'core') {
4530
        $text = t('Your version of Drupal has been revoked and is no longer
available for download. Upgrading is strongly recommended!', array(),
$langcode);
4540
      }
455
      else {
4560
        $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);
457
      }
4580
      break;
459
4600
    case UPDATE_NOT_SUPPORTED:
4610
      if ($msg_type == 'core') {
4620
        $text = t('Your version of Drupal is no longer supported. Upgrading
is strongly recommended!', array(), $langcode);
4630
      }
464
      else {
4650
        $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);
466
      }
4670
      break;
468
4690
    case UPDATE_NOT_CURRENT:
4700
      if ($msg_type == 'core') {
4710
        $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);
4720
      }
473
      else {
4740
        $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);
475
      }
4760
      break;
477
4780
    case UPDATE_UNKNOWN:
4790
    case UPDATE_NOT_CHECKED:
4800
      if ($msg_type == 'core') {
4810
        $text = t('There was a problem determining the status of available
updates for your version of Drupal.', array(), $langcode);
4820
      }
483
      else {
4840
        $text = t('There was a problem determining the status of available
updates for one or more of your modules or themes.', array(), $langcode);
485
      }
4860
      break;
4870
  }
488
4890
  if ($report_link) {
4900
    $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);
4910
  }
492
4930
  return $text;
4940
}
495
496
/**
497
 * Private sort function to order projects based on their status.
498
 *
499
 * @see update_requirements()
500
 * @see uasort()
501
 */
502
function _update_project_status_sort($a, $b) {
503
  // The status constants are numerically in the right order, so we can
504
  // usually subtract the two to compare in the order we want. However,
505
  // negative status values should be treated as if they are huge, since
we
506
  // always want them at the bottom of the list.
5070
  $a_status = $a['status'] > 0 ? $a['status'] : (-10 * $a['status']);
5080
  $b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
5090
  return $a_status - $b_status;
5100
}
511