SandboxDatabaseUpdatesValidator.php
Namespace
Drupal\package_manager\ValidatorFile
-
core/
modules/ package_manager/ src/ Validator/ SandboxDatabaseUpdatesValidator.php
View source
<?php
declare (strict_types=1);
namespace Drupal\package_manager\Validator;
use Drupal\Component\Assertion\Inspector;
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 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 SandboxDatabaseUpdatesValidator implements EventSubscriberInterface {
use StringTranslationTrait;
public function __construct(PathLocator $pathLocator, ModuleExtensionList $moduleList, ThemeExtensionList $themeList) {
}
/**
* 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 {
if (!$event->sandboxManager
->sandboxDirectoryExists()) {
return;
}
$stage_dir = $event->sandboxManager
->getSandboxDirectory();
$extensions_with_updates = $this->getExtensionsWithDatabaseUpdates($stage_dir);
if ($extensions_with_updates) {
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
$extensions_with_updates = array_map($this->t(...), $extensions_with_updates);
$event->addWarning($extensions_with_updates, $this->t('Database updates have been detected in the following extensions.'));
}
}
/**
* Determines if a staged extension has changed update functions.
*
* @param string $stage_dir
* The path of the stage directory.
* @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 In https://drupal.org/i/3253828 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.
*/
public function hasStagedUpdates(string $stage_dir, Extension $extension) : bool {
$active_dir = $this->pathLocator
->getProjectRoot();
$web_root = $this->pathLocator
->getWebRoot();
if ($web_root) {
$active_dir .= DIRECTORY_SEPARATOR . $web_root;
$stage_dir .= DIRECTORY_SEPARATOR . $web_root;
}
$active_functions = $this->getUpdateFunctions($active_dir, $extension);
$staged_functions = $this->getUpdateFunctions($stage_dir, $extension);
return (bool) array_diff($staged_functions, $active_functions);
}
/**
* Returns a list of all update functions for a module.
*
* This method only exists because the API in core that scans for available
* updates can only examine the active (running) code base, but we need to be
* able to scan the staged code base as well to compare it against the active
* one.
*
* @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 names of the update functions in the module's .install and
* .post_update.php files.
*/
private function getUpdateFunctions(string $root_dir, Extension $extension) : array {
$name = $extension->getName();
$path = implode(DIRECTORY_SEPARATOR, [
$root_dir,
$extension->getPath(),
$name,
]);
$function_names = [];
$patterns = [
'.install' => '/^' . $name . '_update_[0-9]+$/i',
'.post_update.php' => '/^' . $name . '_post_update_.+$/i',
];
foreach ($patterns as $suffix => $pattern) {
$file = $path . $suffix;
if (!file_exists($file)) {
continue;
}
// Parse the file and scan for named functions which match the pattern.
$code = file_get_contents($file);
$tokens = token_get_all($code);
for ($i = 0; $i < count($tokens); $i++) {
$chunk = array_slice($tokens, $i, 3);
if ($this->tokensMatchFunctionNamePattern($chunk, $pattern)) {
$function_names[] = $chunk[2][1];
}
}
}
return $function_names;
}
/**
* Determines if a set of tokens contain a function name matching a pattern.
*
* @param array[] $tokens
* A set of three tokens, part of a stream returned by token_get_all().
* @param string $pattern
* If the tokens declare a named function, a regular expression to test the
* function name against.
*
* @return bool
* TRUE if the given tokens declare a function whose name matches the given
* pattern; FALSE otherwise.
*
* @see token_get_all()
*/
private function tokensMatchFunctionNamePattern(array $tokens, string $pattern) : bool {
if (count($tokens) !== 3 || !Inspector::assertAllStrictArrays($tokens)) {
return FALSE;
}
// A named function declaration will always be a T_FUNCTION (the word
// `function`), followed by T_WHITESPACE (or the code would be syntactically
// invalid), followed by a T_STRING (the function name). This will ignore
// anonymous functions, but match class methods (although class methods are
// highly unlikely to match the naming patterns of update hooks).
$names = array_map('token_name', array_column($tokens, 0));
if ($names === [
'T_FUNCTION',
'T_WHITESPACE',
'T_STRING',
]) {
return (bool) preg_match($pattern, $tokens[2][1]);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() : array {
return [
StatusCheckEvent::class => 'checkForStagedDatabaseUpdates',
];
}
/**
* Gets extensions that have database updates in the stage directory.
*
* @param string $stage_dir
* The path of the stage directory.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
* The names of the extensions that have database updates.
*/
public function getExtensionsWithDatabaseUpdates(string $stage_dir) : 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_dir, $list->get($name))) {
$extensions_with_updates[] = $info['name'];
}
}
}
return $extensions_with_updates;
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
SandboxDatabaseUpdatesValidator | Flags a warning if there are database updates in a staged update. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.