You are here

update_status.module in Update Status 5

Same filename and directory in other branches
  1. 5.2 update_status.module

File

update_status.module
View source
<?php

define('UPDATE_STATUS_CURRENT', 1);
define('UPDATE_STATUS_NOT_CURRENT', 2);
define('UPDATE_STATUS_UNKNOWN', 3);
define('UPDATE_STATUS_NOT_CHECKED', 4);
define('UPDATE_STATUS_CANT_CHECK', 5);

/**
 * Implementation of hook_help().
 */
function update_status_help($section) {
  switch ($section) {
    case 'admin/logs/updates':
      return '<p>' . t('Here you can find information on the update status of 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. Also note that this can only check the status for "official releases", and will not be able to check update status for development snapshots and modules updated directly from CVS.') . '</p>';
    case 'admin/build/modules':
      $status = update_status_requirements('runtime');
      if ($status['update_status']['severity'] == REQUIREMENT_ERROR) {
        drupal_set_message(t('There are updates available for one or more of your modules. To ensure the security of your server, you should update immediately. See the !status_page for more information', array(
          '!status_page' => l('update status page', 'admin/logs/updates'),
        )), 'error');
      }
      return '<p>' . t('See the <a href="!updates">updates log page</a> for information on available updates.', array(
        '!updates' => url('admin/logs/updates'),
      )) . '</p>';
  }
}

/** 
 * Implementation of hook_menu().
 */
