You are here

patchinfo.module in PatchInfo 8.2

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

Patch Info primary module file.

File

patchinfo.module
View source
<?php

/**
 * @file
 * Patch Info primary module file.
 */
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Component\Utility\Html;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;

/**
 * Implements hook_system_info_alter().
 */
function patchinfo_system_info_alter(array $info, Extension $file, $type) {
  $patches = [];

  // Gather patch information from patch sources.
  $plugin_manager = \Drupal::service('plugin.manager.patchinfo_source');
  $sources = $plugin_manager
    ->getDefinitions();
  foreach ($sources as $plugin_id => $plugin_defintion) {
    $source = $plugin_manager
      ->createInstance($plugin_id, []);
    $patches_source = $source
      ->getPatches($info, $file, $type);
    $patches = array_merge_recursive($patches, $patches_source);
  }

  // Update patch database.
  _patchinfo_clear_db($file
    ->getName());
  foreach ($patches as $module => $info) {
    _patchinfo_process_module($file
      ->getName(), $module, $info);
  }
}

/**
 * Implements hook_update_projects_alter().
 */
function patchinfo_update_projects_alter(&$projects) {

  // Get modules to be excluded from update check from configuration.
  $excluded_projects = (array) \Drupal::config('patchinfo.settings')
    ->get('exclude from update check');
  $projects = array_diff_key($projects, array_flip($excluded_projects));
}

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

  // Add our CSS.
  $form['#attached']['library'][] = 'patchinfo/patchinfo';

  // Add a highly visible notice for patched modules.
  $patch_info = _patchinfo_get_info();
  if (count($patch_info) > 0) {

    // Get project information from update manager service.
    $projects = \Drupal::service('update.manager')
      ->getProjects();

    // Check each project and its submodules for patches.
    foreach (Element::children($form['project_downloads']) as $module) {
      $patches = _patchinfo_get_patches($patch_info, $projects[$module]);
      if (count($patches) > 0) {

        // Determine, if the module is enabled or disabled and add the patch
        // list to the appropriate section of the form.
        $key = '';
        if (isset($form['projects']['#options'][$module]['title'])) {
          $key = 'projects';
        }
        elseif (isset($form['disabled_projects']['#options'][$module]['title'])) {
          $key = 'disabled_projects';
        }
        if (!empty($key)) {
          $patch_list = [
            '#theme' => 'patchinfo_patches',
            '#patches' => $patches,
          ];
          $form[$key]['#options'][$module]['title']['data']['patches'] = $patch_list;
        }
      }
    }

    // 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) {
        $patch_list = [
          '#theme' => 'patchinfo_patches',
          '#patches' => $patches_drupal,
        ];
        $form['manual_updates']['#rows']['drupal']['data']['title']['data']['patches'] = $patch_list;
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for update_settings().
 */
function patchinfo_form_update_settings_alter(&$form, FormStateInterface $form_state, $form_id) {
  $config = \Drupal::config('patchinfo.settings');
  $form['patchinfo_exclude_from_update_check'] = [
    '#type' => 'textarea',
    '#title' => t('Exclude modules from update check'),
    '#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", (array) $config
      ->get('exclude from update check')),
  ];
  $form['#submit'][] = 'patchinfo_update_settings_form_submit';
}

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

  // Get value of exclude from update check setting from form state and prepare
  // it for storage.
  $exclude_modules = $form_state
    ->getValue('patchinfo_exclude_from_update_check');
  $exclude_modules = array_map('trim', explode("\n", $exclude_modules));
  $exclude_modules = array_filter($exclude_modules);

  // Store exclude from update check setting in configuration.
  $config = \Drupal::service('config.factory')
    ->getEditable('patchinfo.settings')
    ->set('exclude from update check', $exclude_modules)
    ->save();
}

/**
 * Remove all patch information for a module from DB.
 *
 * @param string $module
 *   Machine readable module name.
 */
function _patchinfo_clear_db($module) {
  if (!empty($module)) {

    // We need to wrap this in a try/catch, sice the code might be called during
    // the update process from 8.x-1.x when the source_module field is still
    // missing. The alternative would be to check the schema everytime, which
    // would be much more expensive.
    try {
      \Drupal::database()
        ->delete('patchinfo')
        ->condition('source_module', $module)
        ->execute();
    } catch (Exception $e) {
      watchdog_exception('patchinfo', $e);
    }
  }
}

/**
 * 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 \Drupal::service('update.manager')->getProjects().
 *
 * @return array
 *   Array containing all patch information for a module and its
 *   submodules.
 */
