Commit 603cfb23 authored by Narendra Singh Rathore's avatar Narendra Singh Rathore Committed by Adam G-H
Browse files

Issue #3307611 by narendraR, srishtiiee, tedbow: Create a validator to add a...

Issue #3307611 by narendraR, srishtiiee, tedbow: Create a validator to add a warning if updated extensions have database updates
parent 7b581aa3
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -129,9 +129,7 @@ services:
  automatic_updates.validator.staged_database_updates:
    class: Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
    arguments:
      - '@package_manager.path_locator'
      - '@extension.list.module'
      - '@extension.list.theme'
      - '@package_manager.validator.staged_database_updates'
      - '@string_translation'
    tags:
      - { name: event_subscriber }
+5 −5
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ namespace Drupal\automatic_updates_extensions\Form;

use Drupal\package_manager\Exception\ApplyFailedException;
use Drupal\package_manager\ProjectInfo;
use Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator;
use Drupal\package_manager\Validator\StagedDBUpdateValidator;
use Drupal\automatic_updates_extensions\BatchProcessor;
use Drupal\automatic_updates\BatchProcessor as AutoUpdatesBatchProcessor;
use Drupal\automatic_updates_extensions\ExtensionUpdater;
@@ -52,7 +52,7 @@ final class UpdateReady extends FormBase {
  /**
   * The staged database update validator service.
   *
   * @var \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator
   * @var \Drupal\package_manager\Validator\StagedDBUpdateValidator
   */
  protected $stagedDatabaseUpdateValidator;

@@ -74,12 +74,12 @@ final class UpdateReady extends FormBase {
   *   The state service.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
   *   The module list service.
   * @param \Drupal\automatic_updates\Validator\StagedDatabaseUpdateValidator $staged_database_update_validator
   * @param \Drupal\package_manager\Validator\StagedDBUpdateValidator $staged_database_update_validator
   *   The staged database update validator service.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   */
  public function __construct(ExtensionUpdater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDatabaseUpdateValidator $staged_database_update_validator, RendererInterface $renderer) {
  public function __construct(ExtensionUpdater $updater, MessengerInterface $messenger, StateInterface $state, ModuleExtensionList $module_list, StagedDBUpdateValidator $staged_database_update_validator, RendererInterface $renderer) {
    $this->updater = $updater;
    $this->setMessenger($messenger);
    $this->state = $state;
@@ -104,7 +104,7 @@ final class UpdateReady extends FormBase {
      $container->get('messenger'),
      $container->get('state'),
      $container->get('extension.list.module'),
      $container->get('automatic_updates.validator.staged_database_updates'),
      $container->get('package_manager.validator.staged_database_updates'),
      $container->get('renderer')
    );
  }
+8 −0
Original line number Diff line number Diff line
@@ -123,6 +123,14 @@ services:
      - '@package_manager.path_locator'
    tags:
      - { name: event_subscriber }
  package_manager.validator.staged_database_updates:
    class: Drupal\package_manager\Validator\StagedDBUpdateValidator
    arguments:
      - '@package_manager.path_locator'
      - '@extension.list.module'
      - '@extension.list.theme'
    tags:
      - { name: event_subscriber }
  package_manager.test_site_excluder:
    class: Drupal\package_manager\PathExcluder\TestSiteExcluder
    arguments:
+185 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\package_manager\Validator;

use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\PathLocator;
use Drupal\package_manager\Stage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Flags a warning if there are database updates in a staged update.
 *
 * @internal
 *   This is an internal part of Package Manager and may be changed or removed
 *   at any time without warning. External code should not interact with this
 *   class.
 */
class StagedDBUpdateValidator implements EventSubscriberInterface {

  use StringTranslationTrait;

  /**
   * The path locator service.
   *
   * @var \Drupal\package_manager\PathLocator
   */
  protected $pathLocator;

  /**
   * The module list service.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected $moduleList;

  /**
   * The theme list service.
   *
   * @var \Drupal\Core\Extension\ThemeExtensionList
   */
  protected $themeList;

  /**
   * Constructs a StagedDBUpdateValidator object.
   *
   * @param \Drupal\package_manager\PathLocator $path_locator
   *   The path locator service.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
   *   The module list service.
   * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
   *   The theme list service.
   */
  public function __construct(PathLocator $path_locator, ModuleExtensionList $module_list, ThemeExtensionList $theme_list) {
    $this->pathLocator = $path_locator;
    $this->moduleList = $module_list;
    $this->themeList = $theme_list;
  }

  /**
   * Checks that the staged update does not have changes to its install files.
   *
   * @param \Drupal\package_manager\Event\StatusCheckEvent $event
   *   The event object.
   */
  public function checkForStagedDatabaseUpdates(StatusCheckEvent $event): void {
    $stage = $event->getStage();
    if ($stage->isAvailable()) {
      // No staged updates exist, therefore we don't need to run this check.
      return;
    }

    $extensions_with_updates = $this->getExtensionsWithDatabaseUpdates($stage);
    if ($extensions_with_updates) {
      $event->addWarning($extensions_with_updates, $this->t('Possible database updates have been detected in the following extensions.'));
    }
  }

  /**
   * Determines if a staged extension has changed update functions.
   *
   * @param \Drupal\package_manager\Stage $stage
   *   The updater which is controlling the update process.
   * @param \Drupal\Core\Extension\Extension $extension
   *   The extension to check.
   *
   * @return bool
   *   TRUE if the staged copy of the extension has changed update functions
   *   compared to the active copy, FALSE otherwise.
   *
   * @todo Use a more sophisticated method to detect changes in the staged
   *   extension. Right now, we just compare hashes of the .install and
   *   .post_update.php files in both copies of the given extension, but this
   *   will cause false positives for changes to comments, whitespace, or
   *   runtime code like requirements checks. It would be preferable to use a
   *   static analyzer to detect new or changed functions that are actually
   *   executed during an update. No matter what, this method must NEVER cause
   *   false negatives, since that could result in code which is incompatible
   *   with the current database schema being copied to the active directory.
   *
   * @see https://www.drupal.org/project/automatic_updates/issues/3253828
   */
  public function hasStagedUpdates(Stage $stage, Extension $extension): bool {
    $active_dir = $this->pathLocator->getProjectRoot();
    $stage_dir = $stage->getStageDirectory();

    $web_root = $this->pathLocator->getWebRoot();
    if ($web_root) {
      $active_dir .= DIRECTORY_SEPARATOR . $web_root;
      $stage_dir .= DIRECTORY_SEPARATOR . $web_root;
    }

    $active_hashes = $this->getHashes($active_dir, $extension);
    $staged_hashes = $this->getHashes($stage_dir, $extension);

    return $active_hashes !== $staged_hashes;
  }

  /**
   * Returns hashes of the .install and .post-update.php files for a module.
   *
   * @param string $root_dir
   *   The root directory of the Drupal code base.
   * @param \Drupal\Core\Extension\Extension $extension
   *   The module to check.
   *
   * @return string[]
   *   The hashes of the module's .install and .post_update.php files, in that
   *   order, if they exist. The array will be keyed by file extension.
   */
  protected function getHashes(string $root_dir, Extension $extension): array {
    $path = implode(DIRECTORY_SEPARATOR, [
      $root_dir,
      $extension->getPath(),
      $extension->getName(),
    ]);
    $hashes = [];

    foreach (['.install', '.post_update.php'] as $suffix) {
      $file = $path . $suffix;

      if (file_exists($file)) {
        $hashes[$suffix] = hash_file('sha256', $file);
      }
    }
    return $hashes;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      StatusCheckEvent::class => 'checkForStagedDatabaseUpdates',
    ];
  }

  /**
   * Gets extensions that have database updates.
   *
   * @param \Drupal\package_manager\Stage $stage
   *   The stage.
   *
   * @return string[]
   *   The names of the extensions that have possible database updates.
   */
  public function getExtensionsWithDatabaseUpdates(Stage $stage): array {
    $extensions_with_updates = [];
    // Check all installed extensions for database updates.
    $lists = [$this->moduleList, $this->themeList];
    foreach ($lists as $list) {
      foreach ($list->getAllInstalledInfo() as $name => $info) {
        if ($this->hasStagedUpdates($stage, $list->get($name))) {
          $extensions_with_updates[] = $info['name'];
        }
      }
    }

    return $extensions_with_updates;
  }

}
+5 −2
Original line number Diff line number Diff line
@@ -177,9 +177,12 @@ abstract class PackageManagerKernelTestBase extends KernelTestBase {
   *
   * @param \Drupal\package_manager\ValidationResult[] $expected_results
   *   The expected validation results.
   * @param \Drupal\package_manager\Stage|null $stage
   *   (optional) The stage to use to create the status check event. If none is
   *   provided a new stage will be created.
   */
  protected function assertStatusCheckResults(array $expected_results): void {
    $event = new StatusCheckEvent($this->createStage());
  protected function assertStatusCheckResults(array $expected_results, Stage $stage = NULL): void {
    $event = new StatusCheckEvent($stage ?? $this->createStage());
    $this->container->get('event_dispatcher')->dispatch($event);
    $this->assertValidationResultsEqual($expected_results, $event->getResults());
  }
Loading