<?php

define('UPDATE_STATUS_CURRENT', 1);
define('UPDATE_STATUS_NOT_CURRENT', 2);
define('UPDATE_STATUS_UNKNOWN', 3);

/**
 * Implementation of hook_help().
 */
function update_status_help($section) {
  switch ($section) {
    case 'admin/logs/updates':
			return '<p>'. t('This page is generated by the update_status module, and lists information on the current status of your installed modules. Each module is part of a "project" which may or may not have the same name as the module itself. Note that update_status.module can only check version numbers for official releases, and will not be able to check the status of development snapshots or modules updated directly from CVS. Once you download a newer module, unzip it, and upload it to your server, you must visit !update_php to complete the process.', array('!update_php' => l('the update.php page', 'update.php', ''))) .'</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/force-check',
      'title' => t('Manual update check'),
      'callback' => 'update_status_force_status',
      'access' => user_access('administer site configuration'),
      'type' => MENU_CALLBACK);
  }
  return $items;
}

/**
 * Menu callback. Generate a page of information about the update status of projects.
 */
function update_status_status() {
  if ($info = variable_get('update_status', FALSE)) {
    $data = update_status_get_projects();
    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];
        $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 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')))));
  }
}

/**
 * Fetch project info via XML-RPC from a central server.
 */
function update_status_info($projects = 'all') {
  $server = variable_get('update_status_server', 'http://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);
}

/**
 * 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) {
    $info = _module_parse_info_file(dirname($file->filename) .'/'. $file->name .'.info');
    // Skip modules that aren't enabled or that don't provide info.
    if (isset($file->status) && ($file->status == TRUE) && !empty($info) && array_key_exists('project', $info) && array_key_exists('version', $info) && $info['project']) {
      if (!array_key_exists($info['project'], $projects)) {
        $projects[$info['project']] = array(
          'name' => $info['project'],
          'existing_version' => $info['version'],
          '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;
    }

    $row1 = array(
      'class' => 'top-row ' . ($project['status'] == UPDATE_STATUS_NOT_CURRENT ?
        'error' : 'ok'),
      'data' => array(),
    );
    $row2 = array(
      'class' => 'bottom-row ' . ($project['status'] == UPDATE_STATUS_NOT_CURRENT ?
        'error' : 'ok'),
      '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;
      default:
        $row1['data'][] = t('Unknown');
    }
    $row1['data'][] = array('class' => 'current-version', 'data' => $project['existing_version']);
    $row1['data'][] = array('class' => 'new-version', 'data' => $project['version']);

    $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_get_projects();
      if ($data['drupal']['existing_version'] != $info['drupal']['version']) {
        $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');
      }
      $requirements['update_status']['value'] = t('Up to date');
      foreach (array_keys($data) as $project) {
        if (array_key_exists($project, $info) && 
            $data[$project]['existing_version'] != $info[$project]['version']) {
          $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;
  }
}