function _patchinfo_get_patches(array $patch_info, array $project_info) {
  $return = [];

  // 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 = [];
  $result = \Drupal::database()
    ->select('patchinfo', 'pi')
    ->fields('pi', [
    'module',
    'id',
    'url',
    'info',
    'source',
  ])
    ->execute();
  foreach ($result as $row) {
    if (!isset($patch_info[$row->module])) {
      $patch_info[$row->module] = [];
    }
    if ($raw) {
      $patch_info[$row->module][] = [
        'url' => $row->url,
        'info' => $row->info,
        'source' => $row->source,
      ];
    }
    else {
      if (!empty($row->url)) {
        $url = Url::fromUri($row->url);
        $text = $row->info;
        if (!empty($row->source)) {
          $text = t('@text <abbr title="@source">(src)</abbr>', [
            '@text' => $row->info,
            '@source' => $row->source,
          ]);
        }
        $patch_info[$row->module][] = Link::fromTextAndUrl($text, $url);
      }
      else {
        $text = Html::escape($row->info);
        if (!empty($row->source)) {
          $text = t('@text <abbr title="@source">(src)</abbr>', [
            '@text' => $row->info,
            '@source' => $row->source,
          ]);
        }
        $patch_info[$row->module][] = $text;
      }
    }
  }
  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.yml file.
 *
 * @throws \Exception
 */
function _patchinfo_process_module($source_module, $module, array $patch_info) {
  if (!empty($source_module) && !empty($module)) {
    foreach ($patch_info as $key => $info) {

      // Calculate an index for each patch, which is not 0.
      $index = $key + 1;
      if (!is_array($info)) {
        continue;
      }
      $source = isset($info['source']) ? $info['source'] : '';
      $info = isset($info['info']) ? $info['info'] : '';

      // 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);

      // We need to wrap this in a try/catch, sice the code might be called
      // during the update process from 8.x-1.x when the source_module and
      // source fields are still missing. The alternative would be to check the
      // schema everytime, which would be much more expensive.
      try {

        // Write patch information to db.
        \Drupal::database()
          ->merge('patchinfo')
          ->keys([
          'source_module' => $source_module,
          'module' => $module,
          'id' => $index,
        ])
          ->fields([
          'url' => $url,
          'info' => $info,
          'source' => $source,
        ])
          ->execute();
      } catch (Exception $e) {
        watchdog_exception('patchinfo', $e);
      }
    }
  }
}

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

/**
 * Implements hook_theme_registry_alter().
 */
function patchinfo_theme_registry_alter(&$theme_registry) {
  $module_path = drupal_get_path('module', 'patchinfo');
  if (isset($theme_registry['update_report'])) {

    // Replace template location for update report with our own templates
    // folder, so that we can alter the template.
    $theme_registry['update_report']['path'] = $module_path . '/templates';
  }
  if (isset($theme_registry['update_project_status'])) {

    // Update module will sort parts of the project information using sort().
    // Unfortunately, this results in the keys being lost. Since we need these
    // keys to check for patch information, we will move our preprocess function
    // in front of the preprocess function provided by update module.
    $position = array_search('patchinfo_preprocess_update_project_status', $theme_registry['update_project_status']['preprocess functions']);
    if ($position) {
      $out = array_splice($theme_registry['update_project_status']['preprocess functions'], $position, 1);
      array_splice($theme_registry['update_project_status']['preprocess functions'], 1, 0, $out);
    }

    // Replace template location for update project status with our own
    // templates folder, so that we can alter the template.
    $theme_registry['update_project_status']['path'] = $module_path . '/templates';
  }
}

/**
 * Implements hook_preprocess_HOOK() for update-report.html.twig.
 */
function patchinfo_preprocess_update_report(&$variables) {
  $config = \Drupal::config('patchinfo.settings');

  // Provide excluded modules variable.
  $variables['excluded_modules'] = [
    '#theme' => 'patchinfo_excluded_modules',
    '#excluded_modules' => (array) $config
      ->get('exclude from update check'),
    '#attached' => [
      'library' => [
        'patchinfo/patchinfo',
      ],
    ],
  ];
}

/**
 * Implements hook_preprocess_HOOK() for update-project-status.html.twig.
 */
function patchinfo_preprocess_update_project_status(&$variables) {
  $patch_info = _patchinfo_get_info();
  $patches = _patchinfo_get_patches($patch_info, $variables['project']);

  // Provide patches variable.
  if (count($patches) > 0) {
    $variables['patches'] = [
      '#theme' => 'patchinfo_patches',
      '#patches' => $patches,
    ];
  }
}

/**
 * Prepares variables for patchinfo excluded modules templates.
 *
 * Default template: patchinfo-excluded-modules.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - excluded_modules: List of modules excluded from update check.
 */
function template_preprocess_patchinfo_excluded_modules(array &$variables) {
  if (count($variables['excluded_modules']) > 0) {

    // Prepare excluded modules for use in template.
    $variables['excluded_modules'] = [
      '#theme' => 'item_list',
      '#items' => $variables['excluded_modules'],
    ];

    // Provide URL to settings page of update module.
    $variables['settings_url'] = Url::fromRoute('update.settings', [], [
      'query' => \Drupal::destination()
        ->getAsArray(),
    ]);
  }
}

/**
 * Prepares variables for a patch listing of a module and its submodules.
 *
 * Default template: patchinfo-patches.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - patches: List of patch information for a module and its submodules.
 */
function template_preprocess_patchinfo_patches(array &$variables) {

  // Prepare patch list for use in template.
  $variables['patches'] = [
    '#theme' => 'item_list',
    '#items' => $variables['patches'],
    '#attached' => [
      'library' => [
        'patchinfo/patchinfo',
      ],
    ],
  ];
}

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_preprocess_update_project_status Implements hook_preprocess_HOOK() for update-project-status.html.twig.
patchinfo_preprocess_update_report Implements hook_preprocess_HOOK() for update-report.html.twig.
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_settings_form_submit Submission handler for extended update settings form.
template_preprocess_patchinfo_excluded_modules Prepares variables for patchinfo excluded modules templates.
template_preprocess_patchinfo_patches Prepares variables for a patch listing 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.