5 * External library handling for Drupal modules.
8 use Drupal\Core\DrupalKernel;
9 use Drupal\Core\Extension\ModuleHandler;
10 use Drupal\libraries\ExternalLibrary\Asset\AttachableAssetLibraryRegistrationInterface;
11 use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorInterface;
12 use Drupal\libraries\ExternalLibrary\Utility\LibraryIdAccessorInterface;
13 use Symfony\Component\Yaml\Parser;
16 * Implements hook_library_info_build().
18 * Register external asset libraries with Drupal core's library APIs.
20 function libraries_library_info_build() {
21 /** @var \Drupal\libraries\ExternalLibrary\LibraryManagerInterface $library_manager */
22 $library_manager = \Drupal::service('libraries.manager');
23 $attachable_libraries = [];
24 $libraries_with_errors = [];
25 foreach ($library_manager->getRequiredLibraryIds() as $external_library_id) {
27 $external_library = $library_manager->getLibrary($external_library_id);
28 $library_type = $external_library->getType();
29 if ($library_type instanceof AttachableAssetLibraryRegistrationInterface) {
30 $attachable_libraries += $library_type->getAttachableAssetLibraries($external_library, $library_manager);
33 catch (\Exception $e) {
34 // Library-specific exceptions should not be allowed to kill the rest of
35 // the build process, but should be logged.
36 if ($e instanceof LibraryIdAccessorInterface || $e instanceof LibraryAccessorInterface) {
37 $libraries_with_errors[] = $external_library_id;
38 watchdog_exception('libraries', $e);
41 // Re-throw exceptions that are not library-specific.
46 // If we had library specific errors also log an informative message to
47 // tell admins that detection will not be run again without a cache clear.
48 if ($libraries_with_errors) {
49 \Drupal::logger('libraries')->error('The following external libraries could not successfully be registered with Drupal core: @libs. See earlier log entries for more details. Once these issues are addressed please be sure to clear your Drupal library cache to ensure external library detection is run again.', ['@libs' => implode(',', $libraries_with_errors)]);
51 return $attachable_libraries;
55 * Gets the path of a library.
58 * The machine name of a library to return the path for.
60 * Whether to prefix the resulting path with base_path().
63 * The path to the specified library or FALSE if the library wasn't found.
67 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
68 * new library load and managment concepts described at:
69 * https://www.drupal.org/node/2170763
71 function libraries_get_path($name, $base_path = FALSE) {
72 $libraries = &drupal_static(__FUNCTION__);
74 if (!isset($libraries)) {
75 $libraries = libraries_get_libraries();
78 $path = ($base_path ? base_path() : '');
79 if (!isset($libraries[$name])) {
83 $path .= $libraries[$name];
90 * Returns an array of library directories.
92 * Returns an array of library directories from the all-sites directory
93 * (i.e. sites/all/libraries/), the profiles directory, and site-specific
94 * directory (i.e. sites/somesite/libraries/). The returned array will be keyed
95 * by the library name. Site-specific libraries are prioritized over libraries
96 * in the default directories. That is, if a library with the same name appears
97 * in both the site-wide directory and site-specific directory, only the
98 * site-specific version will be listed.
101 * A list of library directories.
105 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
106 * new library load and managment concepts described at:
107 * https://www.drupal.org/node/2170763
109 function libraries_get_libraries() {
110 $searchdir = array();
111 $config = DrupalKernel::findSitePath(\Drupal::request());
113 // @todo core/libraries
115 // Similar to 'modules' and 'themes' directories inside an installation
116 // profile, installation profiles may want to place libraries into a
117 // 'libraries' directory.
118 if ($profile = drupal_get_profile()) {
119 $profile_path = drupal_get_path('profile', $profile);
120 $searchdir[] = "$profile_path/libraries";
123 // Search sites/all/libraries for backwards-compatibility.
124 $searchdir[] = 'sites/all/libraries';
126 // Always search the root 'libraries' directory.
127 $searchdir[] = 'libraries';
129 // Also search sites/<domain>/*.
130 $searchdir[] = "$config/libraries";
132 // Retrieve list of directories.
133 $directories = array();
134 $nomask = array('CVS');
135 foreach ($searchdir as $dir) {
136 if (is_dir($dir) && $handle = opendir($dir)) {
137 while (FALSE !== ($file = readdir($handle))) {
138 if (!in_array($file, $nomask) && $file[0] != '.') {
139 if (is_dir("$dir/$file")) {
140 $directories[$file] = "$dir/$file";
152 * Looks for library info files.
154 * This function scans the following directories for info files:
156 * - profiles/$profilename/libraries
157 * - sites/all/libraries
158 * - sites/$sitename/libraries
159 * - any directories specified via hook_libraries_info_file_paths()
162 * An array of info files, keyed by library name. The values are the paths of
165 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
166 * new library load and managment concepts described at:
167 * https://www.drupal.org/node/2170763
169 function libraries_scan_info_files() {
170 $profile = drupal_get_path('profile', drupal_get_profile());
171 $config = DrupalKernel::findSitePath(\Drupal::request());
173 // Build a list of directories.
174 $directories = \Drupal::moduleHandler()->invokeAll('libraries_info_file_paths', $args = array());
175 $directories[] = "$profile/libraries";
176 $directories[] = 'sites/all/libraries';
177 $directories[] = 'libraries';
178 $directories[] = "$config/libraries";
180 // Scan for info files.
182 foreach ($directories as $dir) {
183 if (file_exists($dir)) {
184 $files = array_merge($files, file_scan_directory($dir, '@^[a-z0-9._-]+\.libraries\.info\.yml$@', array(
191 foreach ($files as $filename => $file) {
192 $files[basename($filename, '.libraries.info')] = $file;
193 unset($files[$filename]);
200 * Invokes library callbacks.
203 * A string containing the group of callbacks that is to be applied. Should be
204 * either 'info', 'pre-detect', 'post-detect', or 'load'.
206 * An array of library information, passed by reference.
208 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
209 * new library load and managment concepts described at:
210 * https://www.drupal.org/node/2170763
212 function libraries_invoke($group, &$library) {
213 foreach ($library['callbacks'][$group] as $callback) {
214 libraries_traverse_library($library, $callback);
219 * Helper function to apply a callback to all parts of a library.
221 * Because library declarations can include variants and versions, and those
222 * version declarations can in turn include variants, modifying e.g. the 'files'
223 * property everywhere it is declared can be quite cumbersome, in which case
224 * this helper function is useful.
227 * An array of library information, passed by reference.
229 * A string containing the callback to apply to all parts of a library.
231 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
232 * new library load and managment concepts described at:
233 * https://www.drupal.org/node/2170763
235 function libraries_traverse_library(&$library, $callback) {
236 // Always apply the callback to the top-level library.
237 $callback($library, NULL, NULL);
239 // Apply the callback to versions.
240 if (isset($library['versions'])) {
241 foreach ($library['versions'] as $version_string => &$version) {
242 $callback($version, $version_string, NULL);
243 // Versions can include variants as well.
244 if (isset($version['variants'])) {
245 foreach ($version['variants'] as $version_variant_name => &$version_variant) {
246 $callback($version_variant, $version_string, $version_variant_name);
252 // Apply the callback to variants.
253 if (isset($library['variants'])) {
254 foreach ($library['variants'] as $variant_name => &$variant) {
255 $callback($variant, NULL, $variant_name);
261 * Library info callback to make all 'files' properties consistent.
263 * This turns libraries' file information declared as e.g.
265 * $library['files']['js'] = array('example_1.js', 'example_2.js');
269 * $library['files']['js'] = array(
270 * 'example_1.js' => array(),
271 * 'example_2.js' => array(),
274 * It does the same for the 'integration files' property.
277 * An associative array of library information or a part of it, passed by
280 * If the library information belongs to a specific version, the version
281 * string. NULL otherwise.
283 * If the library information belongs to a specific variant, the variant name.
286 * @see libraries_info()
287 * @see libraries_invoke()
289 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
290 * new library load and managment concepts described at:
291 * https://www.drupal.org/node/2170763
293 function libraries_prepare_files(&$library, $version = NULL, $variant = NULL) {
294 // Both the 'files' property and the 'integration files' property contain file
295 // declarations, and we want to make both consistent.
296 $file_types = array();
297 if (isset($library['files'])) {
298 $file_types[] = &$library['files'];
300 if (isset($library['integration files'])) {
301 // Integration files are additionally keyed by module.
302 foreach ($library['integration files'] as &$integration_files) {
303 $file_types[] = &$integration_files;
306 foreach ($file_types as &$files) {
307 // Go through all supported types of files.
308 foreach (array('js', 'css', 'php') as $type) {
309 if (isset($files[$type])) {
310 foreach ($files[$type] as $key => $value) {
311 // Unset numeric keys and turn the respective values into keys.
312 if (is_numeric($key)) {
313 $files[$type][$value] = array();
314 unset($files[$type][$key]);
323 * Library post-detect callback to process and detect dependencies.
325 * It checks whether each of the dependencies of a library are installed and
326 * available in a compatible version.
329 * An associative array of library information or a part of it, passed by
332 * If the library information belongs to a specific version, the version
333 * string. NULL otherwise.
335 * If the library information belongs to a specific variant, the variant name.
338 * @see libraries_info()
339 * @see libraries_invoke()
341 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
342 * new library load and managment concepts described at:
343 * https://www.drupal.org/node/2170763
345 function libraries_detect_dependencies(&$library, $version = NULL, $variant = NULL) {
346 if (isset($library['dependencies'])) {
347 foreach ($library['dependencies'] as &$dependency_string) {
348 $dependency_info = ModuleHandler::parseDependency($dependency_string);
349 $dependency = libraries_detect($dependency_info['name']);
350 if (!$dependency['installed']) {
351 $library['installed'] = FALSE;
352 $library['error'] = 'missing dependency';
353 $library['error message'] = t('The %dependency library, which the %library library depends on, is not installed.', array(
354 '%dependency' => $dependency['name'],
355 '%library' => $library['name'],
358 elseif (drupal_check_incompatibility($dependency_info, $dependency['version'])) {
359 $library['installed'] = FALSE;
360 $library['error'] = 'incompatible dependency';
361 $library['error message'] = t('The version %dependency_version of the %dependency library is not compatible with the %library library.', array(
362 '%dependency_version' => $dependency['version'],
363 '%dependency' => $dependency['name'],
364 '%library' => $library['name'],
368 // Remove the version string from the dependency, so libraries_load() can
369 // load the libraries directly.
370 $dependency_string = $dependency_info['name'];
376 * Returns information about registered libraries.
378 * The returned information is unprocessed; i.e., as registered by modules.
381 * (optional) The machine name of a library to return registered information
382 * for. If omitted, information about all registered libraries is returned.
384 * @return array|false
385 * An associative array containing registered information for all libraries,
386 * the registered information for the library specified by $name, or FALSE if
387 * the library $name is not registered.
389 * @see hook_libraries_info()
391 * @todo Re-introduce support for include file plugin system - either by copying
392 * Wysiwyg's code, or directly switching to CTools.
394 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
395 * new library load and managment concepts described at:
396 * https://www.drupal.org/node/2170763
398 function &libraries_info($name = NULL) {
399 // This static cache is re-used by libraries_detect() to save memory.
400 $libraries = &drupal_static(__FUNCTION__);
402 if (!isset($libraries)) {
403 $libraries = array();
404 // Gather information from hook_libraries_info().
405 $module_handler = \Drupal::moduleHandler();
406 foreach ($module_handler->getImplementations('libraries_info') as $module) {
407 foreach ($module_handler->invoke($module, 'libraries_info') as $machine_name => $properties) {
408 $properties['module'] = $module;
409 $libraries[$machine_name] = $properties;
412 // Gather information from hook_libraries_info() in enabled themes.
413 // @see drupal_alter()
414 global $theme, $base_theme_info;
416 $theme_keys = array();
417 foreach ($base_theme_info as $base) {
418 $theme_keys[] = $base->name;
420 $theme_keys[] = $theme;
421 foreach ($theme_keys as $theme_key) {
422 $function = $theme_key . '_' . 'libraries_info';
423 if (function_exists($function)) {
424 foreach ($function() as $machine_name => $properties) {
425 $properties['theme'] = $theme_key;
426 $libraries[$machine_name] = $properties;
432 // Gather information from .info files.
433 // .info files override module definitions.
434 // In order to stop Drupal's extension and the Drupal.org packaging
435 // system from finding library info files we use the 'libraries.info.yml'
436 // file extension. Therefore, having a 'type' key, like info files of
437 // modules, themes, and profiles have, is superfluous.
438 // \Drupal\Core\Extension\InfoParser, however, enforces the existence of a
439 // 'type' key in info files. We therefore use Symfony's YAML parser
441 // @todo Consider creating a dedicating InfoParser for library info files
442 // similar to \Drupal\Core\Extension\InfoParser
443 $parser = new Parser();
444 foreach (libraries_scan_info_files() as $machine_name => $file) {
445 $properties = $parser->parse(file_get_contents($file->uri));
446 $properties['info file'] = $file->uri;
447 $libraries[$machine_name] = $properties;
451 foreach ($libraries as $machine_name => &$properties) {
452 libraries_info_defaults($properties, $machine_name);
455 // Allow modules to alter the registered libraries.
456 $module_handler->alter('libraries_info', $libraries);
458 // Invoke callbacks in the 'info' group.
459 foreach ($libraries as &$properties) {
460 libraries_invoke('info', $properties);
465 if (!empty($libraries[$name])) {
466 return $libraries[$name];
477 * Applies default properties to a library definition.
480 * An array of library information, passed by reference.
482 * The machine name of the passed-in library.
484 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
485 * new library load and managment concepts described at:
486 * https://www.drupal.org/node/2170763
488 function libraries_info_defaults(&$library, $name) {
490 'machine name' => $name,
493 'download url' => '',
495 'library path' => NULL,
496 'version callback' => 'libraries_get_version',
497 'version arguments' => array(),
499 'dependencies' => array(),
500 'variants' => array(),
501 'versions' => array(),
502 'integration files' => array(),
503 'callbacks' => array(),
505 $library['callbacks'] += array(
507 'pre-detect' => array(),
508 'post-detect' => array(),
509 'pre-load' => array(),
510 'post-load' => array(),
513 // Add our own callbacks before any others.
514 array_unshift($library['callbacks']['info'], 'libraries_prepare_files');
515 array_unshift($library['callbacks']['post-detect'], 'libraries_detect_dependencies');
521 * Tries to detect a library and its installed version.
524 * The machine name of a library to return registered information for.
526 * @return array|false
527 * An associative array containing registered information for the library
528 * specified by $name, or FALSE if the library $name is not registered.
529 * In addition to the keys returned by libraries_info(), the following keys
531 * - installed: A boolean indicating whether the library is installed. Note
532 * that not only the top-level library, but also each variant contains this
534 * - version: If the version could be detected, the full version string.
535 * - error: If an error occurred during library detection, one of the
536 * following error statuses: "not found", "not detected", "not supported".
537 * - error message: If an error occurred during library detection, a detailed
540 * @see libraries_info()
542 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
543 * new library load and managment concepts described at:
544 * https://www.drupal.org/node/2170763
546 function libraries_detect($name) {
547 // Re-use the statically cached value of libraries_info() to save memory.
548 $library = &libraries_info($name);
550 if ($library === FALSE) {
553 // If 'installed' is set, library detection ran already.
554 if (isset($library['installed'])) {
558 $library['installed'] = FALSE;
560 // Check whether the library exists.
561 if (!isset($library['library path'])) {
562 $library['library path'] = libraries_get_path($library['machine name']);
564 if ($library['library path'] === FALSE || !file_exists($library['library path'])) {
565 $library['error'] = 'not found';
566 $library['error message'] = t('The %library library could not be found.', array(
567 '%library' => $library['name'],
572 // Invoke callbacks in the 'pre-detect' group.
573 libraries_invoke('pre-detect', $library);
575 // Detect library version, if not hardcoded.
576 if (!isset($library['version'])) {
577 // We support both a single parameter, which is an associative array, and an
578 // indexed array of multiple parameters.
579 if (isset($library['version arguments'][0])) {
580 // Add the library as the first argument.
581 $library['version'] = call_user_func_array($library['version callback'], array_merge(array($library), $library['version arguments']));
584 $library['version'] = $library['version callback']($library, $library['version arguments']);
586 if (empty($library['version'])) {
587 $library['error'] = 'not detected';
588 $library['error message'] = t('The version of the %library library could not be detected.', array(
589 '%library' => $library['name'],
595 // Determine to which supported version the installed version maps.
596 if (!empty($library['versions'])) {
597 ksort($library['versions']);
599 foreach ($library['versions'] as $supported_version => $version_properties) {
600 if (version_compare($library['version'], $supported_version, '>=')) {
601 $version = $supported_version;
605 $library['error'] = 'not supported';
606 $library['error message'] = t('The installed version %version of the %library library is not supported.', array(
607 '%version' => $library['version'],
608 '%library' => $library['name'],
613 // Apply version specific definitions and overrides.
614 $library = array_merge($library, $library['versions'][$version]);
615 unset($library['versions']);
618 // Check each variant if it is installed.
619 if (!empty($library['variants'])) {
620 foreach ($library['variants'] as $variant_name => &$variant) {
621 // If no variant callback has been set, assume the variant to be
623 if (!isset($variant['variant callback'])) {
624 $variant['installed'] = TRUE;
627 // We support both a single parameter, which is an associative array,
628 // and an indexed array of multiple parameters.
629 if (isset($variant['variant arguments'][0])) {
630 // Add the library as the first argument, and the variant name as the second.
631 $variant['installed'] = call_user_func_array($variant['variant callback'], array_merge(array($library, $variant_name), $variant['variant arguments']));
634 $variant['installed'] = $variant['variant callback']($library, $variant_name, $variant['variant arguments']);
636 if (!$variant['installed']) {
637 $variant['error'] = 'not found';
638 $variant['error message'] = t('The %variant variant of the %library library could not be found.', array(
639 '%variant' => $variant_name,
640 '%library' => $library['name'],
647 // If we end up here, the library should be usable.
648 $library['installed'] = TRUE;
650 // Invoke callbacks in the 'post-detect' group.
651 libraries_invoke('post-detect', $library);
660 * The name of the library to load.
662 * The name of the variant to load. Note that only one variant of a library
663 * can be loaded within a single request. The variant that has been passed
664 * first is used; different variant names in subsequent calls are ignored.
667 * An associative array of the library information as returned from
668 * libraries_info(). The top-level properties contain the effective definition
669 * of the library (variant) that has been loaded. Additionally:
670 * - installed: Whether the library is installed, as determined by
671 * libraries_detect_library().
672 * - loaded: Either the amount of library files that have been loaded, or
673 * FALSE if the library could not be loaded.
674 * See hook_libraries_info() for more information.
676 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
677 * new library load and managment concepts described at:
678 * https://www.drupal.org/node/2170763
680 function libraries_load($name, $variant = NULL) {
681 $loaded = &drupal_static(__FUNCTION__, array());
683 if (!isset($loaded[$name])) {
684 $library = \Drupal::cache('libraries')->get($name);
686 $library = $library->data;
689 $library = libraries_detect($name);
690 \Drupal::cache('libraries')->set($name, $library);
692 // If a variant was specified, override the top-level properties with the
693 // variant properties.
694 if (isset($variant)) {
695 // Ensure that the $variant key exists, and if it does not, set its
696 // 'installed' property to FALSE by default. This will prevent the loading
697 // of the library files below.
698 $library['variants'] += array($variant => array('installed' => FALSE));
699 $library = array_merge($library, $library['variants'][$variant]);
701 // Regardless of whether a specific variant was requested or not, there can
702 // only be one variant of a library within a single request.
703 unset($library['variants']);
705 // If the library (variant) is installed, load it.
706 $library['loaded'] = FALSE;
707 if ($library['installed']) {
708 // Load library dependencies.
709 if (isset($library['dependencies'])) {
710 foreach ($library['dependencies'] as $dependency) {
711 libraries_load($dependency);
715 // Invoke callbacks in the 'pre-load' group.
716 libraries_invoke('pre-load', $library);
718 // Load all the files associated with the library.
719 $library['loaded'] = libraries_load_files($library);
721 // Invoke callbacks in the 'post-load' group.
722 libraries_invoke('post-load', $library);
724 $loaded[$name] = $library;
727 return $loaded[$name];
731 * Loads a library's files.
734 * An array of library information as returned by libraries_info().
737 * The number of loaded files.
739 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
740 * new library load and managment concepts described at:
741 * https://www.drupal.org/node/2170763
743 function libraries_load_files($library) {
745 // Construct the full path to the library for later use.
746 $path = $library['library path'];
747 $path = ($library['path'] !== '' ? $path . '/' . $library['path'] : $path);
749 // Count the number of loaded files for the return value.
752 // Load both the JavaScript and the CSS files.
753 // The parameters for drupal_add_js() and drupal_add_css() require special
755 // @see drupal_process_attached()
756 foreach (array('js', 'css') as $type) {
757 // Given the removal of core functions like _drupal_add_js and
758 // _drupal_add_css the logic below cannot safely be run anymore.
759 // @see https://www.drupal.org/node/2702563
761 if (!empty($library['files'][$type])) {
762 foreach ($library['files'][$type] as $data => $options) {
763 // If the value is not an array, it's a filename and passed as first
764 // (and only) argument.
765 if (!is_array($options)) {
769 // In some cases, the first parameter ($data) is an array. Arrays can't
770 // be passed as keys in PHP, so we have to get $data from the value
772 if (is_numeric($data)) {
773 $data = $options['data'];
774 unset($options['data']);
776 // Prepend the library path to the file name.
777 $data = "$path/$data";
778 // Apply the default group if the group isn't explicitly given.
779 if (!isset($options['group'])) {
780 $options['group'] = ($type == 'js') ? JS_DEFAULT : CSS_AGGREGATE_DEFAULT;
782 if ($type === 'js') {
783 $options['version'] = -1;
785 // @todo Avoid the usage of _drupal_add_js() and _drupal_add_css()
786 call_user_func('_drupal_add_' . $type, $data, $options);
793 if (!empty($library['files']['php'])) {
794 foreach ($library['files']['php'] as $file => $array) {
795 $file_path = DRUPAL_ROOT . '/' . $path . '/' . $file;
796 if (file_exists($file_path)) {
797 require_once $file_path;
803 // Load integration files.
804 if (!empty($library['integration files'])) {
805 foreach ($library['integration files'] as $module => $files) {
806 libraries_load_files(array(
809 'library path' => drupal_get_path('module', $module),
818 * Gets the version information from an arbitrary library.
821 * An associative array containing all information about the library.
823 * An associative array containing with the following keys:
824 * - file: The filename to parse for the version, relative to the library
825 * path. For example: 'docs/changelog.txt'.
826 * - pattern: A string containing a regular expression (PCRE) to match the
827 * library version. For example: '@version\s+([0-9a-zA-Z\.-]+)@'. Note that
828 * the returned version is not the match of the entire pattern (i.e.
829 * '@version 1.2.3' in the above example) but the match of the first
830 * sub-pattern (i.e. '1.2.3' in the above example).
831 * - lines: (optional) The maximum number of lines to search the pattern in.
833 * - cols: (optional) The maximum number of characters per line to take into
834 * account. Defaults to 200. In case of minified or compressed files, this
835 * prevents reading the entire file into memory.
838 * A string containing the version of the library.
840 * @see libraries_get_path()
842 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
843 * new library load and managment concepts described at:
844 * https://www.drupal.org/node/2170763
846 function libraries_get_version($library, $options) {
855 $file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $options['file'];
856 if (empty($options['file']) || !file_exists($file)) {
859 $file = fopen($file, 'r');
860 while ($options['lines'] && $line = fgets($file, $options['cols'])) {
861 if (preg_match($options['pattern'], $line, $version)) {