Commit 1edf15f2 authored by catch's avatar catch
Browse files

Issue #3096648 by alexpott, Berdir, catch, tstoeckler: Add support for third...

Issue #3096648 by alexpott, Berdir, catch, tstoeckler: Add support for third party libraries in site specific and install profile specific libraries folders

(cherry picked from commit 8dad3340117346a06ce48c6925e1cee3d0fba05c)
parent 99133c01
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -1608,7 +1608,10 @@ services:
      - { name: needs_destruction }
  library.discovery.parser:
    class: Drupal\Core\Asset\LibraryDiscoveryParser
    arguments: ['@app.root', '@module_handler', '@theme.manager', '@stream_wrapper_manager']
    arguments: ['@app.root', '@module_handler', '@theme.manager', '@stream_wrapper_manager', '@library.libraries_directory_file_finder']
  library.libraries_directory_file_finder:
    class: Drupal\Core\Asset\LibrariesDirectoryFileFinder
    arguments: ['@app.root', '@site.path', '@extension.list.profile', '%install_profile%']
  library.dependency_resolver:
    class: Drupal\Core\Asset\LibraryDependencyResolver
    arguments: ['@library.discovery']
+100 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Asset;

use Drupal\Core\Extension\ProfileExtensionList;

/**
 * Finds files that are located in the supported 'libraries' directories.
 */
class LibrariesDirectoryFileFinder {

  /**
   * The app root.
   *
   * @var string
   */
  protected $root;

  /**
   * The site path.
   *
   * @var string
   */
  protected $sitePath;

  /**
   * The profile extension list.
   *
   * @var \Drupal\Core\Extension\ExtensionList
   */
  protected $profileExtensionList;

  /**
   * The install profile.
   *
   * @var string
   */
  protected $installProfile;

  /**
   * Constructs a new LibrariesDirectoryFileFinder instance.
   *
   * @param string $root
   *   The app root.
   * @param string $site_path
   *   The site path.
   * @param \Drupal\Core\Extension\ProfileExtensionList $profile_extension_list
   *   The profile extension list.
   * @param string $install_profile
   *   The install profile.
   */
  public function __construct($root, $site_path, ProfileExtensionList $profile_extension_list, $install_profile) {
    $this->root = $root;
    $this->sitePath = $site_path;
    $this->profileExtensionList = $profile_extension_list;
    $this->installProfile = $install_profile;
  }

