From 769adedd36c2ae8a76d4ebae4ffcb8ada99b75ab Mon Sep 17 00:00:00 2001
From: Ryan Jacobs <rjacobs@422459.no-reply.drupal.org>
Date: Mon, 30 Jan 2017 21:39:13 -0600
Subject: [PATCH] Issue #2818689 by rjacobs, tstoeckler: Add ability to scan
 multiple stream wrappers when locting library source

Squashed commit of the following:

commit aed963858529e94dd7c317794a428ec926d9e080
Author: Ryan Jacobs <rjacobs@422459.no-reply.drupal.org>
Date:   Sat Dec 31 17:27:21 2016 -0600

    Issue #2818689: Add GlobalLocator integration test.

commit 6e0225737795ea0716202e246c0a037ec15ba854
Author: Ryan Jacobs <rjacobs@422459.no-reply.drupal.org>
Date:   Wed Dec 28 23:01:55 2016 -0600

    Issue #2818689: Add global locator.

commit 2af9f79423221b79d11de500db4607a229bc2526
Author: Ryan Jacobs <rjacobs@422459.no-reply.drupal.org>
Date:   Sun Dec 18 22:07:12 2016 -0600

    Issue #2818689: Add ChainLocator and refactor StreamLocator into UriLocator.
---
 config/install/libraries.settings.yml         |  1 +
 config/schema/libraries.schema.yml            | 23 ++++++
 src/ExternalLibrary/Asset/AssetLibrary.php    |  6 +-
 .../Asset/MultipleAssetLibrary.php            |  6 +-
 .../PhpFile/PhpFileLibrary.php                |  3 +-
 src/ExternalLibrary/Type/LibraryTypeBase.php  |  6 ++
 src/Plugin/libraries/Locator/ChainLocator.php | 53 +++++++++++++
 .../libraries/Locator/GlobalLocator.php       | 76 +++++++++++++++++++
 .../{StreamLocator.php => UriLocator.php}     | 41 +++++-----
 .../ExternalLibrary/GlobalLocatorTest.php     | 53 +++++++++++++
 10 files changed, 244 insertions(+), 24 deletions(-)
 create mode 100644 src/Plugin/libraries/Locator/ChainLocator.php
 create mode 100644 src/Plugin/libraries/Locator/GlobalLocator.php
 rename src/Plugin/libraries/Locator/{StreamLocator.php => UriLocator.php} (73%)
 create mode 100644 tests/src/Kernel/ExternalLibrary/GlobalLocatorTest.php

diff --git a/config/install/libraries.settings.yml b/config/install/libraries.settings.yml
index 0c0aaf2..0981884 100644
--- a/config/install/libraries.settings.yml
+++ b/config/install/libraries.settings.yml
@@ -7,3 +7,4 @@ definition:
     enable: TRUE
     urls:
       - 'http://cgit.drupalcode.org/libraries_registry/plain/registry/8'
+global_locators: []
\ No newline at end of file
diff --git a/config/schema/libraries.schema.yml b/config/schema/libraries.schema.yml
index 35af8cb..72c4c52 100644
--- a/config/schema/libraries.schema.yml
+++ b/config/schema/libraries.schema.yml
@@ -1,5 +1,6 @@
 # Configuration schema for the Libraries API module.
 
+# Base configuration schema
 libraries.settings:
   type: config_object
   label: 'Libraries API settings'
@@ -28,3 +29,25 @@ libraries.settings:
               sequence:
                 type: uri
                 label: 'The URL of a remote library registry'
+    global_locators:
+      type: sequence
+      title: 'Global library locators'
+      sequence:
+        type: mapping
+        title: 'Global locator plugins'
+        mapping:
+          id:
+            type: string
+            title: 'The locator plugin id'
+          configuration:
+            type: libraries.locator.[%parent.id]
+            title: 'The plugin configuration'
+
+# Dynamic locator plugin schema
+libraries.locator.uri:
+  type: mapping
+  label: 'URI locator configuration'
+  mapping:
+    uri:
+      type: uri
+      label: 'The locator URI'
\ No newline at end of file
diff --git a/src/ExternalLibrary/Asset/AssetLibrary.php b/src/ExternalLibrary/Asset/AssetLibrary.php
index e447158..50a3186 100644
--- a/src/ExternalLibrary/Asset/AssetLibrary.php
+++ b/src/ExternalLibrary/Asset/AssetLibrary.php
@@ -118,7 +118,11 @@ class AssetLibrary extends LibraryBase implements
    * @see \Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface::getLocator()
    */
   public function getLocator(FactoryInterface $locator_factory) {
-    return $locator_factory->createInstance('stream', ['scheme' => 'asset']);
+    // @todo Consider consolidating the stream wrappers used here. For now we
+    // allow asset libs to live almost anywhere.
+    return $locator_factory->createInstance('chain')
+      ->addLocator($locator_factory->createInstance('uri', ['uri' => 'asset://']))
+      ->addLocator($locator_factory->createInstance('uri', ['uri' => 'php-file://']));
   }
 
 }