function update_status_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/logs/updates',
      'title' => t('Available updates'),
      'description' => t('Get a status report on your installed modules and available updates.'),
      'callback' => 'update_status_status',
      'weight' => 10,
      'access' => user_access('administer site configuration'),
    );
    $items[] = array(
      'path' => 'admin/logs/updates/list',
      'title' => t('List'),
      'callback' => 'update_status_status',
      'access' => user_access('administer site configuration'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/logs/updates/settings',
      'title' => t('Settings'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'update_status_settings',
      ),
      'access' => user_access('administer site configuration'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/logs/updates/force-check',
      'title' => t('Manual update check'),
      'callback' => 'update_status_force_status',
      'access' => user_access('administer site configuration'),
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}
function update_status_calculate_project_data($info) {
  $data = update_status_get_projects();
  $settings = variable_get('update_status_settings', array());
  foreach (array_keys($data) as $project) {
    if (array_key_exists($project, $info)) {

      // The name is returned in human-readable format. Change it to title
      // so it's not overwritten by the name key returned by update_status_get_projects().
      $info[$project]['title'] = $info[$project]['name'];
      $data[$project] += $info[$project];
      if (isset($settings[$project]) && isset($settings[$project]['check']) && ($settings[$project]['check'] == 'never' || $settings[$project]['check'] == $info[$project]['version'])) {
        $data[$project]['check'] = FALSE;
        $data[$project]['status'] = UPDATE_STATUS_NOT_CHECKED;
      }
      else {
        if (isset($data[$project]['check']) && empty($data[$project]['check'])) {
          $data[$project]['status'] = UPDATE_STATUS_CANT_CHECK;
        }
        else {
          $data[$project]['status'] = $data[$project]['existing_version'] == $data[$project]['version'] ? UPDATE_STATUS_CURRENT : UPDATE_STATUS_NOT_CURRENT;
        }
      }
    }
    else {
      $data[$project]['status'] = UPDATE_STATUS_UNKNOWN;
    }
  }
  return $data;
}

/**
 * Menu callback. Generate a page of information about the update status of projects.
 */
function update_status_status() {
  if (!function_exists('gzinflate')) {
    drupal_set_message(t('Your system needs the zlib extension for this module to work. See !link for more information.', array(
      '!link' => l('http://us.php.net/manual/en/ref.zlib.php', 'http://us.php.net/manual/en/ref.zlib.php', NULL, NULL, NULL, TRUE),
    )), 'error');
    return FALSE;
  }
  if ($info = variable_get('update_status', FALSE)) {
    $data = update_status_calculate_project_data($info);
    return theme('update_status_report', $data);
  }
  else {
    return theme('update_status_report', t('No update data is available. To fetch data, you may need to !run_cron.', array(
      '!run_cron' => l(t('run cron'), 'admin/logs/status/run-cron', NULL, 'destination=' . url('admin/logs/updates')),
    )));
  }
}

/**
 * Menu callback. Show the settings for the update status module.
 */
function update_status_settings() {
  $form = array();
  if ($info = variable_get('update_status', FALSE)) {
    $values = variable_get('update_status_settings', array());
    $form['projects'] = array(
      '#tree' => TRUE,
    );
    $data = update_status_get_projects();
    $form['data'] = array(
      '#type' => 'value',
      '#value' => $data,
    );
    $form['info'] = array(
      '#type' => 'value',
      '#value' => $info,
    );
    foreach ($data as $key => $project) {
      if (array_key_exists($key, $info)) {
        if (!isset($values[$key])) {
          $values[$key] = array(
            'check' => 'always',
            'notes' => '',
          );
        }
        $options = array(
          'always' => t('Always'),
          $info[$key]['version'] => t('Ignore @version', array(
            '@version' => $info[$key]['version'],
          )),
          'never' => t('Never'),
        );
        $form['projects'][$key]['check'] = array(
          '#type' => 'select',
          '#options' => $options,
          '#default_value' => $values[$key]['check'],
        );
        $form['projects'][$key]['notes'] = array(
          '#type' => 'textfield',
          '#size' => 50,
          '#default_value' => $values[$key]['notes'],
        );
      }
    }
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit changes'),
    );
  }
  else {
    $form['error'] = array(
      '#value' => theme('update_status_report', t('No update data is available. To fetch data, you may need to !run_cron.', array(
        '!run_cron' => l(t('run cron'), 'admin/logs/status/run-cron', NULL, 'destination=' . url('admin/logs/updates')),
      ))),
    );
  }
  return $form;
}
function theme_update_status_settings($form) {
  if (isset($form['error'])) {
    return drupal_render($form);
  }
  $header = array(
    array(
      'data' => t('Project'),
      'class' => 'project',
    ),
    array(
      'data' => t('Warn if out of date'),
      'class' => 'status',
    ),
    array(
      'data' => t('Notes'),
      'class' => 'current-version',
    ),
  );
  $data = $form['data']['#value'];
  $info = $form['info']['#value'];
  $rows = array();
  foreach ($data as $key => $project) {
    if (array_key_exists($key, $info)) {

      // The name is returned in human-readable format. Change it to title
      // so it's not overwritten by the name key returned by update_status_get_projects().
      $row = array();
      $row['data'][] = array(
        'class' => 'project',
        'data' => check_plain($info[$key]['name']),
      );
      $row['data'][] = drupal_render($form['projects'][$key]['check']);
      $row['data'][] = drupal_render($form['projects'][$key]['notes']);
      $rows[] = $row;
    }
  }
  return theme('table', $header, $rows) . drupal_render($form);
}
function update_status_settings_submit($form_id, $form_values) {
  variable_set('update_status_settings', $form_values['projects']);
  drupal_set_message(t('Your changes have been saved.'));
}

/**
 * Fetch project info via XML-RPC from a central server.
 */
function update_status_info($projects = 'all') {
  $server = variable_get('update_status_server', 'http://updates.drupal.org/xmlrpc.php');
  $result = xmlrpc($server, 'project.release.data', $projects, '5.x');
  if ($result === FALSE) {
    watchdog('update_status', t('Update Status: Error %code: %message', array(
      '%code' => xmlrpc_errno(),
      '%message' => xmlrpc_error_msg(),
    )), WATCHDOG_ERROR);
    return FALSE;
  }
  return unserialize(gzinflate(substr(substr(base64_decode($result), 10), 0, -8)));
}

/**
 * Implementation of hook_cron().
 */
function update_status_cron() {
  if (time() - variable_get('update_status_last', 0) > 86400) {
    update_status_refresh();
    variable_set('update_status_last', time());
  }
}

/**
 * Callback to manually check the update status without cron.
 */
function update_status_force_status() {
  update_status_refresh();
  variable_set('update_status_last', time());
  drupal_goto('admin/logs/updates');
}

/**
 * Fetch data from a central server and save as a variable.
 */
function update_status_refresh() {
  $projects = array_keys(update_status_get_projects());
  $info = update_status_info($projects);
  variable_set('update_status', $info);
}

/**
 * Make a CVS version nicer if we know how. Code by webchick.
 */
function update_status_make_nice_version($version, &$check) {
  if (!$version) {
    $version = t('Unknown');
  }
  elseif (preg_match('/\\$' . 'Name: (.*?)\\$/', $version, $matches)) {
    $version = trim($matches[1]);
    if (!$version) {
      $version = 'HEAD';
    }
  }

  // TODO: sanitize this further but I can't figure out where dww's code to
  // do that lives.
  $check = FALSE;
  return $version;
}

/**
 * Fetch an array of installed and enabled projects.
 *
 * @todo
 *   Extend this to include themes and theme engines when they get .info files.
 */
function update_status_get_projects() {
  $projects = array();

  // Get current list of modules.
  $files = drupal_system_listing('\\.module$', 'modules', 'name', 0);

  // Extract current files from database.
  system_get_files_database($files, 'module');
  foreach ($files as $filename => $file) {

    // Skip not enabled modules.
    if (empty($file->status)) {
      continue;
    }
    $info = _module_parse_info_file(dirname($file->filename) . '/' . $file->name . '.info');

    // Skip if this is broken.
    if (empty($info)) {
      continue;
    }
    $info['check'] = TRUE;
    if (!array_key_exists('project', $info)) {

      // guess the project from the directory.
      $last = '';
      foreach (array_reverse(explode('/', $file->filename)) as $dir) {
        if ($dir == 'modules') {
          break;
        }
        $last = $dir;
      }
      if ($last) {
        $info['project'] = $last;
      }
      else {
        continue;
      }
    }
    if (!array_key_exists($info['project'], $projects)) {
      if (!array_key_exists('version', $info)) {
        $info['check'] = FALSE;
        $info['version'] = t('Unknown');
      }
      if (strpos($info['version'], '$Name') !== FALSE) {
        $info['version'] = update_status_make_nice_version($info['version'], $info['check']);
      }
      $projects[$info['project']] = array(
        'name' => $info['project'],
        'existing_version' => $info['version'],
        'check' => $info['check'],
        'modules' => array(
          $file->name => $info['name'],
        ),
      );
    }
    else {
      $projects[$info['project']]['modules'][$file->name] = $info['name'];
    }
  }
  asort($projects);
  return $projects;
}

/**
 * Theme project status report.
 */
function theme_update_status_report($data) {
  $i = 0;
  $last = variable_get('update_status_last', 0);
  $output = '<p>' . t('Last checked: ') . ($last ? format_date($last) : t('Never'));
  $output .= ' ' . l(t('Check manually'), 'admin/logs/updates/force-check') . '</p>';
  if (!is_array($data)) {
    $output .= '<p>' . $data . '</p>';
    return $output;
  }

  // move 'drupal' to the top
  $data = array(
    'drupal' => $data['drupal'],
  ) + $data;
  $header = array(
    array(
      'data' => t('Project'),
      'class' => 'project',
    ),
    array(
      'data' => t('Status'),
      'class' => 'status',
    ),
    array(
      'data' => t('Current version'),
      'class' => 'current-version',
    ),
    array(
      'data' => t('Available version'),
      'class' => 'available-version',
    ),
    array(
      'data' => t('Download latest version'),
      'class' => 'links',
    ),
  );
  foreach ($data as $project) {

    // This protects us from homegrown projects that either aren't
    // configured properly or don't actually have info on drupal.org
    if (!$project['title']) {
      continue;
    }
    switch ($project['status']) {
      case UPDATE_STATUS_NOT_CURRENT:
        $class = 'error';
        break;
      case UPDATE_STATUS_CURRENT:
        $class = 'ok';
        break;
      default:
        $class = 'unknown';
        break;
    }
    $row1 = array(
      'class' => 'top-row ' . $class,
      'data' => array(),
    );
    $row2 = array(
      'class' => 'bottom-row ' . $class,
      'data' => array(),
    );
    $row1['data'][] = array(
      'class' => 'project',
      'data' => l($project['title'], $project['link']),
    );
    switch ($project['status']) {
      case UPDATE_STATUS_CURRENT:
        $row1['data'][] = t('Up to date');
        break;
      case UPDATE_STATUS_NOT_CURRENT:
        $row1['data'][] = t('Update available');
        break;
      case UPDATE_STATUS_NOT_CHECKED:
        $row1['data'][] = t('Ignored');
        break;
      case UPDATE_STATUS_CANT_CHECK:
        $row1['data'][] = t("Ignored (CVS)");
        break;
      default:
        $row1['data'][] = t('Unknown');
    }
    $row1['data'][] = array(
      'class' => 'current-version',
      'data' => $project['existing_version'],
    );
    $row1['data'][] = array(
      'class' => 'new-version',
      'data' => l($project['version'], $project['release']) . ' (' . format_date($project['date'], 'custom', 'Y-M-d') . ')',
    );
    $links = array();
    $links[] = l($project['download']['title'], $project['download']['href']);
    $row1['data'][] = array(
      'class' => 'links',
      'data' => implode(' | ', $links),
    );
    $row2['data'][] = array(
      'class' => 'info',
      'colspan' => 5,
      'data' => t('Includes: %modules', array(
        '%modules' => implode(', ', $project['modules']),
      )),
    );
    $rows[] = $row1;
    $rows[] = $row2;
  }
  $output .= theme('table', $header, $rows, array(
    'class' => 'update-status',
  ));
  drupal_add_css(drupal_get_path('module', 'update_status') . '/' . 'update_status.css');
  return $output;
}

/**
 * Implementation of hook_requirements
 */
function update_status_requirements($phase) {
  if ($phase == 'runtime') {
    $requirements['update_status']['title'] = t('Module update status');
    $requirements['update_status_drupal']['title'] = t('Drupal core update status');
    if ($info = variable_get('update_status', FALSE)) {
      $data = update_status_calculate_project_data($info);
      if ($data['drupal']['status'] == UPDATE_STATUS_NOT_CURRENT) {
        $requirements['update_status_drupal']['value'] = t('Out of date. Version @version available.', array(
          '@version' => $info['drupal']['version'],
        ));
        $requirements['update_status_drupal']['severity'] = REQUIREMENT_ERROR;
        $requirements['update_status_drupal']['description'] = t('There are updates available for your version of Drupal. To ensure the security of your server, you should update immediately.See the !status_page for more information', array(
          '!status_page' => l('update status page', 'admin/logs/updates'),
        ));
      }
      else {
        $requirements['update_status_drupal']['value'] = t('Up to date');
      }

      // We don't want to check drupal a second time.
      unset($data['drupal']);
      $requirements['update_status']['value'] = t('Up to date');
      foreach (array_keys($data) as $project) {
        if (array_key_exists($project, $info) && $data[$project]['status'] == UPDATE_STATUS_NOT_CURRENT) {
          $requirements['update_status']['value'] = t('Out of date');
          $requirements['update_status']['severity'] = REQUIREMENT_ERROR;
          $requirements['update_status']['description'] = t('There are updates available for one or more of your modules. To ensure the security of your server, you should update immediately. See the !status_page for more information', array(
            '!status_page' => l('update status page', 'admin/logs/updates'),
          ));
          break;
        }
      }
    }
    else {
      $requirements['update_status']['value'] = t('Update status is unavailable. cron may need to be run.');
      $requirements['update_status_drupal']['value'] = t('Update status is unavailable. cron may need to be run.');
    }
    return $requirements;
  }
}

Functions

Namesort descending Description
theme_update_status_report Theme project status report.
theme_update_status_settings
update_status_calculate_project_data
update_status_cron Implementation of hook_cron().
update_status_force_status Callback to manually check the update status without cron.
update_status_get_projects Fetch an array of installed and enabled projects.
update_status_help Implementation of hook_help().
update_status_info Fetch project info via XML-RPC from a central server.
update_status_make_nice_version Make a CVS version nicer if we know how. Code by webchick.
update_status_menu Implementation of hook_menu().
update_status_refresh Fetch data from a central server and save as a variable.
update_status_requirements Implementation of hook_requirements
update_status_settings Menu callback. Show the settings for the update status module.
update_status_settings_submit
update_status_status Menu callback. Generate a page of information about the update status of projects.

Constants