securityUpdates = [];
$security_advisories_composer_json = $this->fetchAdvisoryComposerJson();
$composer_lock_data = $this->loadSiteComposerLock();
$this->registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json);
if ($this->securityUpdates) {
// @todo Modernize.
drush_set_context('DRUSH_EXIT_CODE', DRUSH_FRAMEWORK_ERROR);
$result = new RowsOfFields($this->securityUpdates);
return $result;
} else {
$this->logger()->success("There are no outstanding security updates for Drupal projects.");
}
}
/**
* Emit suggested Composer command for security updates.
*
* @hook post-command pm:security
*/
public function suggestComposerCommand($result, CommandData $commandData)
{
if (!empty($this->securityUpdates)) {
$suggested_command = 'composer require ';
foreach ($this->securityUpdates as $package) {
$suggested_command .= $package['name'] . ':^' . $package['min-version'] . ' ';
}
$suggested_command .= '--update-with-dependencies';
$this->logger()->warning("One or more of your dependencies has an outstanding security update. Please apply update(s) immediately.");
$this->logger()->notice("Try running: $suggested_command");
$this->logger()->notice("If that fails due to a conflict then you must update one or more root dependencies.");
}
}
/**
* Fetches the generated composer.json from drupal-security-advisories.
*
* @return mixed
*
* @throws \Exception
*/
protected function fetchAdvisoryComposerJson()
{
try {
$response_body = file_get_contents('https://raw.githubusercontent.com/drupal-composer/drupal-security-advisories/8.x/composer.json');
} catch (Exception $e) {
throw new Exception("Unable to fetch drupal-security-advisories information.");
}
$security_advisories_composer_json = json_decode($response_body, true);
return $security_advisories_composer_json;
}
/**
* Loads the contents of the local Drupal application's composer.lock file.
*
* @return array
*
* @throws \Exception
*/
protected function loadSiteComposerLock()
{
$composer_root = Drush::bootstrapManager()->getComposerRoot();
$composer_lock_file_name = getenv('COMPOSER') ? str_replace(
'.json',
'',
getenv('COMPOSER')
) : 'composer';
$composer_lock_file_name .= '.lock';
$composer_lock_file_path = Path::join(
$composer_root,
$composer_lock_file_name
);
if (!file_exists($composer_lock_file_path)) {
throw new Exception("Cannot find $composer_lock_file_path!");
}
$composer_lock_contents = file_get_contents($composer_lock_file_path);
$composer_lock_data = json_decode($composer_lock_contents, true);
if (!array_key_exists('packages', $composer_lock_data)) {
throw new Exception("No packages were found in $composer_lock_file_path! Contents:\n $composer_lock_contents");
}
return $composer_lock_data;
}
/**
* Register all available security updates in $this->securityUpdates.
* @param array $composer_lock_data
* The contents of the local Drupal application's composer.lock file.
* @param array $security_advisories_composer_json
* The composer.json array from drupal-security-advisories.
*/
protected function registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json)
{
$both = $composer_lock_data['packages-dev'] + $composer_lock_data['packages'];
foreach ($both as $package) {
$name = $package['name'];
$this->registerPackageSecurityUpdates($security_advisories_composer_json, $name, $package);
}
}
/**
* Determines if update is available based on a conflict constraint.
*
* @param string $conflict_constraint
* The constraint for the conflicting, insecure package version.
* E.g., <1.0.0.
* @param array $package
* The package to be evaluated.
* @param string $name
* The human readable display name for the package.
*
* @return array
* An associative array containing name, version, and min-version keys.
*/
public static function determineUpdatesFromConstraint(
$conflict_constraint,
$package,
$name
) {
// Only parse constraints that follow pattern like "<1.0.0".
if (substr($conflict_constraint, 0, 1) == '<') {
$min_version = substr($conflict_constraint, 1);
if (Comparator::lessThan(
$package['version'],
$min_version
)) {
return [
'name' => $name,
'version' => $package['version'],
// Assume that conflict constraint of <1.0.0 indicates that
// 1.0.0 is the available, secure version.
'min-version' => $min_version,
];
}
} // Compare exact versions that are insecure.
elseif (preg_match(
'/^[[:digit:]](?![-*><=~ ])/',
$conflict_constraint
)) {
$exact_version = $conflict_constraint;
if (Comparator::equalTo(
$package['version'],
$exact_version
)) {
$version_parts = explode('.', $package['version']);
if (count($version_parts) == 3) {
$version_parts[2]++;
$min_version = implode('.', $version_parts);
return [
'name' => $name,
'version' => $package['version'],
// Assume that conflict constraint of 1.0.0 indicates that
// 1.0.1 is the available, secure version.
'min-version' => $min_version,
];
}
}
}
return [];
}
/**
* Registers available security updates for a given package.
*
* @param array $security_advisories_composer_json
* The composer.json array from drupal-security-advisories.
* @param string $name
* The human readable display name for the package.
* @param array $package
* The package to be evaluated.
*/
protected function registerPackageSecurityUpdates(
$security_advisories_composer_json,
$name,
$package
) {
if (empty($this->securityUpdates[$name]) &&
!empty($security_advisories_composer_json['conflict'][$name])) {
$conflict_constraints = explode(
',',
$security_advisories_composer_json['conflict'][$name]
);
foreach ($conflict_constraints as $conflict_constraint) {
$available_update = $this->determineUpdatesFromConstraint(
$conflict_constraint,
$package,
$name
);
if ($available_update) {
$this->securityUpdates[$name] = $available_update;
}
}
}
}
}