b316b55579ef03b379489ae548d08288134e0421
[project/libraries.git] / libraries.module
1 <?php
2
3 /**
4 * @file
5 * External library handling for Drupal modules.
6 */
7
8 /**
9 * Implements hook_flush_caches().
10 */
11 function libraries_flush_caches() {
12 return array('cache_libraries');
13 }
14
15 /**
16 * Gets the path of a library.
17 *
18 * @param $name
19 * The machine name of a library to return the path for.
20 * @param $base_path
21 * Whether to prefix the resulting path with base_path().
22 *
23 * @return
24 * The path to the specified library.
25 *
26 * @ingroup libraries
27 */
28 function libraries_get_path($name, $base_path = FALSE) {
29 $libraries = &drupal_static(__FUNCTION__);
30
31 if (!isset($libraries)) {
32 $libraries = libraries_get_libraries();
33 }
34
35 $path = ($base_path ? base_path() : '');
36 if (!isset($libraries[$name])) {
37 // Most often, external libraries can be shared across multiple sites, so
38 // we return sites/all/libraries as the default path.
39 $path .= 'sites/all/libraries/' . $name;
40 }
41 else {
42 $path .= $libraries[$name];
43 }
44
45 return $path;
46 }
47
48 /**
49 * Returns an array of library directories.
50 *
51 * Returns an array of library directories from the all-sites directory
52 * (i.e. sites/all/libraries/), the profiles directory, and site-specific
53 * directory (i.e. sites/somesite/libraries/). The returned array will be keyed
54 * by the library name. Site-specific libraries are prioritized over libraries
55 * in the default directories. That is, if a library with the same name appears
56 * in both the site-wide directory and site-specific directory, only the
57 * site-specific version will be listed.
58 *
59 * @return
60 * A list of library directories.
61 *
62 * @ingroup libraries
63 */
64 function libraries_get_libraries() {
65 $directory = 'libraries';
66 $searchdir = array();
67 $profile = drupal_get_profile();
68 $config = conf_path();
69
70 // Similar to 'modules' and 'themes' directories in the root directory,
71 // certain distributions may want to place libraries into a 'libraries'
72 // directory in Drupal's root directory.
73 $searchdir[] = $directory;
74
75 // The 'profiles' directory contains pristine collections of modules and
76 // themes as organized by a distribution. It is pristine in the same way
77 // that /modules is pristine for core; users should avoid changing anything
78 // there in favor of sites/all or sites/<domain> directories.
79 if (file_exists("profiles/$profile/$directory")) {
80 $searchdir[] = "profiles/$profile/$directory";
81 }
82
83 // Always search sites/all/*.
84 $searchdir[] = 'sites/all/' . $directory;
85
86 // Also search sites/<domain>/*.
87 if (file_exists("$config/$directory")) {
88 $searchdir[] = "$config/$directory";
89 }
90
91 // Retrieve list of directories.
92 // @todo Core: Allow to scan for directories.
93 $directories = array();
94 $nomask = array('CVS');
95 foreach ($searchdir as $dir) {
96 if (is_dir($dir) && $handle = opendir($dir)) {
97 while (FALSE !== ($file = readdir($handle))) {
98 if (!in_array($file, $nomask) && $file[0] != '.') {
99 if (is_dir("$dir/$file")) {
100 $directories[$file] = "$dir/$file";
101 }
102 }
103 }
104 closedir($handle);
105 }
106 }
107
108 return $directories;
109 }
110
111 /**
112 * Looks for library info files.
113 *
114 * This function scans the following directories for info files:
115 * - libraries
116 * - profiles/$profilename/libraries
117 * - sites/all/libraries
118 * - sites/$sitename/libraries
119 * - any directories specified via hook_libraries_info_file_paths()
120 *
121 * @return
122 * An array of info files, keyed by library name. The values are the paths of
123 * the files.
124 */
125 function libraries_scan_info_files() {
126 $profile = drupal_get_profile();
127 $config = conf_path();
128
129 // Build a list of directories.
130 $directories = module_invoke_all('libraries_info_file_paths');
131 $directories[] = 'libraries';
132 $directories[] = "profiles/$profile/libraries";
133 $directories[] = 'sites/all/libraries';
134 $directories[] = "$config/libraries";
135
136 // Scan for info files.
137 $files = array();
138 foreach ($directories as $dir) {
139 if (file_exists($dir)) {
140 $files = array_merge($files, file_scan_directory($dir, '@^[a-z0-9._-]+\.libraries\.info$@', array(
141 'key' => 'name',
142 'recurse' => FALSE,
143 )));
144 }
145 }
146
147 foreach ($files as $filename => $file) {
148 $files[basename($filename, '.libraries')] = $file;
149 unset($files[$filename]);
150 }
151
152 return $files;
153 }
154 /**
155 * Returns information about registered libraries.
156 *
157 * The returned information is unprocessed, i.e. as registered by modules.
158 *
159 * @param $name
160 * (optional) The machine name of a library to return registered information
161 * for, or FALSE if no library with the given name exists. If omitted,
162 * information about all libraries is returned.
163 *
164 * @return
165 * An associative array containing registered information for all libraries,
166 * or the registered information for the library specified by $name.
167 *
168 * @see hook_libraries_info()
169 *
170 * @todo Re-introduce support for include file plugin system - either by copying
171 * Wysiwyg's code, or directly switching to CTools.
172 */
173 function libraries_info($name = NULL) {
174 $libraries = &drupal_static(__FUNCTION__);
175
176 if (!isset($libraries)) {
177 $libraries = array();
178 // Gather information from hook_libraries_info().
179 foreach (module_implements('libraries_info') as $module) {
180 foreach (module_invoke($module, 'libraries_info') as $machine_name => $properties) {
181 $properties['module'] = $module;
182 $libraries[$machine_name] = $properties;
183 }
184 }
185 // Gather information from .info files.
186 // .info files override module definitions.
187 foreach (libraries_scan_info_files() as $machine_name => $file) {
188 $properties = drupal_parse_info_file($file->uri);
189 $properties['info file'] = $file->uri;
190 $libraries[$machine_name] = $properties;
191 }
192
193 // Provide defaults.
194 foreach ($libraries as $machine_name => &$properties) {
195 $properties += array(
196 'machine name' => $machine_name,
197 'name' => $machine_name,
198 'vendor url' => '',
199 'download url' => '',
200 'path' => '',
201 'library path' => NULL,
202 'version callback' => 'libraries_get_version',
203 'version arguments' => array(),
204 'files' => array(),
205 'variants' => array(),
206 'versions' => array(),
207 'integration files' => array(),
208 );
209 }
210
211 // Allow modules to alter the registered libraries.
212 drupal_alter('libraries_info', $libraries);
213 }
214
215 if (isset($name)) {
216 return !empty($libraries[$name]) ? $libraries[$name] : FALSE;
217 }
218 return $libraries;
219 }
220
221 /**
222 * Detect libraries and library versions.
223 *
224 * @todo We need to figure out whether, and if, how we want to retain the
225 * processed information. I.e. either use a static cache here, or make
226 * libraries_info() conditionally invoke libraries_detect($name). D7 only way:
227 * Re-use drupal_static() of libraries_info() - but would still require to
228 * update the (DB) cache (there likely will be one soon). Also, we probably do
229 * not want to ALWAYS parse ALL possible libraries; rather, the
230 * requesting/consuming module likely wants to know whether a list of
231 * supported libraries (possibly those registered by itself, or in a certain
232 * "category") is available... Food for thought.
233 *
234 * @param $libraries
235 * An array of libraries to detect, as returned from libraries_info().
236 *
237 * @see libraries_info()
238 */
239 function libraries_detect($libraries) {
240 foreach ($libraries as $name => &$library) {
241 libraries_detect_library($library);
242 cache_set($name, $library, 'cache_libraries');
243 }
244 return $libraries;
245 }
246
247 /**
248 * Tries to detect a library and its installed version.
249 *
250 * @param $library
251 * An associative array describing a single library, as returned from
252 * libraries_info().
253 */
254 function libraries_detect_library(&$library) {
255 $library['installed'] = FALSE;
256
257 // Check whether the library exists.
258 if (!isset($library['library path'])) {
259 $library['library path'] = libraries_get_path($library['machine name']);
260 }
261 if (!file_exists($library['library path'])) {
262 $library['error'] = 'not found';
263 $library['error message'] = t('The %library library could not be found.', array(
264 '%library' => $library['name'],
265 ));
266 return;
267 }
268
269 // Detect library version, if not hardcoded.
270 if (!isset($library['version'])) {
271 // We support both a single parameter, which is an associative array, and an
272 // indexed array of multiple parameters.
273 if (isset($library['version arguments'][0])) {
274 // Add the library as the first argument.
275 $library['version'] = call_user_func_array($library['version callback'], array_merge(array($library), $library['version arguments']));
276 }
277 else {
278 $library['version'] = $library['version callback']($library, $library['version arguments']);
279 }
280 if (empty($library['version'])) {
281 $library['error'] = 'not detected';
282 $library['error message'] = t('The version of the %library library could not be detected.', array(
283 '%library' => $library['name'],
284 ));
285 return;
286 }
287 }
288
289 // Determine to which supported version the installed version maps.
290 if (!empty($library['versions'])) {
291 ksort($library['versions']);
292 $version = 0;
293 foreach ($library['versions'] as $supported_version => $version_properties) {
294 if (version_compare($library['version'], $supported_version, '>=')) {
295 $version = $supported_version;
296 }
297 }
298 if (!$version) {
299 $library['error'] = 'not supported';
300 $library['error message'] = t('The installed version %version of the %library library is not supported.', array(
301 '%version' => $library['version'],
302 '%library' => $library['name'],
303 ));
304 return;
305 }
306
307 // Apply version specific definitions and overrides.
308 $library = array_merge($library, $library['versions'][$version]);
309 unset($library['versions']);
310 }
311
312 // Check each variant if it is installed.
313 if (!empty($library['variants'])) {
314 foreach ($library['variants'] as $variant_name => &$variant) {
315 // If no variant callback has been set, assume the variant to be
316 // installed.
317 if (!isset($variant['variant callback'])) {
318 $variant['installed'] = TRUE;
319 }
320 else {
321 // We support both a single parameter, which is an associative array,
322 // and an indexed array of multiple parameters.
323 if (isset($variant['variant arguments'][0])) {
324 // Add the library as the first argument, and the variant name as the second.
325 $variant['installed'] = call_user_func_array($variant['variant callback'], array_merge(array($library, $variant_name), $variant['variant arguments']));
326 }
327 else {
328 $variant['installed'] = $variant['variant callback']($library, $variant_name, $variant['variant arguments']);
329 }
330 if (!$variant['installed']) {
331 $variant['error'] = 'not found';
332 $variant['error message'] = t('The %variant variant of the %library library could not be found.', array(
333 '%variant' => $variant_name,
334 '%library' => $library['name'],
335 ));
336 }
337 }
338 }
339 }
340
341 // If we end up here, the library should be usable.
342 $library['installed'] = TRUE;
343 return $library;
344 }
345
346 /**
347 * Loads a library.
348 *
349 * @param $name
350 * The name of the library to load.
351 * @param $variant
352 * The name of the variant to load. Note that only one variant of a library
353 * can be loaded within a single request. The variant that has been passed
354 * first is used; different variant names in subsequent calls are ignored.
355 *
356 * @return
357 * An associative array of the library information as returned from
358 * libraries_info(). The top-level properties contain the effective definition
359 * of the library (variant) that has been loaded. Additionally:
360 * - installed: Whether the library is installed, as determined by
361 * libraries_detect_library().
362 * - loaded: Either the amount of library files that have been loaded, or
363 * FALSE if the library could not be loaded.
364 * See hook_libraries_info() for more information.
365 */
366 function libraries_load($name, $variant = NULL) {
367 $loaded = &drupal_static(__FUNCTION__, array());
368
369 if (!isset($loaded[$name])) {
370 $library = cache_get($name, 'cache_libraries');
371 if ($library) {
372 $library = $library->data;
373 }
374 else {
375 $library = libraries_info($name);
376 libraries_detect_library($library);
377 cache_set($name, $library, 'cache_libraries');
378 }
379
380 // If a variant was specified, override the top-level properties with the
381 // variant properties.
382 if (isset($variant)) {
383 // Ensure that the $variant key exists, and if it does not, set its
384 // 'installed' property to FALSE by default. This will prevent the loading
385 // of the library files below.
386 $library['variants'] += array($variant => array('installed' => FALSE));
387 $library = array_merge($library, $library['variants'][$variant]);
388 }
389 // Regardless of whether a specific variant was requested or not, there can
390 // only be one variant of a library within a single request.
391 unset($library['variants']);
392
393 // If the library (variant) is installed, load it.
394 $library['loaded'] = FALSE;
395 if ($library['installed']) {
396 $library['loaded'] = libraries_load_files($library);
397 }
398 $loaded[$name] = $library;
399 }
400
401 return $loaded[$name];
402 }
403
404 /**
405 * Loads a library's files.
406 *
407 * @param $library
408 * An array of library information as returned by libraries_info().
409 *
410 * @return
411 * The number of loaded files.
412 */
413 function libraries_load_files($library) {
414 // Load integration files.
415 if (!empty($library['integration files'])) {
416 foreach ($library['integration files'] as $module => $files) {
417 libraries_load_files(array(
418 'files' => $files,
419 'path' => '',
420 'library path' => drupal_get_path('module', $module),
421 ));
422 }
423 }
424
425 // Construct the full path to the library for later use.
426 $path = $library['library path'];
427 $path = ($library['path'] !== '' ? $path . '/' . $library['path'] : $path);
428
429 // Count the number of loaded files for the return value.
430 $count = 0;
431
432 // Load both the JavaScript and the CSS files.
433 // The parameters for drupal_add_js() and drupal_add_css() require special
434 // handling.
435 // @see drupal_process_attached()
436 foreach (array('js', 'css') as $type) {
437 if (!empty($library['files'][$type])) {
438 foreach ($library['files'][$type] as $data => $options) {
439 // If the value is not an array, it's a filename and passed as first
440 // (and only) argument.
441 if (!is_array($options)) {
442 // Prepend the library path to the file name.
443 $data = "$path/$options";
444 $options = NULL;
445 }
446 // In some cases, the first parameter ($data) is an array. Arrays can't
447 // be passed as keys in PHP, so we have to get $data from the value
448 // array.
449 if (is_numeric($data)) {
450 $data = $options['data'];
451 unset($options['data']);
452 }
453 // Apply the default group if the group isn't explicitly given.
454 if (!isset($options['group'])) {
455 $options['group'] = ($type == 'js') ? JS_DEFAULT : CSS_DEFAULT;
456 }
457 call_user_func('drupal_add_' . $type, $data, $options);
458 $count++;
459 }
460 }
461 }
462
463 // Load PHP files.
464 if (!empty($library['files']['php'])) {
465 foreach ($library['files']['php'] as $file) {
466 $file_path = DRUPAL_ROOT . '/' . $path . '/' . $file;
467 if (file_exists($file_path)) {
468 require_once $file_path;
469 $count++;
470 }
471 }
472 }
473
474 return $count;
475 }
476
477 /**
478 * Gets the version information from an arbitrary library.
479 *
480 * @param $library
481 * An associative array containing all information about the library.
482 * @param $options
483 * An associative array containing with the following keys:
484 * - file: The filename to parse for the version, relative to the library
485 * path. For example: 'docs/changelog.txt'.
486 * - pattern: A string containing a regular expression (PCRE) to match the
487 * library version. For example: '@version\s+([0-9a-zA-Z\.-]+)@'.
488 * - lines: (optional) The maximum number of lines to search the pattern in.
489 * Defaults to 20.
490 * - cols: (optional) The maximum number of characters per line to take into
491 * account. Defaults to 200. In case of minified or compressed files, this
492 * prevents reading the entire file into memory.
493 *
494 * @return
495 * A string containing the version of the library.
496 *
497 * @see libraries_get_path()
498 */
499 function libraries_get_version($library, $options) {
500 // Provide defaults.
501 $options += array(
502 'file' => '',
503 'pattern' => '',
504 'lines' => 20,
505 'cols' => 200,
506 );
507
508 $file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $options['file'];
509 if (empty($options['file']) || !file_exists($file)) {
510 return;
511 }
512 $file = fopen($file, 'r');
513 while ($options['lines'] && $line = fgets($file, $options['cols'])) {
514 if (preg_match($options['pattern'], $line, $version)) {
515 fclose($file);
516 return $version[1];
517 }
518 $options['lines']--;
519 }
520 fclose($file);
521 }