/[drupal]/contributions/modules/update_status/update_status.module
ViewVC logotype

Contents of /contributions/modules/update_status/update_status.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.91 - (show annotations) (download) (as text)
Wed Jan 30 21:40:51 2008 UTC (21 months, 4 weeks ago) by dww
Branch: MAIN
CVS Tags: HEAD
Changes since 1.90: +468 -128 lines
File MIME type: text/x-php
Merged DRUPAL-5--2 as tagged at DRUPAL-5--2-1 into HEAD.
1 <?php
2 // $Id: update_status.module,v 1.90 2007/10/31 02:17:03 dww Exp $
3
4 // Version of core that this module is currently at (and therefore,
5 // that sites running it must be at and will want to query for).
6 define('UPDATE_STATUS_CORE_VERSION', '5.x');
7
8 // URL to check updates at, if a given project doesn't define its own.
9 define('UPDATE_STATUS_DEFAULT_URL', 'http://updates.drupal.org/release-history');
10
11 // These are internally used constants for this code, do not modify.
12 /**
13 * Project is missing security update(s).
14 */
15 define('UPDATE_STATUS_NOT_SECURE', 1);
16
17 /**
18 * Current release has been unpublished and is no longer available.
19 */
20 define('UPDATE_STATUS_REVOKED', 2);
21
22 /**
23 * Current release is no longer supported by the project maintainer.
24 */
25 define('UPDATE_STATUS_NOT_SUPPORTED', 3);
26
27 /**
28 * Project has a new release available, but it is not a security release.
29 */
30 define('UPDATE_STATUS_NOT_CURRENT', 4);
31
32 /**
33 * Project is up to date.
34 */
35 define('UPDATE_STATUS_CURRENT', 5);
36
37 /**
38 * Project's status cannot be checked.
39 */
40 define('UPDATE_STATUS_NOT_CHECKED', -1);
41
42 /**
43 * No available update data was found for project.
44 */
45 define('UPDATE_STATUS_UNKNOWN', -2);
46
47
48 /**
49 * Implementation of hook_help().
50 */
51 function update_status_help($section) {
52 switch ($section) {
53 case 'admin/logs/updates':
54 return '<p>'. t('Here you can find information about available updates for your installed modules. Note that each module is part of a "project", which may have the same name as the module or may have a different name.') .'</p>';
55
56 case 'admin/logs/updates/settings':
57 return '<p>'. t('Here you can configure what kinds of available updates for your installed modules should be marked as an error on the <a href="@status_report">Status report</a> and the <a href="@modules_page">Modules</a> page, and other related settings.', array('@status_report' => url('admin/logs/status'), '@modules_page' => url('admin/build/modules'))) .'</p>';
58
59 case 'admin/build/modules':
60 include_once './includes/install.inc';
61 $status = update_status_requirements('runtime');
62 $types = array('update_status_core', 'update_status_contrib');
63 foreach ($types as $type) {
64 if (isset($status[$type]['severity'])) {
65 if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
66 drupal_set_message($status[$type]['description'], 'error');
67 }
68 elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
69 drupal_set_message($status[$type]['description']);
70 }
71 }
72 }
73 return '<p>'. t('See the <a href="@available_updates">available updates</a> page for information on installed modules with new versions released.', array('@available_updates' => url('admin/logs/updates'))) .'</p>';
74
75 case 'admin/logs/updates/settings':
76 case 'admin/logs/status':
77 // These two pages don't need additional nagging.
78 break;
79
80 default:
81 // Otherwise, if we're on *any* admin page and there's a security
82 // update missing, print an error message about it.
83 if (arg(0) == 'admin' && strpos($section, '#') === FALSE
84 && user_access('administer site configuration')) {
85 include_once './includes/install.inc';
86 $status = update_status_requirements('runtime');
87 foreach (array('core', 'contrib') as $report_type) {
88 $type = 'update_status_'. $report_type;
89 if (isset($status[$type])
90 && isset($status[$type]['reason'])
91 && $status[$type]['reason'] === UPDATE_STATUS_NOT_SECURE) {
92 drupal_set_message($status[$type]['description'], 'error');
93 }
94 }
95 }
96 }
97 }
98
99 /**
100 * Implementation of hook_menu().
101 */
102 function update_status_menu($may_cache) {
103 $items = array();
104 if ($may_cache) {
105 $admin_access = user_access('administer site configuration');
106 $items[] = array(
107 'path' => 'admin/logs/updates',
108 'title' => t('Available updates'),
109 'description' => t('Get a status report on installed modules and available updates.'),
110 'callback' => 'update_status_status',
111 'weight' => 10,
112 'access' => $admin_access,
113 );
114 $items[] = array(
115 'path' => 'admin/logs/updates/list',
116 'title' => t('List'),
117 'callback' => 'update_status_status',
118 'access' => $admin_access,
119 'type' => MENU_DEFAULT_LOCAL_TASK,
120 );
121 $items[] = array(
122 'path' => 'admin/logs/updates/settings',
123 'title' => t('Settings'),
124 'callback' => 'drupal_get_form',
125 'callback arguments' => array('update_status_settings'),
126 'access' => $admin_access,
127 'type' => MENU_LOCAL_TASK,
128 );
129 $items[] = array(
130 'path' => 'admin/logs/updates/check',
131 'title' => t('Manual update check'),
132 'callback' => 'update_status_manual_status',
133 'access' => $admin_access,
134 'type' => MENU_CALLBACK,
135 );
136 }
137 return $items;
138 }
139
140 /**
141 * Menu callback. Generate a page about the update status of projects.
142 */
143 function update_status_status() {
144 if ($available = update_status_get_available(TRUE)) {
145 $data = update_status_calculate_project_data($available);
146 return theme('update_status_report', $data);
147 }
148 else {
149 return theme('update_status_report', _update_status_no_data());
150 }
151 }
152
153 /**
154 * Menu callback. Show the settings for the update status module.
155 */
156 function update_status_settings() {
157 $form = array();
158
159 if ($available = update_status_get_available(TRUE)) {
160 $values = variable_get('update_status_settings', array());
161 $form['projects'] = array('#tree' => TRUE);
162
163 $data = update_status_calculate_project_data($available);
164 $form['data'] = array('#type' => 'value', '#value' => $data);
165 $form['avail'] = array('#type' => 'value', '#value' => $available);
166
167 $notify_emails = variable_get('update_status_notify_emails', array());
168 $form['notify_emails'] = array(
169 '#type' => 'textarea',
170 '#title' => t('E-mail addresses to notify when updates are available'),
171 '#rows' => 4,
172 '#default_value' => implode("\n", $notify_emails),
173 '#description' => t('Whenever your site checks for available updates and finds new releases, it can notify a list of users via e-mail. Put each address on a separate line. If blank, no e-mails will be sent.'),
174 );
175
176 $form['check_frequency'] = array(
177 '#type' => 'radios',
178 '#title' => t('Check for updates'),
179 '#default_value' => variable_get('update_status_check_frequency', 'daily'),
180 '#options' => array(
181 'daily' => t('Daily'),
182 'weekly' => t('Weekly'),
183 ),
184 '#description' => t('Select how frequently you want to automatically check for new releases of your currently installed modules.'),
185 );
186
187 $form['notification_threshold'] = array(
188 '#type' => 'radios',
189 '#title' => t('Notification threshold'),
190 '#default_value' => variable_get('update_status_notification_threshold', 'all'),
191 '#options' => array(
192 'all' => t('All newer versions'),
193 'security' => t('Only security updates'),
194 ),
195 '#description' => t('If there are updates available of Drupal core or any of your installed modules, your site will print an error message on the <a href="@status_report">status report</a> and the <a href="@modules_page">modules page</a>. You can choose to only see these error messages if a security update is available, or to be notified about any newer versions.', array('@status_report' => url('admin/logs/status'), '@modules_page' => url('admin/build/modules'))),
196 );
197
198 $form['project_help'] = array(
199 '#value' => t('These settings allow you to control if a certain project, or even a specific release of that project, should be ignored by the available updates report. For each project, you can select if it should always warn you about a newer release, never warn you (ignore the project completely), or ignore a specific available release you do not want to upgrade to. You can also specify a note explaining why you are ignoring a specific project or version, and that will be displayed on the available updates report.'),
200 );
201
202 foreach ($data as $key => $project) {
203 if (isset($available[$key])) {
204 if (!isset($values[$key])) {
205 $values[$key] = array(
206 'check' => 'always',
207 'notes' => '',
208 );
209 }
210
211 $options = array();
212 $options['always'] = t('Always');
213 if (isset($project['recommended'])) {
214 $options[$project['recommended']] = t('Ignore @version', array('@version' => $project['recommended']));
215 }
216 $options['never'] = t('Never');
217
218 $form['projects'][$key]['check'] = array(
219 '#type' => 'select',
220 '#options' => $options,
221 '#default_value' => $values[$key]['check'],
222 );
223 $form['projects'][$key]['notes'] = array(
224 '#type' => 'textfield',
225 '#size' => 50,
226 '#default_value' => $values[$key]['notes'],
227 );
228 }
229 }
230 $form['submit'] = array(
231 '#type' => 'submit',
232 '#value' => t('Submit changes'),
233 );
234 }
235 else {
236 $form['error'] = array(
237 '#value' => theme('update_status_report', _update_status_no_data())
238 );
239 }
240
241 drupal_add_css(drupal_get_path('module', 'update_status') .'/update_status.css');
242 return $form;
243 }
244
245 function theme_update_status_settings($form) {
246 if (isset($form['error'])) {
247 return drupal_render($form);
248 }
249
250 $output = '';
251 $output .= drupal_render($form['notify_emails']);
252 $output .= drupal_render($form['check_frequency']);
253 $output .= drupal_render($form['notification_threshold']);
254
255 $header = array(
256 array('data' => t('Project'), 'class' => 'update-status-project'),
257 array('data' => t('Warn if out of date'), 'class' => 'update-status-status'),
258 array('data' => t('Notes'), 'class' => 'update-status-notes'),
259 );
260
261 $data = $form['data']['#value'];
262 $available = $form['avail']['#value'];
263
264 $rows = array();
265 foreach ($data as $key => $project) {
266 if (isset($available[$key])) {
267 $row = array();
268 $row[] = array(
269 'class' => 'update-status-project',
270 'data' => check_plain($available[$key]['title']),
271 );
272 $row[] = array(
273 'class' => 'update-status-status',
274 'data' => drupal_render($form['projects'][$key]['check']),
275 );
276 $row[] = array(
277 'class' => 'update-status-notes',
278 'data' => drupal_render($form['projects'][$key]['notes']),
279 );
280 $rows[] = $row;
281 }
282 }
283 $output .= theme('table', $header, $rows, array('class' => 'update-status-settings'));
284 $output .= '<div class="form-item"><div class="description">';
285 $output .= drupal_render($form['project_help']);
286 $output .= '</div></div>';
287
288 $output .= drupal_render($form);
289 return $output;
290 }
291
292 /**
293 * Validates the update_status settings form.
294 *
295 * Ensures that the email addresses are valid and properly formatted.
296 */
297 function update_status_settings_validate($form_id, $form_values) {
298 if (!empty($form_values['notify_emails'])) {
299 $invalid = array();
300 foreach (explode("\n", trim($form_values['notify_emails'])) as $email) {
301 $email = trim($email);
302 if (!empty($email)) {
303 if (!valid_email_address($email)) {
304 $invalid[] = $email;
305 }
306 }
307 }
308 if (!empty($invalid)) {
309 if (count($invalid) == 1) {
310 form_set_error('notify_emails', t('%email is not a valid e-mail address.', array('%email' => reset($invalid))));
311 }
312 else {
313 form_set_error('notify_emails', t('%emails are not valid e-mail addresses.', array('%emails' => implode(', ', $invalid))));
314 }
315 }
316 }
317 }
318
319 function update_status_settings_submit($form_id, $form_values) {
320 variable_set('update_status_check_frequency', $form_values['check_frequency']);
321 variable_set('update_status_notification_threshold', $form_values['notification_threshold']);
322 if (empty($form_values['notify_emails'])) {
323 variable_del('update_status_notify_emails');
324 }
325 else {
326 $emails = array();
327 foreach (explode("\n", trim($form_values['notify_emails'])) as $email) {
328 $email = trim($email);
329 if (!empty($email)) {
330 $emails[] = $email;
331 }
332 }
333 variable_set('update_status_notify_emails', $emails);
334 }
335 variable_set('update_status_settings', $form_values['projects']);
336 drupal_set_message(t('Your changes have been saved.'));
337 }
338
339 /**
340 * Implementation of hook_requirements.
341 *
342 * @return
343 * An array describing the status of the site regarding available updates.
344 * If there is no update data, only one record will be returned, indicating
345 * that the status of core can't be determined. If data is available, there
346 * will be two records: one for core, and another for all of contrib In
347 * addition to the fields expected by hook_requirements ('value',
348 * 'severity', and optionally 'description'), this array will contain a
349 * 'reason' attribute, which is an integer constant to indicate why the
350 * given status is being returned (UPDATE_STATUS_NOT_SECURE,
351 * UPDATE_STATUS_NOT_CURRENT, or UPDATE_STATUS_UNKNOWN). This is used for
352 * generating the appropriate e-mail notification messages during
353 * update_status_cron(), and might be useful for other modules that invoke
354 * update_status_requirements() to find out if the site is up to date.
355 */
356 function update_status_requirements($phase) {
357 if ($phase == 'runtime') {
358 if ($available = update_status_get_available(FALSE)) {
359 $data = update_status_calculate_project_data($available);
360 // First, populate the requirements for core:
361 $requirements['update_status_core'] = _update_status_requirement_check($data['drupal'], 'core');
362 // We don't want to check drupal a second time.
363 unset($data['drupal']);
364 if (!empty($data)) {
365 // Now, sort our $data array based on each project's status. The
366 // status constants are numbered in the right order of precedence, so
367 // we just need to make sure the projects are sorted in ascending
368 // order of status, and we can look at the first project we find.
369 uasort($data, '_update_status_project_status_sort');
370 $first_project = reset($data);
371 $requirements['update_status_contrib'] = _update_status_requirement_check($first_project, 'contrib');
372 }
373 }
374 else {
375 $requirements['update_status_core']['title'] = t('Drupal core update status');
376 $requirements['update_status_core']['value'] = t('No update data available');
377 $requirements['update_status_core']['severity'] = REQUIREMENT_WARNING;
378 $requirements['update_status_core']['reason'] = UPDATE_STATUS_UNKNOWN;
379 $requirements['update_status_core']['description'] = _update_status_no_data();
380 }
381 return $requirements;
382 }
383 }
384
385 /**
386 * Private helper method to fill in the requirements array.
387 *
388 * This is shared for both core and contrib to generate the right elements in
389 * the array for hook_requirements().
390 *
391 * @param $project
392 * Array of information about the project we're testing as returned by
393 * update_calculate_project_data().
394 * @param $type
395 * What kind of project is this ('core' or 'contrib').
396 *
397 * @return
398 * An array to be included in the nested $requirements array.
399 *
400 * @see hook_requirements()
401 * @see update_status_requirements()
402 * @see update_status_calculate_project_data()
403 */
404 function _update_status_requirement_check($project, $type) {
405 $requirement = array();
406 if ($type == 'core') {
407 $requirement['title'] = t('Drupal core update status');
408 }
409 else {
410 $requirement['title'] = t('Module update status');
411 }
412 $status = $project['status'];
413 if ($status != UPDATE_STATUS_CURRENT) {
414 $requirement['reason'] = $status;
415 $requirement['description'] = _update_status_message_text($type, $status, TRUE);
416 $requirement['severity'] = REQUIREMENT_ERROR;
417 }
418 switch ($status) {
419 case UPDATE_STATUS_NOT_SECURE:
420 $requirement_label = t('Not secure!');
421 break;
422 case UPDATE_STATUS_REVOKED:
423 $requirement_label = t('Revoked!');
424 break;
425 case UPDATE_STATUS_NOT_SUPPORTED:
426 $requirement_label = t('Unsupported release');
427 break;
428 case UPDATE_STATUS_NOT_CURRENT:
429 $requirement_label = t('Out of date');
430 $requirement['severity'] = variable_get('update_notification_threshold', 'all') == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
431 break;
432 case UPDATE_STATUS_UNKNOWN:
433 case UPDATE_STATUS_NOT_CHECKED:
434 $requirement_label = isset($project['reason']) ? $project['reason'] : t('Can not determine status');
435 $requirement['severity'] = REQUIREMENT_WARNING;
436 break;
437 default:
438 $requirement_label = t('Up to date');
439 }
440 if ($status != UPDATE_STATUS_CURRENT && $type == 'core' && isset($project['recommended'])) {
441 $requirement_label .= ' '. t('(version @version available)', array('@version' => $project['recommended']));
442 }
443 $requirement['value'] = l($requirement_label, 'admin/logs/updates');
444 return $requirement;
445 }
446
447 /**
448 * Implementation of hook_cron().
449 */
450 function update_status_cron() {
451 $frequency = variable_get('update_status_check_frequency', 'daily');
452 $interval = 60 * 60 * 24 * ($frequency == 'weekly' ? 7 : 1);
453 if (time() - variable_get('update_status_last', 0) > $interval) {
454 update_status_refresh();
455 _update_status_cron_notify();
456 }
457 }
458
459 /**
460 * Perform any notifications that should be done once cron fetches new data.
461 *
462 * This method checks the status of the site using the new data and depending
463 * on the configuration of the site, notifys administrators via email if there
464 * are new releases or missing security updates.
465 *
466 * @see update_status_requirements()
467 */
468 function _update_status_cron_notify() {
469 $status = update_status_requirements('runtime');
470 $body = array();
471 $types = array('core', 'contrib');
472 foreach ($types as $report_type) {
473 $type = 'update_status_'. $report_type;
474 if (isset($status[$type]['severity'])
475 && $status[$type]['severity'] == REQUIREMENT_ERROR) {
476 $body[] = wordwrap(_update_status_message_text($report_type, $status[$type]['reason'], FALSE));
477 }
478 }
479 if (!empty($body)) {
480 $notify_list = variable_get('update_status_notify_emails', '');
481 if (!empty($notify_list)) {
482 $body[] = t('See the available updates page for more information:') ."\n". url('admin/logs/updates', NULL, NULL, TRUE) ."\n\n";
483 $subject = t('New release(s) available for !site_name', array('!site_name' => variable_get('site_name', 'Drupal')));
484 $body_text = implode("\n\n", $body);
485 foreach ($notify_list as $target) {
486 drupal_mail('update-status', $target, $subject, $body_text);
487 }
488 }
489 }
490 }
491
492 /**
493 * Implementation of hook_form_alter().
494 *
495 * Adds a submit handler to the system modules and themes forms, so that if a
496 * site admin saves either form, we invalidate the cache of available updates.
497 *
498 * @see update_status_invalidate_cache()
499 */
500 function update_status_form_alter($form_id, &$form) {
501 if ($form_id == 'system_modules') {
502 $form['#submit']['update_status_system_submit'] = array();
503 }
504 }
505
506 /**
507 * Submit handler for system modules pages.
508 */
509 function update_status_system_submit($form_id, $form_values) {
510 update_status_invalidate_cache();
511 }
512
513 /**
514 * Helper function to return the appropriate message text when the site is out
515 * of date or missing a security update.
516 *
517 * These error messages are shared by both update_requirements() for the
518 * site-wide status report at admin/logs/status and in the body of the
519 * notification emails generated by update_cron().
520 *
521 * @param $msg_type
522 * String to indicate what kind of message to generate. Can be either
523 * 'core' or 'contrib'.
524 * @param $msg_reason
525 * Integer constant specifying why message is generated. Can be any of the
526 * UPDATE_STATUS_* constants from the top of this file.
527 * @param $report_link
528 * Boolean that controls if a link to the updates report should be added.
529 * @return
530 * The properly translated error message for the given key.
531 */
532 function _update_status_message_text($msg_type, $msg_reason, $report_link = FALSE) {
533 $text = '';
534 switch ($msg_reason) {
535 case UPDATE_STATUS_NOT_SECURE:
536 if ($msg_type == 'core') {
537 $text = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!');
538 }
539 else {
540 $text = t('There are security updates available for one or more of your modules. To ensure the security of your server, you should update immediately!');
541 }
542 break;
543
544 case UPDATE_STATUS_REVOKED:
545 if ($msg_type == 'core') {
546 $text = t('Your version of Drupal has been revoked and is no longer available for download. Upgrading is strongly recommended!');
547 }
548 else {
549 $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!');
550 }
551 break;
552
553 case UPDATE_STATUS_NOT_SUPPORTED:
554 if ($msg_type == 'core') {
555 $text = t('Your version of Drupal is no longer supported. Upgrading is strongly recommended!');
556 }
557 else {
558 $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.');
559 }
560 break;
561
562 case UPDATE_STATUS_NOT_CURRENT:
563 if ($msg_type == 'core') {
564 $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.');
565 }
566 else {
567 $text = t('There are updates available for one or more of your modules. To ensure the proper functioning of your site, you should update as soon as possible.');
568 }
569 break;
570
571 case UPDATE_STATUS_UNKNOWN:
572 case UPDATE_STATUS_NOT_CHECKED:
573 if ($msg_type == 'core') {
574 $text = t('There was a problem determining the status of available updates for your version of Drupal.');
575 }
576 else {
577 $text = t('There was a problem determining the status of available updates for one or more of your modules or themes.');
578 }
579 break;
580 }
581
582 if ($report_link) {
583 $text .= ' '. t('See the <a href="@available_updates">available updates</a> page for more information.', array('@available_updates' => url('admin/logs/updates')));
584 }
585
586 return $text;
587 }
588
589 /**
590 * Callback to manually check the update status without cron.
591 */
592 function update_status_manual_status() {
593 if (update_status_refresh()) {
594 drupal_set_message(t('Fetched information about all available new releases and updates.'));
595 }
596 else {
597 drupal_set_message(t('Unable to fetch any information on available new releases and updates.'), 'error');
598 }
599 drupal_goto('admin/logs/updates');
600 }
601
602 /**
603 * Prints a warning message when there is no data about available updates.
604 */
605 function _update_status_no_data() {
606 $destination = drupal_get_destination();
607 return t('No information is available about potential new releases for currently installed modules. 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(
608 '@run_cron' => url('admin/logs/status/run-cron', $destination),
609 '@check_manually' => url('admin/logs/updates/check', $destination),
610 ));
611 }
612
613 /**
614 * Fetch an array of installed and enabled projects.
615 *
616 * This is only responsible for generating an array of projects (taking into
617 * account projects that include more than one module). Other information
618 * like the specific version and install type (official release, dev snapshot,
619 * etc) is handled later in update_status_process_project_info() since that
620 * logic is only required when preparing the status report, not for fetching
621 * the available release data.
622 *
623 * @see update_status_process_project_info()
624 * @see update_status_calculate_project_data()
625 *
626 * @todo
627 * Extend this to include themes and theme engines when they get .info files.
628 */
629 function update_status_get_projects() {
630 static $projects = array();
631 if (!empty($projects)) {
632 return $projects;
633 }
634 // Retrieve the projects from cache, if present.
635 $projects = update_status_project_cache('update_status_projects');
636 if (!empty($projects)) {
637 return $projects;
638 }
639
640 // Still empty, will have to compute this and save it to the DB cache.
641
642 // Get current list of modules.
643 $files = drupal_system_listing('\.module$', 'modules', 'name', 0);
644
645 // Extract current files from database.
646 system_get_files_database($files, 'module');
647
648 foreach ($files as $filename => $file) {
649 // Skip not enabled modules.
650 if (empty($file->status)) {
651 continue;
652 }
653 $info_filename = dirname($file->filename) .'/'. $file->name .'.info';
654 $file->info = _module_parse_info_file($info_filename);
655
656 // Skip if this is broken.
657 if (empty($file->info)) {
658 continue;
659 }
660
661 // Record the change time on the .info file itself. Note: we need to use
662 // the ctime, not the mtime (modification time) since many (all?) tar
663 // implementations will go out of their way to set the mtime on the files
664 // it creates to the timestamps recorded in the tarball. We want to see
665 // the last time the file was changed on disk, which is left alone by tar
666 // and correctly set to the time the .info file was unpacked.
667 $file->info['_info_file_ctime'] = filectime($info_filename);
668
669 $info = $file->info;
670 $info['check'] = TRUE;
671
672 if (!isset($info['project'])) {
673 $info['project'] = update_status_get_project($file);
674 }
675
676 // Give other modules a chance to fill-in and clean the version,
677 // datestamp, or any other data from the .info file they need to alter.
678 // We can't use module_invoke_all() since we pass a reference to the hook
679 // so it can modify the info array.
680 $project = array();
681 $project['name'] = $file->name;
682 $project['project'] = $info['project'];
683 $project['filename'] = $file->filename;
684 foreach (module_implements('system_info_alter') as $module) {
685 $function = $module .'_system_info_alter';
686 $function($info, $project);
687 }
688
689 // If we still don't know the 'project', give up.
690 if (empty($info['project'])) {
691 continue;
692 }
693
694 if (!isset($projects[$info['project']])) {
695 // Only process this if we haven't done this project, since a single
696 // project can have multiple modules.
697 $projects[$info['project']] = array(
698 'name' => $info['project'],
699 'info' => $info,
700 'datestamp' => isset($info['datestamp']) ? $info['datestamp'] : 0,
701 'modules' => array($file->name => $info['name']),
702 );
703 }
704 else {
705 $projects[$info['project']]['modules'][$file->name] = $info['name'];
706 }
707 }
708 asort($projects);
709 cache_set('update_status_projects', 'cache', serialize($projects), time() + (60 * 60));
710 return $projects;
711 }
712
713 /**
714 * Given a $module object (as returned by system_get_files_database()), figure
715 * out what project that module belongs to.
716 *
717 * @see system_get_files_database()
718 */
719 function update_status_get_project($module) {
720 $project = '';
721 if (isset($module->info['project'])) {
722 $project = $module->info['project'];
723 }
724 elseif (isset($module->info['package'])
725 && (strpos($module->info['package'], 'Core -') !== FALSE)) {
726 $project = 'drupal';
727 }
728 return $project;
729 }
730
731 /**
732 * Process the list of projects on the system to figure out the currently
733 * installed versions, and other information that is required before we can
734 * compare against the available releases to produce the status report.
735 *
736 * @param $projects
737 * Array of project information from update_status_get_projects().
738 */
739 function update_status_process_project_info(&$projects) {
740 foreach ($projects as $key => $project) {
741 // Assume an official release until we see otherwise.
742 $type = 'official';
743
744 $info = $project['info'];
745
746 if (isset($info['version'])) {
747 // Check for development snapshots
748 if (preg_match('@(dev|HEAD)@', $info['version'])) {
749 $type = 'dev';
750 }
751
752 // Figure out what the currently installed major version is. We need
753 // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
754 // (e.g. "5.1", major = 5) version strings.
755 $matches = array();
756 if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
757 $info['major'] = $matches[2];
758 }
759 elseif (!isset($info['major'])) {
760 // This would only happen for version strings that don't follow the
761 // drupal.org convention. We let contribs define "major" in their
762 // .info in this case, and only if that's missing would we hit this.
763 $info['major'] = -1;
764 }
765 }
766 else {
767 // No version info available at all.
768 $type = 'unknown';
769 $info['version'] = t('Unknown');
770 $info['major'] = -1;
771 }
772
773 // Finally, save the results we care about into the $projects array.
774 $projects[$key]['existing_version'] = $info['version'];
775 $projects[$key]['existing_major'] = $info['major'];
776 $projects[$key]['type'] = $type;
777 unset($projects[$key]['info']);
778 }
779 }
780
781 /**
782 * Given the installed projects and the available release data retrieved from
783 * remote servers, calculate the current status.
784 *
785 * This function is the heart of the update status feature. It iterates over
786 * every currently installed project. For each one, it first checks if the
787 * project has been flagged with a special status like "unsupported" or
788 * "insecure", or if the project node itself has been unpublished. In any of
789 * those cases, the project is marked with an error and the next project is
790 * considered.
791 *
792 * If the project itself is valid, the function decides what major release
793 * series to consider. The project defines what the currently supported major
794 * versions are for each version of core, so the first step is to make sure
795 * the current version is still supported. If so, that's the target version.
796 * If the current version is unsupported, the project maintainer's recommended
797 * major version is used. There's also a check to make sure that this function
798 * never recommends an earlier release than the currently installed major
799 * version.
800 *
801 * Given a target major version, it scans the available releases looking for
802 * the specific release to recommend (avoiding beta releases and development
803 * snapshots if possible). This is complicated to describe, but an example
804 * will help clarify. For the target major version, find the highest patch
805 * level. If there is a release at that patch level with no extra ("beta",
806 * etc), then we recommend the release at that patch level with the most
807 * recent release date. If every release at that patch level has extra (only
808 * betas), then recommend the latest release from the previous patch
809 * level. For example:
810 *
811 * 1.6-bugfix <-- recommended version because 1.6 already exists.
812 * 1.6
813 *
814 * or
815 *
816 * 1.6-beta
817 * 1.5 <-- recommended version because no 1.6 exists.
818 * 1.4
819 *
820 * It also looks for the latest release from the same major version, even a
821 * beta release, to display to the user as the "Latest version" option.
822 * Additionally, it finds the latest official release from any higher major
823 * versions that have been released to provide a set of "Also available"
824 * options.
825 *
826 * Finally, and most importantly, it keeps scanning the release history until
827 * it gets to the currently installed release, searching for anything marked
828 * as a security update. If any security updates have been found between the
829 * recommended release and the installed version, all of the releases that
830 * included a security fix are recorded so that the site administrator can be
831 * warned their site is insecure, and links pointing to the release notes for
832 * each security update can be included (which, in turn, will link to the
833 * official security announcements for each vulnerability).
834 *
835 * This function relies on the fact that the .xml release history data comes
836 * sorted based on major version and patch level, then finally by release date
837 * if there are multiple releases such as betas from the same major.patch
838 * version (e.g. 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development
839 * snapshots for a given major version are always listed last.
840 *
841 * @param $available
842 * Array of data about available project releases.
843 *
844 * @see update_status_get_available()
845 * @see update_status_get_projects()
846 * @see update_status_process_project_info()
847 */
848 function update_status_calculate_project_data($available) {
849
850 // Retrieve the projects from cache, if present.
851 $projects = update_status_project_cache('update_status_data');
852 // If $projects is empty, then the cache must be rebuilt.
853 // Otherwise, return the cached data and skip the rest of the function.
854 if (!empty($projects)) {
855 return $projects;
856 }
857
858 $projects = update_status_get_projects();
859 update_status_process_project_info($projects);
860 $settings = variable_get('update_status_settings', array());
861 foreach ($projects as $project => $project_info) {
862 if (isset($available[$project])) {
863
864 // If the project status is marked as something bad, there's nothing
865 // else to consider.
866 if (isset($available[$project]['project_status'])) {
867 switch ($available[$project]['project_status']) {
868 case 'insecure':
869 $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
870 if (empty($projects[$project]['extra'])) {
871 $projects[$project]['extra'] = array();
872 }
873 $projects[$project]['extra'][] = array(
874 'class' => 'project-not-secure',
875 'label' => t('Project not secure'),
876 'data' => t('This project has been labeled insecure by the Drupal security team, and is no longer available for download. Immediately disabling everything included by this project is strongly recommended!'),
877 );
878 break;
879 case 'unpublished':
880 case 'revoked':
881 $projects[$project]['status'] = UPDATE_STATUS_REVOKED;
882 if (empty($projects[$project]['extra'])) {
883 $projects[$project]['extra'] = array();
884 }
885 $projects[$project]['extra'][] = array(
886 'class' => 'project-revoked',
887 'label' => t('Project revoked'),
888 'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
889 );
890 break;
891 case 'unsupported':
892 $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
893 if (empty($projects[$project]['extra'])) {
894 $projects[$project]['extra'] = array();
895 }
896 $projects[$project]['extra'][] = array(
897 'class' => 'project-not-supported',
898 'label' => t('Project not supported'),
899 'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
900 );
901 break;
902 default:
903 // Assume anything else (e.g. 'published') is valid and we should
904 // perform the rest of the logic in this function.
905 break;
906 }
907 }
908
909 if (!empty($projects[$project]['status'])) {
910 // We already know the status for this project, so there's nothing
911 // else to compute. Just record everything else we fetched from the
912 // XML file into our projects array and move to the next project.
913 $projects[$project] += $available[$project];
914 continue;
915 }
916
917 // Figure out the target major version.
918 $existing_major = $project_info['existing_major'];
919 $supported_majors = array();
920 if (isset($available[$project]['supported_majors'])) {
921 $supported_majors = explode(',', $available[$project]['supported_majors']);
922 }
923 elseif (isset($available[$project]['default_major'])) {
924 // Older release history XML file without supported or recommended.
925 $supported_majors[] = $available[$project]['default_major'];
926 }
927
928 if (in_array($existing_major, $supported_majors)) {
929 // Still supported, stay at the current major version.
930 $target_major = $existing_major;
931 }
932 elseif (isset($available[$project]['recommended_major'])) {
933 // Since 'recommended_major' is defined, we know this is the new XML
934 // format. Therefore, we know the current release is unsupported since
935 // its major version was not in the 'supported_majors' list. We should
936 // find the best release from the recommended major version.
937 $target_major = $available[$project]['recommended_major'];
938 $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
939 }
940 elseif (isset($available[$project]['default_major'])) {
941 // Older release history XML file without recommended, so recommend
942 // the currently defined "default_major" version.
943 $target_major = $available[$project]['default_major'];
944 }
945 else {
946 // Malformed XML file? Stick with the current version.
947 $target_major = $existing_major;
948 }
949
950 // Make sure we never tell the admin to downgrade. If we recommended an
951 // earlier version than the one they're running, they'd face an
952 // impossible data migration problem, since Drupal never supports a DB
953 // downgrade path. In the unfortunate case that what they're running is
954 // unsupported, and there's nothing newer for them to upgrade to, we
955 // can't print out a "Recommended version", but just have to tell them
956 // what they have is unsupported and let them figure it out.
957 $target_major = max($existing_major, $target_major);
958
959 $version_patch_changed = '';
960 $patch = '';
961
962 // Defend ourselves from XML history files that contain no releases.
963 if (empty($available[$project]['releases'])) {
964 $projects[$project]['status'] = UPDATE_STATUS_UNKNOWN;
965 $projects[$project]['reason'] = t('No available releases found');
966 continue;
967 }
968 foreach ($available[$project]['releases'] as $version => $release) {
969 // First, if this is the existing release, check a few conditions.
970 if ($projects[$project]['existing_version'] == $version) {
971 if (isset($release['terms']['Release type']) &&
972 in_array('Insecure', $release['terms']['Release type'])) {
973 $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
974 }
975 elseif ($release['status'] == 'unpublished') {
976 $projects[$project]['status'] = UPDATE_STATUS_REVOKED;
977 if (empty($projects[$project]['extra'])) {
978 $projects[$project]['extra'] = array();
979 }
980 $projects[$project]['extra'][] = array(
981 'class' => 'release-revoked',
982 'label' => t('Release revoked'),
983 'data' => t('Your currently installed release has been revoked, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
984 );
985 }
986 elseif (isset($release['terms']['Release type']) &&
987 in_array('Unsupported', $release['terms']['Release type'])) {
988 $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
989 if (empty($projects[$project]['extra'])) {
990 $projects[$project]['extra'] = array();
991 }
992 $projects[$project]['extra'][] = array(
993 'class' => 'release-not-supported',
994 'label' => t('Release not supported'),
995 'data' => t('Your currently installed release is now unsupported, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
996 );
997 }
998 }
999
1000 // Otherwise, ignore unpublished, insecure, or unsupported releases.
1001 if ($release['status'] == 'unpublished' ||
1002 (isset($release['terms']['Release type']) &&
1003 (in_array('Insecure', $release['terms']['Release type']) ||
1004 in_array('Unsupported', $release['terms']['Release type'])))) {
1005 continue;
1006 }
1007
1008 // See if this is a higher major version than our target and yet still
1009 // supported. If so, record it as an "Also available" release.
1010 if ($release['version_major'] > $target_major &&
1011 in_array($release['version_major'], $supported_majors)) {
1012 if (!isset($available[$project]['also'])) {
1013 $available[$project]['also'] = array();
1014 }
1015 if (!isset($available[$project]['also'][$release['version_major']])) {
1016 $available[$project]['also'][$release['version_major']] = $version;
1017 }
1018 // Otherwise, this release can't matter to us, since it's neither
1019 // from the release series we're currently using nor the recommended
1020 // release. We don't even care about security updates for this
1021 // branch, since if a project maintainer puts out a security release
1022 // at a higher major version and not at the lower major version,
1023 // they must change the default major release at the same time, in
1024 // which case we won't hit this code.
1025 continue;
1026 }
1027
1028 // Look for the 'latest version' if we haven't found it yet. Latest is
1029 // defined as the most recent version for the target major version.
1030 if (!isset($available[$project]['latest_version'])
1031 && $release['version_major'] == $target_major) {
1032 $available[$project]['latest_version'] = $version;
1033 }
1034
1035 // Look for the development snapshot release for this branch.
1036 if (!isset($available[$project]['dev_version'])
1037 && isset($release['version_extra'])
1038 && $release['version_extra'] == 'dev') {
1039 $available[$project]['dev_version'] = $version;
1040 }
1041
1042 // Look for the 'recommended' version if we haven't found it yet (see
1043 // phpdoc at the top of this function for the definition).
1044 if (!isset($available[$project]['recommended'])
1045 && $release['version_major'] == $target_major
1046 && isset($release['version_patch'])) {
1047 if ($patch != $release['version_patch']) {
1048 $patch = $release['version_patch'];
1049 $version_patch_changed = $release['version'];
1050 }
1051 if (empty($release['version_extra']) && $patch == $release['version_patch']) {
1052 $available[$project]['recommended'] = $version_patch_changed;
1053 }
1054 }
1055
1056 // Stop searching once we hit the currently installed version.
1057 if ($projects[$project]['existing_version'] == $version) {
1058 break;
1059 }
1060
1061 // If we're running a dev snapshot and have a timestamp, stop
1062 // searching for security updates once we hit an official release
1063 // older than what we've got. Allow 100 seconds of leeway to handle
1064 // differences between the datestamp in the .info file and the
1065 // timestamp of the tarball itself (which are usually off by 1 or 2
1066 // seconds) so that we don't flag that as a new release.
1067 if ($projects[$project]['type'] == 'dev') {
1068 if (empty($projects[$project]['datestamp'])) {
1069 // We don't have current timestamp info, so we can't know.
1070 continue;
1071 }
1072 elseif (isset($release['date']) && ($projects[$project]['datestamp'] + 100 > $release['date'])) {
1073 // We're newer than this, so we can skip it.
1074 continue;
1075 }
1076 }
1077
1078 // See if this release is a security update.
1079 if (isset($release['terms']['Release type'])
1080 && in_array('Security update', $release['terms']['Release type'])) {
1081 $projects[$project]['security updates'][] = $release;
1082 }
1083 }
1084
1085 // If we were unable to find a recommended version, then make the latest
1086 // version the recommended version if possible.
1087 if (!isset($available[$project]['recommended']) && isset($available[$project]['latest_version'])) {
1088 $available[$project]['recommended'] = $available[$project]['latest_version'];
1089 }
1090
1091 // If we're running a dev snapshot, compare the date of the dev snapshot
1092 // with the latest official version, and record the absolute latest in
1093 // 'latest_dev' so we can correctly decide if there's a newer release
1094 // than our current snapshot.
1095 if ($projects[$project]['type'] == 'dev') {
1096 if (isset($available[$project]['dev_version']) && $available[$project]['releases'][$available[$project]['dev_version']]['date'] > $available[$project]['releases'][$available[$project]['latest_version']]['date']) {
1097 $projects[$project]['latest_dev'] = $available[$project]['dev_version'];
1098 }
1099 else {
1100 $projects[$project]['latest_dev'] = $available[$project]['latest_version'];
1101 }
1102 }
1103
1104 // Stash the info about available releases into our $projects array.
1105 $projects[$project] += $available[$project];
1106
1107 //
1108 // Check to see if we need an update or not.
1109 //
1110
1111 if (!empty($projects[$project]['security updates'])) {
1112 // If we found security updates, that always trumps any other status.
1113 $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
1114 }
1115
1116 if (isset($projects[$project]['status'])) {
1117 // If we already know the status, we're done.
1118 continue;
1119 }
1120
1121 // If we don't know what to recommend, there's nothing we can report.
1122 // Bail out early.
1123 if (!isset($projects[$project]['recommended'])) {
1124 $projects[$project]['status'] = UPDATE_STATUS_UNKNOWN;
1125 $projects[$project]['reason'] = t('No available releases found');
1126 continue;
1127 }
1128
1129 // First, see if we're not supposed to check due to settings.
1130 if (isset($settings[$project]) && isset($settings[$project]['check']) &&
1131 ($settings[$project]['check'] == 'never' ||
1132 $settings[$project]['check'] == $available[$project]['recommended'])) {
1133 $projects[$project]['check'] = FALSE;
1134 $projects[$project]['status'] = UPDATE_STATUS_NOT_CHECKED;
1135 $projects[$project]['reason'] = t('Ignored by settings');
1136 $projects[$project]['notes'] = $settings[$project]['notes'];
1137 continue;
1138 }
1139
1140 // If we're running a dev snapshot, compare the date of the dev snapshot
1141 // with the latest official version, and record the absolute latest in
1142 // 'latest_dev' so we can correctly decide if there's a newer release
1143 // than our current snapshot.
1144 if ($projects[$project]['type'] == 'dev') {
1145 if (isset($available[$project]['dev_version']) && $available[$project]['releases'][$available[$project]['dev_version']]['date'] > $available[$project]['releases'][$available[$project]['latest_version']]['date']) {
1146 $projects[$project]['latest_dev'] = $available[$project]['dev_version'];
1147 }
1148 else {
1149 $projects[$project]['latest_dev'] = $available[$project]['latest_version'];
1150 }
1151 }
1152
1153 // Figure out the status, based on what we've seen and the install type.
1154 switch ($projects[$project]['type']) {
1155 case 'official':
1156 if ($projects[$project]['existing_version'] == $projects[$project]['recommended'] || $projects[$project]['existing_version'] == $projects[$project]['latest_version']) {
1157 $projects[$project]['status'] = UPDATE_STATUS_CURRENT;
1158 }
1159 else {
1160 $projects[$project]['status'] = UPDATE_STATUS_NOT_CURRENT;
1161 }
1162 break;
1163
1164 case 'dev':
1165 $latest = $available[$project]['releases'][$projects[$project]['latest_dev']];
1166 if (empty($projects[$project]['datestamp'])) {
1167 $projects[$project]['status'] = UPDATE_STATUS_NOT_CHECKED;
1168 $projects[$project]['reason'] = t('No filedate available');
1169 }
1170 elseif (($projects[$project]['datestamp'] + 100 > $latest['date'])) {
1171 $projects[$project]['status'] = UPDATE_STATUS_CURRENT;
1172 }