  /**
   * Finds files that are located in the supported 'libraries' directories.
   *
   * It searches the following locations:
   * - A libraries directory in the current site directory, for example:
   *   sites/default/libraries.
   * - The root libraries directory.
   * - A libraries directory in the selected installation profile, for example:
   *   profiles/my_install_profile/libraries.
   * If the same library is present in multiple locations the first location
   * found will be used. The locations are searched in the order listed.
   *
   * @param string $path
   *   The path for the library file to find.
   *
   * @return string|false
   *   The real path to the library file relative to the root directory. If the
   *   library cannot be found then FALSE.
   */
  public function find($path) {
    // Search sites/<domain>/*.
    $directories[] = "{$this->sitePath}/libraries/";

    // Always search the root 'libraries' directory.
    $directories[] = 'libraries/';

    // Installation profiles can place libraries into a 'libraries' directory.
    if ($this->installProfile) {
      $profile_path = $this->profileExtensionList->getPath($this->installProfile);
      $directories[] = "$profile_path/libraries/";
    }

    foreach ($directories as $dir) {
      if (file_exists($this->root . '/' . $dir . $path)) {
        return $dir . $path;
      }
    }
    // The library has not been found.
    return FALSE;
  }

}
+32 −2
Original line number Diff line number Diff line
@@ -46,6 +46,13 @@ class LibraryDiscoveryParser {
   */
  protected $streamWrapperManager;

  /**
   * The libraries directory file finder.
   *
   * @var \Drupal\Core\Asset\LibrariesDirectoryFileFinder
   */
  protected $librariesDirectoryFileFinder;

  /**
   * Constructs a new LibraryDiscoveryParser instance.
   *
@@ -57,8 +64,10 @@ class LibraryDiscoveryParser {
   *   The theme manager.
   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
   *   The stream wrapper manager.
   * @param \Drupal\Core\Asset\LibrariesDirectoryFileFinder $libraries_directory_file_finder
   *   The libraries directory file finder.
   */
  public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, StreamWrapperManagerInterface $stream_wrapper_manager = NULL) {
  public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, StreamWrapperManagerInterface $stream_wrapper_manager = NULL, LibrariesDirectoryFileFinder $libraries_directory_file_finder = NULL) {
    $this->root = $root;
    $this->moduleHandler = $module_handler;
    $this->themeManager = $theme_manager;
@@ -67,6 +76,11 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Theme
      $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
    }
    $this->streamWrapperManager = $stream_wrapper_manager;
    if (!$libraries_directory_file_finder) {
      @trigger_error('Calling LibraryDiscoveryParser::__construct() without the $libraries_directory_file_finder argument is deprecated in drupal:8.9.0. The $libraries_directory_file_finder argument will be required in drupal:10.0.0. See https://www.drupal.org/node/3099614', E_USER_DEPRECATED);
      $libraries_directory_file_finder = \Drupal::service('library.libraries_directory_file_finder');
    }
    $this->librariesDirectoryFileFinder = $libraries_directory_file_finder;
  }

  /**
@@ -189,7 +203,15 @@ public function buildByExtension($extension) {
            if ($source[0] === '/') {
              // An absolute path maps to DRUPAL_ROOT / base_path().
              if ($source[1] !== '/') {
                $options['data'] = substr($source, 1);
                $source = substr($source, 1);
                // Non core provided libraries can be in multiple locations.
                if (strpos($source, 'libraries/') === 0) {
                  $path_to_source = $this->librariesDirectoryFileFinder->find(substr($source, 10));
                  if ($path_to_source) {
                    $source = $path_to_source;
                  }
                }
                $options['data'] = $source;
              }
              // A protocol-free URI (e.g., //cdn.com/example.js) is external.
              else {
@@ -278,6 +300,14 @@ public function buildByExtension($extension) {
   *   Just like with JavaScript files, each CSS file is the key of an object
   *   that can define specific attributes. The format of the file path is the
   *   same as for the JavaScript files.
   *   If the JavaScript or CSS file starts with /libraries/ the
   *   library.libraries_directory_file_finder service is used to find the files
   *   in the following locations:
   *   - A libraries directory in the current site directory, for example:
   *     sites/default/libraries.
   *   - The root libraries directory.
   *   - A libraries directory in the selected installation profile, for
   *     example: profiles/my_install_profile/libraries.
   * - dependencies: A list of libraries this library depends on.
   * - version: The library version. The string "VERSION" can be used to mean
   *   the current Drupal core version.
+88 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\Core\Asset;

use Drupal\Core\Asset\LibrariesDirectoryFileFinder;
use Drupal\Core\Extension\ProfileExtensionList;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;

/**
 * @coversDefaultClass \Drupal\Core\Asset\LibrariesDirectoryFileFinder
 * @group Asset
 */
class LibrariesDirectoryFileFinderTest extends UnitTestCase {

  /**
   * @covers ::find
   */
  public function testFind() {
    // Place a library file in all the possible locations.
    $structure = [
      'sites' => [
        'example.com' => [
          'libraries' => [
            'third_party_library' => [
              'css' => [
                'example.css' => '/*Some css*/',
              ],
            ],
          ],
        ],
      ],
      'libraries' => [
        'third_party_library' => [
          'css' => [
            'example.css' => '/*Some css*/',
          ],
        ],
      ],
      'profiles' => [
        'library_testing' => [
          'libraries' => [
            'third_party_library' => [
              'css' => [
                'example.css' => '/*Some css*/',
              ],
            ],
          ],
        ],
      ],
    ];
    vfsStream::setup('root', NULL, $structure);

    $extension_list = $this->prophesize(ProfileExtensionList::class);
    $extension_list->getPath('library_testing')->willReturn('profiles/library_testing');

    $finder = new LibrariesDirectoryFileFinder('vfs://root', 'sites/example.com', $extension_list->reveal(), 'library_testing');

    // The site specific location is the first location used.
    $path = $finder->find('third_party_library/css/example.css');
    $this->assertEquals('sites/example.com/libraries/third_party_library/css/example.css', $path);

    // After removing the site specific location the root libraries folder
    // should be used.
    unlink('vfs://root/sites/example.com/libraries/third_party_library/css/example.css');
    $path = $finder->find('third_party_library/css/example.css');
    $this->assertEquals('libraries/third_party_library/css/example.css', $path);

    // The profile's libraries is now the only remaining location.
    unlink('vfs://root/libraries/third_party_library/css/example.css');
    $path = $finder->find('third_party_library/css/example.css');
    $this->assertEquals('profiles/library_testing/libraries/third_party_library/css/example.css', $path);

    // If the libraries file cannot be found FALSE is returned.
    unlink('vfs://root/profiles/library_testing/libraries/third_party_library/css/example.css');
    $this->assertFalse($finder->find('third_party_library/css/example.css'));

    // Test finding by the directory only. As all the directories still we'll
    // find the first location.
    $path = $finder->find('third_party_library');
    $this->assertEquals('sites/example.com/libraries/third_party_library', $path);
  }

}