diff --git a/src/ExternalLibrary/Asset/MultipleAssetLibrary.php b/src/ExternalLibrary/Asset/MultipleAssetLibrary.php
index 0e42447..f2c3d3c 100644
--- a/src/ExternalLibrary/Asset/MultipleAssetLibrary.php
+++ b/src/ExternalLibrary/Asset/MultipleAssetLibrary.php
@@ -113,7 +113,11 @@ class MultipleAssetLibrary extends LibraryBase implements
    * @see \Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface::getLocator()
    */
   public function getLocator(FactoryInterface $locator_factory) {
-    return $locator_factory->createInstance('stream', ['scheme' => 'asset']);
+    // @todo Consider consolidating the stream wrappers used here. For now we
+    // allow asset libs to live almost anywhere.
+    return $locator_factory->createInstance('chain')
+      ->addLocator($locator_factory->createInstance('uri', ['uri' => 'asset://']))
+      ->addLocator($locator_factory->createInstance('uri', ['uri' => 'php-file://']));
   }
 
 }
diff --git a/src/ExternalLibrary/PhpFile/PhpFileLibrary.php b/src/ExternalLibrary/PhpFile/PhpFileLibrary.php
index 6134849..bf3c683 100644
--- a/src/ExternalLibrary/PhpFile/PhpFileLibrary.php
+++ b/src/ExternalLibrary/PhpFile/PhpFileLibrary.php
@@ -66,7 +66,8 @@ class PhpFileLibrary extends LibraryBase implements PhpFileLibraryInterface {
    * {@inheritdoc}
    */
   public function getLocator(FactoryInterface $locator_factory) {
-    return $locator_factory->createInstance('stream', ['scheme' => 'php-file']);
+    // @todo Consider refining the stream wrapper used here.
+    return $locator_factory->createInstance('uri', ['uri' => 'php-file://']);
   }
 
 }
