You are here

patchinfo.module in PatchInfo 7

Same filename and directory in other branches
  1. 8.2 patchinfo.module
  2. 8 patchinfo.module

Patch Info primary module file.

File

patchinfo.module
View source
<?php

/**
 * @file
 * Patch Info primary module file.
 */

/**
 * Implements hook_system_info_alter().
 */
function patchinfo_system_info_alter(&$info, $file, $type) {

  // Get patch information from .info files and save it to the database.
  if (array(
    $type,
    array(
      'module',
      'theme',
    ),
  )) {
    _patchinfo_clear_db($file->name);
    if (isset($info['patch']) && is_array($info['patch']) && count($info['patch']) > 0) {
      _patchinfo_process_module($file->name, $info['patch']);
    }
  }
}

/**
 * Implements hook_update_projects_alter().
 */
function patchinfo_update_projects_alter(&$projects) {
  $excluded_modules = variable_get('patchinfo_exclude_from_update_check', array());

  // Hide excluded modules from update check.
  if (count($excluded_modules) > 0) {
    foreach ($excluded_modules as $module) {
      if (isset($projects[$module])) {
        unset($projects[$module]);
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for update_manager_update_form().
 */
function patchinfo_form_update_manager_update_form_alter(&$form, &$form_state, $form_id) {

  // Add our CSS.
  $form['#attached']['css'][] = drupal_get_path('module', 'patchinfo') . '/patchinfo.css';

  // Add a highly visible notice for patched modules.
  $patch_info = _patchinfo_get_info();
  if (count($patch_info) > 0) {
    $projects = update_get_projects();
    foreach (element_children($form['project_downloads']) as $module) {
      $patches = _patchinfo_get_patches($patch_info, $projects[$module]);
      if (count($patches) > 0) {
        if (isset($form['projects']['#options'][$module]['title'])) {
          $form['projects']['#options'][$module]['title'] .= theme('patchinfo_patches', array(
            'patches' => $patches,
            'is_core' => FALSE,
          ));
        }
        elseif (isset($form['disabled_projects']['#options'][$module]['title'])) {
          $form['disabled_projects']['#options'][$module]['title'] .= theme('patchinfo_patches', array(
            'patches' => $patches,
            'is_core' => FALSE,
          ));
        }
      }
    }

    // If a manual update is available, check, if Drupal core has any
    // patches. If so, show a warning above the update form.
    if (isset($form['manual_updates'])) {
      $patches_drupal = _patchinfo_get_patches($patch_info, $projects['drupal']);
      if (count($patches_drupal) > 0) {
        $form['#prefix'] = theme('patchinfo_patches', array(
          'patches' => $patches_drupal,
          'is_core' => TRUE,
        ));
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for update_settings().
 */
function patchinfo_form_update_settings_alter(&$form, &$form_state, $form_id) {
  $excluded_modules = variable_get('patchinfo_exclude_from_update_check', array());

  // Add text area for exclude modules setting.
  $form['patchinfo_exclude_from_update_check'] = array(
    '#title' => t('Exclude modules from update check'),
    '#type' => 'textarea',
    '#description' => t('Modules, which should be excluded from the update check, can be listed here. Each entry should use the machine readable name of the module and go on a separate line.'),
    '#default_value' => implode("\n", $excluded_modules),
  );
  $form['#submit'][] = 'patchinfo_update_settings_form_submit';
}

/**
 * Submission handler for extended update settings form.
 *
 * @param array $form
 *   Drupal Form API form information.
 * @param array $form_state
 *   Drupal Form API form state information.
 */
function patchinfo_update_settings_form_submit(array $form, array &$form_state) {

  // Get value of exclude update setting from form state and save it
  // into a variable.
  $exclude_modules = $form_state['values']['patchinfo_exclude_from_update_check'];
  $exclude_modules = explode("\n", $exclude_modules);
  foreach ($exclude_modules as $k => $e) {
    $e = trim($e);
    if (!empty($e)) {
      $exclude_modules[$k] = $e;
    }
    else {
      unset($exclude_modules[$k]);
    }
  }
  variable_set('patchinfo_exclude_from_update_check', $exclude_modules);
}

/**
 * Remove all patch information for a module from DB.
 *
 * @param string $module
 *   Machine readable module name.
 */
function _patchinfo_clear_db($module) {
  if (!empty($module)) {
    db_delete('patchinfo')
      ->condition('module', $module)
      ->execute();
  }
}

/**
 * Get all patches for a module (including its submodules).
 *
 * @param array $patch_info
 *   Patch information as returned by _patchinfo_get_info().
 * @param array $project_info
 *   Project information for a single project, i.e. a single value from
 *   the array returned by update_get_projects().
 *
 * @return array
 *   Array containing all patch information for a module and its
 *   submodules.
 */
function _patchinfo_get_patches(array $patch_info, array $project_info) {
  $return = array();

  // For each module in this project (including submodules) check, if
  // there are patches and if so, merge them into our array containing
  // all patch information for this project.
  foreach ($project_info['includes'] as $module_key => $module_name) {
    if (isset($patch_info[$module_key])) {
      $return = array_merge($return, $patch_info[$module_key]);
    }
  }
  return $return;
}

/**
 * Get patch information from DB.
 *
 * @param bool $raw
 *   If TRUE, uses an array containing url and info keys for each
 *   patch. If FALSE (default), either use info text only or, if
 *   a URL is available, a suitable link for each patch.
 *
 * @return array
 *   Array of patches in DB keyed by machine readable module name.
 */
function _patchinfo_get_info($raw = FALSE) {
  $patch_info = array();
  $result = db_select('patchinfo', 'pi')
    ->fields('pi', array(
    'module',
    'id',
    'url',
    'info',
  ))
    ->execute();
  foreach ($result as $row) {
    if (!isset($patch_info[$row->module])) {
      $patch_info[$row->module] = array();
    }
    if ($raw) {
      $patch_info[$row->module][$row->id]['url'] = $row->url;
      $patch_info[$row->module][$row->id]['info'] = $row->info;
    }
    else {
      if (!empty($row->url)) {
        $patch_info[$row->module][$row->id] = l($row->info, $row->url, array(
          'external' => TRUE,
        ));
      }
      else {
        $patch_info[$row->module][$row->id] = check_plain($row->info);
      }
    }
  }
  return $patch_info;
}

/**
 * Process patch information for a module.
 *
 * @param string $module
 *   Machine readable module name.
 * @param array $patch_info
 *   Patch info from module info file.
 */
function _patchinfo_process_module($module, array $patch_info) {
  if (!empty($module)) {
    foreach ($patch_info as $key => $info) {

      // Calculate an index for each patch, which is not 0.
      $index = $key + 1;

      // Extract URL from patch information, if any.
      $info = explode(' ', $info);
      $url = '';
      if (filter_var($info[0], FILTER_VALIDATE_URL) !== FALSE) {
        $url = $info[0];
        unset($info[0]);
      }
      $info = implode(' ', $info);

      // Write patch information to db.
      db_merge('patchinfo')
        ->key(array(
        'module' => $module,
        'id' => $index,
      ))
        ->fields(array(
        'url' => $url,
        'info' => $info,
      ))
        ->execute();
    }
  }
}

/**
 * Implements hook_theme().
 */
function patchinfo_theme($existing, $type, $theme, $path) {
  return array(
    'patchinfo_excluded_modules' => array(
      'variables' => array(
        'excluded_modules' => array(),
      ),
    ),
    'patchinfo_patches' => array(
      'variables' => array(
        'patches' => array(),
        'is_core' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_theme_registry_alter().
 */
function patchinfo_theme_registry_alter(&$theme_registry) {

  // Replace theme function for update report with our own function,
  // so that we can alter it.
  if (isset($theme_registry['update_report'])) {
    $theme_registry['update_report']['function'] = 'patchinfo_update_report';
  }
}

/**
 * Custom implementation of theme_update_report().
 *
 * @see theme_update_report()
 */
function patchinfo_update_report($variables) {
  $patch_info = _patchinfo_get_info();
  $data = $variables['data'];
  $last = variable_get('update_last_check', 0);
  $output = theme('update_last_check', array(
    'last' => $last,
  ));

  // Add our CSS.
  drupal_add_css(drupal_get_path('module', 'patchinfo') . '/patchinfo.css');

  // Add markup for excluded modules.
  $output .= theme('patchinfo_excluded_modules', array(
    'excluded_modules' => variable_get('patchinfo_exclude_from_update_check', array()),
  ));
  if (!is_array($data)) {
    $output .= '<p>' . $data . '</p>';
    return $output;
  }
  $header = array();
  $rows = array();

  // Create an array of status values keyed by module or theme name, since
  // we'll need this while generating the report if we have to cross reference
  // anything (e.g. subthemes which have base themes missing an update).
  foreach ($data as $project) {
    foreach ($project['includes'] as $key => $name) {
      $status[$key] = $project['status'];
    }
  }
  foreach ($data as $project) {
    switch ($project['status']) {
      case UPDATE_CURRENT:
        $class = 'ok';
        $icon = theme('image', array(
          'path' => 'misc/watchdog-ok.png',
          'width' => 18,
          'height' => 18,
          'alt' => t('ok'),
          'title' => t('ok'),
        ));
        break;
      case UPDATE_UNKNOWN:
      case UPDATE_FETCH_PENDING:
      case UPDATE_NOT_FETCHED:
        $class = 'unknown';
        $icon = theme('image', array(
          'path' => 'misc/watchdog-warning.png',
          'width' => 18,
          'height' => 18,
          'alt' => t('warning'),
          'title' => t('warning'),
        ));
        break;
      case UPDATE_NOT_SECURE:
      case UPDATE_REVOKED:
      case UPDATE_NOT_SUPPORTED:
        $class = 'error';
        $icon = theme('image', array(
          'path' => 'misc/watchdog-error.png',
          'width' => 18,
          'height' => 18,
          'alt' => t('error'),
          'title' => t('error'),
        ));
        break;
      case UPDATE_NOT_CHECKED:
      case UPDATE_NOT_CURRENT:
      default:
        $class = 'warning';
        $icon = theme('image', array(
          'path' => 'misc/watchdog-warning.png',
          'width' => 18,
          'height' => 18,
          'alt' => t('warning'),
          'title' => t('warning'),
        ));
        break;
    }
    $row = '<div class="version-status">';
    $status_label = theme('update_status_label', array(
      'status' => $project['status'],
    ));
    $row .= !empty($status_label) ? $status_label : check_plain($project['reason']);
    $row .= '<span class="icon">' . $icon . '</span>';
    $row .= "</div>\n";
    $row .= '<div class="project">';
    if (isset($project['title'])) {
      if (isset($project['link'])) {
        $row .= l($project['title'], $project['link']);
      }
      else {
        $row .= check_plain($project['title']);
      }
    }
    else {
      $row .= check_plain($project['name']);
    }
    $row .= ' ' . check_plain($project['existing_version']);
    if ($project['install_type'] == 'dev' && !empty($project['datestamp'])) {
      $row .= ' <span class="version-date">(' . format_date($project['datestamp'], 'custom', 'Y-M-d') . ')</span>';
    }
    $row .= "</div>\n";
    $versions_inner = '';
    $security_class = array();
    $version_class = array();
    if (isset($project['recommended'])) {
      if ($project['status'] != UPDATE_CURRENT || $project['existing_version'] !== $project['recommended']) {

        // First, figure out what to recommend.
        // If there's only 1 security update and it has the same version we're
        // recommending, give it the same CSS class as if it was recommended,
        // but don't print out a separate "Recommended" line for this project.
        if (!empty($project['security updates']) && count($project['security updates']) == 1 && $project['security updates'][0]['version'] === $project['recommended']) {
          $security_class[] = 'version-recommended';
          $security_class[] = 'version-recommended-strong';
        }
        else {
          $version_class[] = 'version-recommended';

          // Apply an extra class if we're displaying both a recommended
          // version and anything else for an extra visual hint.
          if ($project['recommended'] !== $project['latest_version'] || !empty($project['also']) || $project['install_type'] == 'dev' && isset($project['dev_version']) && $project['latest_version'] !== $project['dev_version'] && $project['recommended'] !== $project['dev_version'] || isset($project['security updates'][0]) && $project['recommended'] !== $project['security updates'][0]) {
            $version_class[] = 'version-recommended-strong';
          }
          $versions_inner .= theme('update_version', array(
            'version' => $project['releases'][$project['recommended']],
            'tag' => t('Recommended version:'),
            'class' => $version_class,
          ));
        }

        // Now, print any security updates.
        if (!empty($project['security updates'])) {
          $security_class[] = 'version-security';
          foreach ($project['security updates'] as $security_update) {
            $versions_inner .= theme('update_version', array(
              'version' => $security_update,
              'tag' => t('Security update:'),
              'class' => $security_class,
            ));
          }
        }
      }
      if ($project['recommended'] !== $project['latest_version']) {
        $versions_inner .= theme('update_version', array(
          'version' => $project['releases'][$project['latest_version']],
          'tag' => t('Latest version:'),
          'class' => array(
            'version-latest',
          ),
        ));
      }
      if ($project['install_type'] == 'dev' && $project['status'] != UPDATE_CURRENT && isset($project['dev_version']) && $project['recommended'] !== $project['dev_version']) {
        $versions_inner .= theme('update_version', array(
          'version' => $project['releases'][$project['dev_version']],
          'tag' => t('Development version:'),
          'class' => array(
            'version-latest',
          ),
        ));
      }
    }
    if (isset($project['also'])) {
      foreach ($project['also'] as $also) {
        $versions_inner .= theme('update_version', array(
          'version' => $project['releases'][$also],
          'tag' => t('Also available:'),
          'class' => array(
            'version-also-available',
          ),
        ));
      }
    }
    if (!empty($versions_inner)) {
      $row .= "<div class=\"versions\">\n" . $versions_inner . "</div>\n";
    }
    $row .= "<div class=\"info\">\n";
    if (!empty($project['extra'])) {
      $row .= '<div class="extra">' . "\n";
      foreach ($project['extra'] as $key => $value) {
        $row .= '<div class="' . implode(' ', $value['class']) . '">';
        $row .= check_plain($value['label']) . ': ';
        $row .= drupal_placeholder($value['data']);
        $row .= "</div>\n";
      }
      $row .= "</div>\n";

      // Extra div.
    }

    // Display patch information.
    $patches = _patchinfo_get_patches($patch_info, $project);
    if (count($patches) > 0) {
      $row .= theme('patchinfo_patches', array(
        'patches' => $patches,
        'is_core' => FALSE,
      ));
    }
    $row .= '<div class="includes">';
    sort($project['includes']);
    if (!empty($project['disabled'])) {
      sort($project['disabled']);

      // Make sure we start with a clean slate for each project in the report.
      $includes_items = array();
      $row .= t('Includes:');
      $includes_items[] = t('Enabled: %includes', array(
        '%includes' => implode(', ', $project['includes']),
      ));
      $includes_items[] = t('Disabled: %disabled', array(
        '%disabled' => implode(', ', $project['disabled']),
      ));
      $row .= theme('item_list', array(
        'items' => $includes_items,
      ));
    }
    else {
      $row .= t('Includes: %includes', array(
        '%includes' => implode(', ', $project['includes']),
      ));
    }
    $row .= "</div>\n";
    if (!empty($project['base_themes'])) {
      $row .= '<div class="basethemes">';
      asort($project['base_themes']);
      $base_themes = array();
      foreach ($project['base_themes'] as $base_key => $base_theme) {
        $switch_key = isset($status[$base_key]) ? $status[$base_key] : 'UNDEFINED';
        switch ($switch_key) {
          case UPDATE_NOT_SECURE:
          case UPDATE_REVOKED:
          case UPDATE_NOT_SUPPORTED:
            $base_themes[] = t('%base_theme (!base_label)', array(
              '%base_theme' => $base_theme,
              '!base_label' => theme('update_status_label', array(
                'status' => $status[$base_key],
              )),
            ));
            break;
          default:
            $base_themes[] = drupal_placeholder($base_theme);
        }
      }
      $row .= t('Depends on: !basethemes', array(
        '!basethemes' => implode(', ', $base_themes),
      ));
      $row .= "</div>\n";
    }
    if (!empty($project['sub_themes'])) {
      $row .= '<div class="subthemes">';
      sort($project['sub_themes']);
      $row .= t('Required by: %subthemes', array(
        '%subthemes' => implode(', ', $project['sub_themes']),
      ));
      $row .= "</div>\n";
    }
    $row .= "</div>\n";

    // Info div.
    if (!isset($rows[$project['project_type']])) {
      $rows[$project['project_type']] = array();
    }
    $row_key = isset($project['title']) ? drupal_strtolower($project['title']) : drupal_strtolower($project['name']);
    $rows[$project['project_type']][$row_key] = array(
      'class' => array(
        $class,
      ),
      'data' => array(
        $row,
      ),
    );
  }
  $project_types = array(
    'core' => t('Drupal core'),
    'module' => t('Modules'),
    'theme' => t('Themes'),
    'module-disabled' => t('Disabled modules'),
    'theme-disabled' => t('Disabled themes'),
  );
  foreach ($project_types as $type_name => $type_label) {
    if (!empty($rows[$type_name])) {
      ksort($rows[$type_name]);
      $output .= "\n<h3>" . $type_label . "</h3>\n";
      $output .= theme('table', array(
        'header' => $header,
        'rows' => $rows[$type_name],
        'attributes' => array(
          'class' => array(
            'update',
          ),
        ),
      ));
    }
  }
  drupal_add_css(drupal_get_path('module', 'update') . '/update.css');
  return $output;
}

/**
 * Returns HTML for the excluded modules information.
 *
 * @param array $variables
 *   An associative array containing:
 *   - excluded_modules: Array of machine readable names of modules
 *       excluded from update check.
 *
 * @ingroup themeable
 */
function theme_patchinfo_excluded_modules(array $variables) {
  $output = '';

  // Get excluded modules from variables.
  $excluded_modules = $variables['excluded_modules'];

  // Generate markup.
  if (count($excluded_modules) > 0) {
    $output .= '<div class="patchinfo-excluded-modules">';
    $output .= '<strong>' . t('Modules excluded from update check:') . '</strong>';
    $excluded_modules = array_map('check_plain', $excluded_modules);
    $output .= theme('item_list', array(
      'items' => $excluded_modules,
    ));
    $output .= '<p>' . t("If you don't want to exclude one of these modules any longer, please update your !settings.", array(
      '!settings' => l(t('settings'), 'admin/reports/updates/settings'),
    )) . '</p>';
    $output .= '</div>';
  }
  return $output;
}

/**
 * Returns HTML listing all patches of a module and its submodules.
 *
 * @param array $variables
 *   An associative array containing:
 *   - patches: Array of patch information for a module and its
 *       submodules.
 *   - is_core: TRUE, if the patches are in Drupal core. Otherwise FALSE.
 *
 * @ingroup themeable
 */
function theme_patchinfo_patches(array $variables) {
  $patches = $variables['patches'];
  $is_core = $variables['is_core'];

  // Generate markup for patch list.
  $output = '<div class="patchinfo-patches">';
  if ($is_core) {
    $output .= '<p class="patchinfo-patches-core">';
    $output .= t('Before you do a manual update, please note, that the following patches are currently applied to Drupal core:');
    $output .= '</p>';
  }
  $output .= '<div class="patchinfo-patches-title">' . t('Patches:') . '</div>';
  $patches = array_map('filter_xss', $patches);
  $output .= theme('item_list', array(
    'items' => $patches,
  ));
  $output .= '</div>';
  return $output;
}

Functions

Namesort descending Description
patchinfo_form_update_manager_update_form_alter Implements hook_form_FORM_ID_alter() for update_manager_update_form().
patchinfo_form_update_settings_alter Implements hook_form_FORM_ID_alter() for update_settings().
patchinfo_system_info_alter Implements hook_system_info_alter().
patchinfo_theme Implements hook_theme().
patchinfo_theme_registry_alter Implements hook_theme_registry_alter().
patchinfo_update_projects_alter Implements hook_update_projects_alter().
patchinfo_update_report Custom implementation of theme_update_report().
patchinfo_update_settings_form_submit Submission handler for extended update settings form.
theme_patchinfo_excluded_modules Returns HTML for the excluded modules information.
theme_patchinfo_patches Returns HTML listing all patches of a module and its submodules.
_patchinfo_clear_db Remove all patch information for a module from DB.
_patchinfo_get_info Get patch information from DB.
_patchinfo_get_patches Get all patches for a module (including its submodules).
_patchinfo_process_module Process patch information for a module.