From 39bf8c10fee9beddd1ad28d44880544c60e695b1 Mon Sep 17 00:00:00 2001
From: lucashedding <lucashedding@1463982.no-reply.drupal.org>
Date: Wed, 12 Jun 2019 08:25:52 -0500
Subject: [PATCH] Issue #3054002 by heddn, catch: Parse project name and
 version from composer.json

---
 composer.json                               |   6 +-
 drupalci.yml                                |   2 +
 src/ProjectInfoTrait.php                    | 108 +++++++++++++++
 src/ReadinessChecker/MissingProjectInfo.php |   9 +-
 src/ReadinessChecker/ModifiedFiles.php      |   2 +-
 src/Services/ModifiedFiles.php              |  39 +-----
 tests/src/Kernel/ProjectInfoTraitTest.php   | 146 ++++++++++++++++++++
 7 files changed, 268 insertions(+), 44 deletions(-)
 create mode 100644 src/ProjectInfoTrait.php
 create mode 100644 tests/src/Kernel/ProjectInfoTraitTest.php

diff --git a/composer.json b/composer.json
index 0b5f48fa1f..58ff78dd10 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,7 @@
   "type": "drupal-module",
   "description": "Drupal Automatic Updates",
   "keywords": ["Drupal"],
-  "license": "GPL-2.0+",
+  "license": "GPL-2.0-or-later",
   "homepage": "https://www.drupal.org/project/automatic_updates",
   "minimum-stability": "dev",
   "support": {
@@ -13,6 +13,10 @@
   "require": {
     "ext-json": "*",
     "composer/semver": "^1.0",
+    "ocramius/package-versions": "^1.4",
     "webflo/drupal-finder": "^1.1"
+  },
+  "require-dev": {
+    "drupal/ctools": "3.2.0"
   }
 }
diff --git a/drupalci.yml b/drupalci.yml
index 7286c3ba56..8fa0ea2178 100644
--- a/drupalci.yml
+++ b/drupalci.yml
@@ -10,6 +10,8 @@ build:
         sniff-all-files: true
         halt-on-fail: true
     testing:
+      container_command:
+        commands: "cd ${SOURCE_DIR} && sudo -u www-data composer require drupal/ctools:3.2.0 --prefer-source --optimize-autoloader"
       # run_tests task is executed several times in order of performance speeds.
       # halt-on-fail can be set on the run_tests tasks in order to fail fast.
       # suppress-deprecations is false in order to be alerted to usages of
diff --git a/src/ProjectInfoTrait.php b/src/ProjectInfoTrait.php
new file mode 100644
index 0000000000..1fde5d6b42
--- /dev/null
+++ b/src/ProjectInfoTrait.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+use PackageVersions\Versions;
+
+/**
+ * Provide a helper to get project info.
+ */
+trait ProjectInfoTrait {
+
+  /**
+   * Get the extension version.
+   *
+   * @param string $extension_name
+   *   The extension name.
+   * @param array $info
+   *   The extension's info.
+   *
+   * @return string|null
+   *   The version or NULL if undefined.
+   */
+  protected function getExtensionVersion($extension_name, array $info) {
+    if (isset($info['version']) && strpos($info['version'], '-dev') === FALSE) {
+      return $info['version'];
+    }
+    $composer_json = $this->getComposerJson($extension_name, $info);
+    $extension_name = isset($composer_json['name']) ? $composer_json['name'] : $extension_name;
+    try {
+      $version = Versions::getVersion($extension_name);
+      $version = $this->getSuffix($version, '@', $version);
+      // If we do not have a core compatibility tagged git branch, we're
+      // dealing with a  dev-master branch that cannot be updated in place.
+      return substr($version, 0, 3) === \Drupal::CORE_COMPATIBILITY ? $version : NULL;
+    }
+    catch (\OutOfBoundsException $exception) {
+      \Drupal::logger('automatic_updates')->error('Version cannot be located for @extension', ['@extension' => $extension_name]);
+    }
+  }
+
+  /**
+   * Get the extension's project name.
+   *
+   * @param string $extension_name
+   *   The extension name.
+   * @param array $info
+   *   The extension's info.
+   *
+   * @return string
+   *   The project name or fallback to extension name if project is undefined.
+   */
+  protected function getProjectName($extension_name, array $info) {
+    $project_name = $extension_name;
+    if (isset($info['project'])) {
+      $project_name = $info['project'];
+    }
+    elseif ($composer_json = $this->getComposerJson($extension_name, $info)) {
+      if (isset($composer_json['name'])) {
+        $project_name = $this->getSuffix($composer_json['name'], '/', $extension_name);
+      }
+    }
+    return $project_name;
+  }
+
+  /**
+   * Get string suffix.
+   *
+   * @param string $string
+   *   The string to parse.
+   * @param string $needle
+   *   The needle.
+   * @param string $default
+   *   The default value.
+   *
+   * @return string
+   *   The sub string.
+   */
+  protected function getSuffix($string, $needle, $default) {
+    $pos = strrpos($string, $needle);
+    return $pos === FALSE ? $default : substr($string, ++$pos);
+  }
+
+  /**
+   * Get the composer.json as a JSON array.
+   *
+   * @param string $extension_name
+   *   The extension name.
+   * @param array $info
+   *   The extension's info.
+   *
+   * @return array|null
+   *   The composer.json as an array or NULL.
+   */
+  protected function getComposerJson($extension_name, array $info) {
+    try {
+      if ($directory = drupal_get_path($info['type'], $extension_name)) {
+        $composer_json = $directory . DIRECTORY_SEPARATOR . 'composer.json';
+        if (file_exists($composer_json)) {
+          return json_decode(file_get_contents($composer_json), TRUE);
+        }
+      }
+    }
+    catch (\Throwable $exception) {
+      \Drupal::logger('automatic_updates')->error('Composer.json could not be located for @extension', ['@extension' => $extension_name]);
+    }
+  }
+
+}
diff --git a/src/ReadinessChecker/MissingProjectInfo.php b/src/ReadinessChecker/MissingProjectInfo.php
index e36d7df90c..a1599ee4c8 100644
--- a/src/ReadinessChecker/MissingProjectInfo.php
+++ b/src/ReadinessChecker/MissingProjectInfo.php
@@ -3,6 +3,7 @@
 namespace Drupal\automatic_updates\ReadinessChecker;
 
 use Drupal\automatic_updates\IgnoredPathsTrait;