if (!defined('DRUPAL_MINIMUM_PHP')) {
  define('DRUPAL_MINIMUM_PHP', '7.0.8');
}
+68 −1
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Asset\LibrariesDirectoryFileFinder;
use Drupal\Core\Asset\LibraryDiscoveryParser;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Tests\UnitTestCase;
@@ -62,6 +63,13 @@ class LibraryDiscoveryParserTest extends UnitTestCase {
   */
  protected $streamWrapperManager;

  /**
   * The mocked libraries directory file finder.
   *
   * @var \Drupal\Core\Asset\LibrariesDirectoryFileFinder||\PHPUnit\Framework\MockObject\MockObject
   */
  protected $librariesDirectoryFileFinder;

  /**
   * {@inheritdoc}
   */
@@ -80,7 +88,8 @@ protected function setUp() {
      ->method('getActiveTheme')
      ->willReturn($mock_active_theme);
    $this->streamWrapperManager = $this->createMock(StreamWrapperManagerInterface::class);
    $this->libraryDiscoveryParser = new TestLibraryDiscoveryParser($this->root, $this->moduleHandler, $this->themeManager, $this->streamWrapperManager);
    $this->librariesDirectoryFileFinder = $this->createMock(LibrariesDirectoryFileFinder::class);
    $this->libraryDiscoveryParser = new TestLibraryDiscoveryParser($this->root, $this->moduleHandler, $this->themeManager, $this->streamWrapperManager, $this->librariesDirectoryFileFinder);
  }

  /**
@@ -577,6 +586,64 @@ public function providerTestCssAssert() {
    ];
  }

  /**
   * @covers ::buildByExtension
   */
  public function testNonCoreLibrariesFound() {
    $this->moduleHandler->expects($this->atLeastOnce())
      ->method('moduleExists')
      ->with('example_contrib_module')
      ->will($this->returnValue(TRUE));

    $path = __DIR__ . '/library_test_files';
    $path = substr($path, strlen($this->root) + 1);
    $this->libraryDiscoveryParser->setPaths('module', 'example_contrib_module', $path);

    $this->librariesDirectoryFileFinder->expects($this->once())
      ->method('find')
      ->with('third_party_library/css/example.css')
      ->will($this->returnValue('sites/example.com/libraries/third_party_library/css/example.css'));

    $libraries = $this->libraryDiscoveryParser->buildByExtension('example_contrib_module');
    $library = $libraries['third_party_library'];

    $this->assertCount(0, $library['js']);
    $this->assertCount(1, $library['css']);
    $this->assertCount(0, $library['dependencies']);
    // The location is determined by the libraries directory file finder.
    $this->assertEquals('sites/example.com/libraries/third_party_library/css/example.css', $library['css'][0]['data']);
  }

  /**
   * @covers ::buildByExtension
   */
  public function testNonCoreLibrariesNotFound() {
    $this->moduleHandler->expects($this->atLeastOnce())
      ->method('moduleExists')
      ->with('example_contrib_module')
      ->will($this->returnValue(TRUE));

    $path = __DIR__ . '/library_test_files';
    $path = substr($path, strlen($this->root) + 1);
    $this->libraryDiscoveryParser->setPaths('module', 'example_contrib_module', $path);
    $this->libraryDiscoveryParser->setPaths('profile', 'library_testing', 'profiles/library_testing');

    $this->librariesDirectoryFileFinder->expects($this->once())
      ->method('find')
      ->with('third_party_library/css/example.css')
      ->will($this->returnValue(FALSE));

    $libraries = $this->libraryDiscoveryParser->buildByExtension('example_contrib_module');
    $library = $libraries['third_party_library'];

    $this->assertCount(0, $library['js']);
    $this->assertCount(1, $library['css']);
    $this->assertCount(0, $library['dependencies']);
    // The location will be the same as provided in the library definition even
    // though it does not exist.
    $this->assertEquals('libraries/third_party_library/css/example.css', $library['css'][0]['data']);
  }

}

/**
Loading