diff --git a/src/ExternalLibrary/Type/LibraryTypeBase.php b/src/ExternalLibrary/Type/LibraryTypeBase.php
index a21ef04..08cd61a 100644
--- a/src/ExternalLibrary/Type/LibraryTypeBase.php
+++ b/src/ExternalLibrary/Type/LibraryTypeBase.php
@@ -68,6 +68,12 @@ abstract class LibraryTypeBase implements
   public function onLibraryCreate(LibraryInterface $library) {
     if ($library instanceof LocalLibraryInterface) {
       $library->getLocator($this->locatorFactory)->locate($library);
+      // Fallback on global locators.
+      // @todo Consider if global locators should be checked as a fallback or as
+      // the primary locator source.
+      if (!$library->isInstalled()) {
+        $this->locatorFactory->createInstance('global')->locate($library);
+      }
     }
     if ($library instanceof VersionedLibraryInterface) {
       $library->getVersionDetector($this->detectorFactory)->detectVersion($library);
diff --git a/src/Plugin/libraries/Locator/ChainLocator.php b/src/Plugin/libraries/Locator/ChainLocator.php
new file mode 100644
index 0000000..4e0acff
--- /dev/null
+++ b/src/Plugin/libraries/Locator/ChainLocator.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\libraries\Plugin\libraries\Locator;
+
+use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
+use Drupal\libraries\ExternalLibrary\Local\LocatorInterface;
+
+/**
+ * Provides a locator utilizing a chain of other individual locators.
+ *
+ * @Locator("chain")
+ *
+ * @see \Drupal\libraries\ExternalLibrary\Local\LocatorInterface
+ */
+class ChainLocator implements LocatorInterface {
+
+  /**
+   * The locators to check.
+   *
+   * @var \Drupal\libraries\ExternalLibrary\Local\LocatorInterface[]
+   */
+  protected $locators = [];
+
+  /**
+   * Add a locator to the chain.
+   *
+   * @param \Drupal\libraries\ExternalLibrary\Local\LocatorInterface $locator
+   *   A locator to add to the chain.
+   */
+  public function addLocator(LocatorInterface $locator) {
+    $this->locators[] = $locator;
+    return $this;
+  }
+
+  /**
+   * Locates a library.
+   *
+   * @param \Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface $library
+   *   The library to locate.
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Local\LocatorInterface::locate()
+   */
+  public function locate(LocalLibraryInterface $library) {
+    foreach ($this->locators as $locator) {
+      $locator->locate($library);
+      if ($library->isInstalled()) {
+        return;
+      }
+    }
+    $library->setUninstalled();
+  }
+
+}
diff --git a/src/Plugin/libraries/Locator/GlobalLocator.php b/src/Plugin/libraries/Locator/GlobalLocator.php
new file mode 100644
index 0000000..3501338
--- /dev/null
+++ b/src/Plugin/libraries/Locator/GlobalLocator.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\libraries\Plugin\libraries\Locator;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Component\Plugin\Factory\FactoryInterface;
+use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
+use Drupal\libraries\ExternalLibrary\Local\LocatorInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a locator based on global configuration.
+ *
+ * @Locator("global")
+ *
+ * @see \Drupal\libraries\ExternalLibrary\Local\LocatorInterface
+ */
+class GlobalLocator implements LocatorInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The Drupal config factory service.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The locator factory.
+   *
+   * @var \Drupal\Component\Plugin\Factory\FactoryInterface
+   */
+  protected $locatorFactory;
+
+  /**
+   * Constructs a global locator.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The Drupal config factory service.
+   * @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
+   *   The locator factory.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, FactoryInterface $locator_factory) {
+    $this->configFactory = $config_factory;
+    $this->locatorFactory = $locator_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.libraries.locator')
+    );
+  }
+
+  /**
+   * Locates a library.
+   *
+   * @param \Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface $library
+   *   The library to locate.
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Local\LocatorInterface::locate()
+   */
+  public function locate(LocalLibraryInterface $library) {
+    foreach ($this->configFactory->get('libraries.settings')->get('global_locators') as $locator) {
+      $this->locatorFactory->createInstance($locator['id'], $locator['configuration'])->locate($library);
+      if ($library->isInstalled()) {
+        return;
+      }
+    }
+    $library->setUninstalled();
+  }
+
+}
diff --git a/src/Plugin/libraries/Locator/StreamLocator.php b/src/Plugin/libraries/Locator/UriLocator.php
similarity index 73%
rename from src/Plugin/libraries/Locator/StreamLocator.php
rename to src/Plugin/libraries/Locator/UriLocator.php
index 1873176..348be0c 100644
--- a/src/Plugin/libraries/Locator/StreamLocator.php
+++ b/src/Plugin/libraries/Locator/UriLocator.php
@@ -10,7 +10,7 @@ use Drupal\libraries\Plugin\MissingPluginConfigurationException;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Provides a locator utilizing a stream wrapper.
+ * Provides a locator utilizing a URI.
  *
  * It makes the following assumptions:
  * - The library files can be accessed using a specified stream.
@@ -19,11 +19,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  * - The first component of the file URIs are the library IDs (i.e. file URIs
  *   are of the form: scheme://library-id/path/to/file/filename).
  *
- * @Locator("stream")
+ * @Locator("uri")
  *
  * @see \Drupal\libraries\ExternalLibrary\Local\LocatorInterface
  */
-class StreamLocator implements LocatorInterface, ContainerFactoryPluginInterface {
+class UriLocator implements LocatorInterface, ContainerFactoryPluginInterface {
 
   /**
    * The stream wrapper manager.
@@ -33,33 +33,33 @@ class StreamLocator implements LocatorInterface, ContainerFactoryPluginInterface
   protected $streamWrapperManager;
 
   /**
-   * The scheme of the stream wrapper.
+   * The URI to check.
    *
    * @var string
    */
-  protected $scheme;
+  protected $uri;
 
   /**
-   * Constructs a stream locator.
+   * Constructs a URI locator.
    *
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
    *   The stream wrapper manager.
-   * @param string $scheme
-   *   The scheme of the stream wrapper.
+   * @param string $uri
+   *   The URI to check.
    */
-  public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, $scheme) {
+  public function __construct(StreamWrapperManagerInterface $stream_wrapper_manager, $uri) {
     $this->streamWrapperManager = $stream_wrapper_manager;
-    $this->scheme = (string) $scheme;
+    $this->uri = (string) $uri;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    if (!isset($configuration['scheme'])) {
-      throw new MissingPluginConfigurationException($plugin_id, $plugin_definition, $configuration, 'scheme');
+    if (!isset($configuration['uri'])) {
+      throw new MissingPluginConfigurationException($plugin_id, $plugin_definition, $configuration, 'uri');
     }
-    return new static($container->get('stream_wrapper_manager'), $configuration['scheme']);
+    return new static($container->get('stream_wrapper_manager'), $configuration['uri']);
   }
 
   /**
@@ -72,19 +72,18 @@ class StreamLocator implements LocatorInterface, ContainerFactoryPluginInterface
    */
   public function locate(LocalLibraryInterface $library) {
     /** @var \Drupal\Core\StreamWrapper\LocalStream $stream_wrapper */
-    $stream_wrapper = $this->streamWrapperManager->getViaScheme($this->scheme);
+    $stream_wrapper = $this->streamWrapperManager->getViaUri($this->uri);
     assert('$stream_wrapper instanceof \Drupal\Core\StreamWrapper\LocalStream');
     // Calling LocalStream::getDirectoryPath() explicitly avoids the realpath()
     // usage in LocalStream::getLocalPath(), which breaks if Libraries API is
     // symbolically linked into the Drupal installation.
-    $path = $stream_wrapper->getDirectoryPath() . '/' . $library->getId();
-
-    if (is_dir($path) && is_readable($path)) {
-      $library->setLocalPath($path);
-    }
-    else {
-      $library->setUninstalled();
+    list($scheme, $target) = explode('://', $this->uri, 2);
+    $base_path = str_replace('//', '/', $stream_wrapper->getDirectoryPath() . '/' . $target . '/' . $library->getId());
+    if (is_dir($base_path) && is_readable($base_path)) {
+      $library->setLocalPath($base_path);
+      return;
     }
+    $library->setUninstalled();
   }
 
 }
diff --git a/tests/src/Kernel/ExternalLibrary/GlobalLocatorTest.php b/tests/src/Kernel/ExternalLibrary/GlobalLocatorTest.php
new file mode 100644
index 0000000..84eb86e
--- /dev/null
+++ b/tests/src/Kernel/ExternalLibrary/GlobalLocatorTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\libraries\Kernel\ExternalLibrary;
+
+use Drupal\Tests\libraries\Kernel\ExternalLibrary\TestLibraryFilesStream;
+use Drupal\Tests\libraries\Kernel\LibraryTypeKernelTestBase;
+
+/**
+ * Tests that a global locator can be properly used to load a libraries.
+ *
+ * @group libraries
+ */
+class GlobalLocatorTest extends LibraryTypeKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Assign our test stream (which points to the test php lib) to the asset
+    // scheme. This gives us a scheme to work with in the test that is not
+    // used to locate a php lib by default.
+    $this->container->set('stream_wrapper.asset_libraries', new TestLibraryFilesStream(
+      $this->container->get('module_handler'),
+      $this->container->get('string_translation'),
+      'libraries'
+    ));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getLibraryTypeId() {
+    return 'php_file';
+  }
+
+  /**
+   * Tests that the library is located via the global loactor.
+   */
+  public function testGlobalLocator() {
+    // By default the library will not be locatable (control assertion) until we
+    // add the asset stream to the global loctors conf list.
+    $library = $this->getLibrary();
+    $this->assertFalse($library->isInstalled());
+    $config_factory = $this->container->get('config.factory');
+    $config_factory->getEditable('libraries.settings')
+      ->set('global_locators', [['id' => 'uri', 'configuration' => ['uri' => 'asset://']]])
+      ->save();
+    $library = $this->getLibrary();
+    $this->assertTrue($library->isInstalled());
+  }
+
+}
-- 
GitLab