+use Drupal\automatic_updates\ProjectInfoTrait;
 use Drupal\Core\Extension\ExtensionList;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use DrupalFinder\DrupalFinder;
@@ -12,6 +13,7 @@ use DrupalFinder\DrupalFinder;
  */
 class MissingProjectInfo extends Filesystem {
   use IgnoredPathsTrait;
+  use ProjectInfoTrait;
   use StringTranslationTrait;
 
   /**
@@ -74,11 +76,8 @@ class MissingProjectInfo extends Filesystem {
         if ($this->isIgnoredPath(drupal_get_path($info['type'], $extension_name))) {
           continue;
         }
-        if (empty($info['version'])) {
-          $messages[] = $this->t('The project "@extension" will not be updated because it is missing the "version" key in the @extension.info.yml file.', ['@extension' => $extension_name]);
-        }
-        if (empty($info['project'])) {
-          $messages[] = $this->t('The project "@extension" will not be updated because it is missing the "project" key in the @extension.info.yml file.', ['@extension' => $extension_name]);
+        if (!$this->getExtensionVersion($extension_name, $info)) {
+          $messages[] = $this->t('The project "@extension" can not be updated because its version is either undefined or a dev release.', ['@extension' => $extension_name]);
         }
       }
     }
diff --git a/src/ReadinessChecker/ModifiedFiles.php b/src/ReadinessChecker/ModifiedFiles.php
index 3991254369..2d2b516d62 100644
--- a/src/ReadinessChecker/ModifiedFiles.php
+++ b/src/ReadinessChecker/ModifiedFiles.php
@@ -83,7 +83,7 @@ class ModifiedFiles implements ReadinessCheckerInterface {
   protected function modifiedFilesCheck() {
     $messages = [];
     $extensions = [];
-    $extensions['drupal'] = $this->modules->get('system')->info;
+    $extensions['system'] = $this->modules->get('system')->info;
     foreach ($this->getExtensionsTypes() as $extension_type) {
       foreach ($this->getInfos($extension_type) as $extension_name => $info) {
         if (substr($this->getPath($extension_type, $extension_name), 0, 4) !== 'core') {
diff --git a/src/Services/ModifiedFiles.php b/src/Services/ModifiedFiles.php
index 7d2d13ba7f..41f5088508 100644
--- a/src/Services/ModifiedFiles.php
+++ b/src/Services/ModifiedFiles.php
@@ -3,6 +3,7 @@
 namespace Drupal\automatic_updates\Services;
 
 use Drupal\automatic_updates\IgnoredPathsTrait;
+use Drupal\automatic_updates\ProjectInfoTrait;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Url;
 use DrupalFinder\DrupalFinder;
@@ -16,6 +17,7 @@ use Psr\Log\LoggerInterface;
  */
 class ModifiedFiles implements ModifiedFilesInterface {
   use IgnoredPathsTrait;
+  use ProjectInfoTrait;
 
   /**
    * The logger.
@@ -173,43 +175,6 @@ class ModifiedFiles implements ModifiedFilesInterface {
     return Url::fromUri($uri . "/$project_name/$version/$hash_name")->toString();
   }
 
-  /**
-   * Get the extension version.
-   *
-   * @param string $extension_name
-   *   The extension name.
-   * @param array $info
-   *   The extension's info.
-   *
-   * @return string|null
-   *   The version or NULL if undefined.
-   */
-  protected function getExtensionVersion($extension_name, array $info) {
-    $version = isset($info['version']) ? $info['version'] : NULL;
-    // TODO: consider using ocramius/package-versions to discover the installed
-    // version from composer.lock.
-    // See https://www.drupal.org/project/automatic_updates/issues/3054002
-    return $version;
-  }
-
-  /**
-   * Get the extension's project name.
-   *
-   * @param string $extension_name
-   *   The extension name.
-   * @param array $info
-   *   The extension's info.
-   *
-   * @return string
-   *   The project name or fallback to extension name if project is undefined.
-   */
-  protected function getProjectName($extension_name, array $info) {
-    $project_name = isset($info['project']) ? $info['project'] : $extension_name;
-    // TODO: parse the composer.json for the name if it isn't set in info.
-    // See https://www.drupal.org/project/automatic_updates/issues/3054002.
-    return $project_name;
-  }
-
   /**
    * Get the hash file name.
    *
diff --git a/tests/src/Kernel/ProjectInfoTraitTest.php b/tests/src/Kernel/ProjectInfoTraitTest.php
new file mode 100644
index 0000000000..6c961db607
--- /dev/null
+++ b/tests/src/Kernel/ProjectInfoTraitTest.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Kernel;
+
+use Drupal\automatic_updates\ProjectInfoTrait;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\automatic_updates\ProjectInfoTrait
+ * @group automatic_updates
+ */
+class ProjectInfoTraitTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'automatic_updates',
+  ];
+
+  /**
+   * @covers ::getExtensionVersion
+   * @covers ::getProjectName
+   * @dataProvider providerInfos
+   */
+  public function testTrait($expected, $info, $extension_name) {
+    $class = new ProjectInfoTestClass();
+    $this->assertSame($expected['version'], $class->getExtensionVersion($extension_name, $info));
+    $this->assertSame($expected['project'], $class->getProjectName($extension_name, $info));
+  }
+
+  /**
+   * Data provider for testTrait.
+   */
+  public function providerInfos() {
+    $infos['node']['expected'] = [
+      'version' => NULL,
+      'project' => 'drupal',
+    ];
+    $infos['node']['info'] = [
+      'name' => 'Node',
+      'type' => 'module',
+      'description' => 'Allows content to be submitted to the site and displayed on pages.',
+      'package' => 'Core',
+      'version' => '8.8.x-dev',
+      'project' => 'drupal',
+      'core' => '8.x',
+      'configure' => 'entity.node_type.collection',
+      'dependencies' => ['drupal:text'],
+    ];
+    $infos['node']['extension_name'] = 'node';
+
+    $infos['update']['expected'] = [
+      'version' => NULL,
+      'project' => 'drupal/update',
+    ];
+    $infos['update']['info'] = [
+      'name' => 'Update manager',
+      'type' => 'module',
+      'description' => 'Checks for available updates, and can securely install or update modules and themes via a web interface.',
+      'package' => 'Core',
+      'core' => '8.x',
+      'configure' => 'update.settings',
+      'dependencies' => ['file'],
+    ];
+    $infos['update']['extension_name'] = 'drupal/update';
+
+    $infos['system']['expected'] = [
+      'version' => '8.8.0',
+      'project' => 'drupal',
+    ];
+    $infos['system']['info'] = [
+      'name' => 'System',
+      'type' => 'module',
+      'description' => 'Handles general site configuration for administrators.',
+      'package' => 'Core',
+      'version' => '8.8.0',
+      'project' => 'drupal',
+      'core' => '8.x',
+      'required' => 'true',
+      'configure' => 'system.admin_config_system',
+      'dependencies' => [],
+    ];
+    $infos['system']['extension_name'] = 'system';
+
+    $infos['automatic_updates']['expected'] = [
+      'version' => NULL,
+      'project' => 'automatic_updates',
+    ];
+    $infos['automatic_updates']['info'] = [
+      'name' => 'Automatic Updates',
+      'type' => 'module',
+      'description' => 'Display public service announcements and verify readiness for applying automatic updates to the site.',
+      'package' => 'Core',
+      'core' => '8.x',
+      'configure' => 'automatic_updates.settings',
+      'dependencies' => ['system', 'update'],
+    ];
+    $infos['automatic_updates']['extension_name'] = 'automatic_updates';
+
+    // TODO: Investigate switching to this project after stable release in
+    // https://www.drupal.org/project/automatic_updates/issues/3061229.
+    $infos['ctools']['expected'] = [
+      'version' => '8.x-3.2',
+      'project' => 'ctools',
+    ];
+    $infos['ctools']['info'] = [
+      'name' => 'Chaos tool suite',
+      'type' => 'module',
+      'description' => 'Provides a number of utility and helper APIs for Drupal developers and site builders.',
+      'package' => 'Core',
+      'core' => '8.x',
+      'dependencies' => ['system'],
+    ];
+    $infos['ctools']['extension_name'] = 'ctools';
+
+    return $infos;
+  }
+
+}
+
+/**
+ * Class ProjectInfoTestClass.
+ */
+class ProjectInfoTestClass {
+
+  use ProjectInfoTrait {
+    getExtensionVersion as getVersion;
+    getProjectName as getProject;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExtensionVersion($extension_name, array $info) {
+    return $this->getVersion($extension_name, $info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProjectName($extension_name, array $info) {
+    return $this->getProject($extension_name, $info);
+  }
+
+}
-- 
GitLab