Newer
Older
Earl Miles
committed
<?php
Earl Miles
committed
/**
* @file
*
* Contains routines to organize and load plugins. It allows a special
* variation of the hook system so that plugins can be kept in separate
* .inc files, and can be either loaded all at once or loaded only when
* necessary.
*/
Earl Miles
committed
/**
Earl Miles
committed
* Get an array of information about modules that support an API.
Earl Miles
committed
*
* This will ask each module if they support the given API, and if they do
Earl Miles
committed
* it will return an array of information about the modules that do.
Earl Miles
committed
*
* This function invokes hook_ctools_api. This invokation is statically
* cached, so feel free to call it as often per page run as you like, it
* will cost very little.
*
Earl Miles
committed
* This function can be used as an alternative to module_implements and can
* thus be used to find a precise list of modules that not only support
* a given hook (aka 'api') but also restrict to only modules that use
* the given version. This will allow multiple modules moving at different
* paces to still be able to work together and, in the event of a mismatch,
* either fall back to older behaviors or simply cease loading, which is
* still better than a crash.
*
Earl Miles
committed
* @param $owner
* The name of the module that controls the API.
* @param $api
* The name of the api. The api name forms the file name:
* $module.$api.inc
* @param $minimum_version
* The lowest version API that is compatible with this one. If a module
* reports its API as older than this, its files will not be loaded. This
* should never change during operation.
* @param $current_version
* The current version of the api. If a module reports its minimum API as
* higher than this, its files will not be loaded. This should never change
* during operation.
*
* @return
Earl Miles
committed
* An array of API information, keyed by module. Each module's information will
* contain:
* - 'version': The version of the API required by the module. The module
* should use the lowest number it can support so that the widest range
* of supported versions can be used.
* - 'path': If not provided, this will be the module's path. This is
* where the module will store any subsidiary files. This differs from
* plugin paths which are figured separately.
*
* APIs can request any other information to be placed here that they might
* need. This should be in the documentation for that particular API.
Earl Miles
committed
*/
Earl Miles
committed
function ctools_plugin_api_info($owner, $api, $minimum_version, $current_version) {
$cache = &drupal_static(__FUNCTION__, array());
Earl Miles
committed
if (!isset($cache[$owner][$api])) {
$cache[$owner][$api] = array();
foreach (module_implements('ctools_plugin_api') as $module) {
$function = $module . '_ctools_plugin_api';
Earl Miles
committed
$info = $function($owner, $api);
Earl Miles
committed
if (!isset($info['version'])) {
continue;
}
// Only process if version is between minimum and current, inclusive.
Earl Miles
committed
if ($info['version'] >= $minimum_version && $info['version'] <= $current_version) {
Earl Miles
committed
if (!isset($info['path'])) {
$info['path'] = drupal_get_path('module', $module);
}
$cache[$owner][$api][$module] = $info;
}
}
// And allow themes to implement these as well.
$themes = _ctools_list_themes();
foreach ($themes as $name => $theme) {
if (!empty($theme->info['api'][$owner][$api])) {
$info = $theme->info['api'][$owner][$api];
if (!isset($info['version'])) {
continue;
}
// Only process if version is between minimum and current, inclusive.
if ($info['version'] >= $minimum_version && $info['version'] <= $current_version) {
if (!isset($info['path'])) {
$info['path'] = '';
}
// Because themes can't easily specify full path, we add it here
// even though we do not for modules:
$info['path'] = drupal_get_path('theme', $name) . '/' . $info['path'];
$cache[$owner][$api][$name] = $info;
}
}
}
Earl Miles
committed
}
return $cache[$owner][$api];
}
Earl Miles
committed
Earl Miles
committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* Load a group of API files.
*
* This will ask each module if they support the given API, and if they do
* it will load the specified file name. The API and the file name
* coincide by design.
*
* @param $owner
* The name of the module that controls the API.
* @param $api
* The name of the api. The api name forms the file name:
* $module.$api.inc, though this can be overridden by the module's response.
* @param $minimum_version
* The lowest version API that is compatible with this one. If a module
* reports its API as older than this, its files will not be loaded. This
* should never change during operation.
* @param $current_version
* The current version of the api. If a module reports its minimum API as
* higher than this, its files will not be loaded. This should never change
* during operation.
*
* @return
* The API information, in case you need it.
*/
function ctools_plugin_api_include($owner, $api, $minimum_version, $current_version) {
static $already_done = array();
$info = ctools_plugin_api_info($owner, $api, $minimum_version, $current_version);
if (!isset($already_done[$owner][$api])) {
foreach ($info as $module => $plugin_info) {
if (!isset($plugin_info['file'])) {
$plugin_info['file'] = "$module.$api.inc";
Earl Miles
committed
}
if (file_exists("./$plugin_info[path]/$plugin_info[file]")) {
$plugin_info[$module]['included'] = TRUE;
require_once "./$plugin_info[path]/$plugin_info[file]";
Earl Miles
committed
}
$info[$module] = $plugin_info;
Earl Miles
committed
}
Earl Miles
committed
$already_done[$owner][$api] = TRUE;
Earl Miles
committed
}
Earl Miles
committed
return $info;
Earl Miles
committed
}
/**
* Fetch a group of plugins by name.
*
* @param $module
* The name of the module that utilizes this plugin system. It will be
* used to call hook_ctools_plugin_$plugin() to get more data about the plugin.
* @param $type
* The type identifier of the plugin.
* @param $id
* If specified, return only information about plugin with this identifier.
* The system will do its utmost to load only plugins with this id.
*
* @return
* An array of information arrays about the plugins received. The contents
* of the array are specific to the plugin.
*/
function ctools_get_plugins($module, $type, $id = NULL) {
// Store local caches of plugins and plugin info so we don't have to do full
// lookups everytime.
static $info = array();
static $plugins = array();
// Store the status of plugin loading. If a module plugin type pair is true,
// then it is fully loaded and no searching or setup needs to be done.
static $setup = array();
// Request metadata/defaults for this plugin from the declaring module. This
// is done once per page request, upon a request being made for that plugin.
if (!isset($info[$module][$type])) {
$info[$module][$type] = ctools_plugin_get_info($module, $type);
// Also, initialize the local plugin cache.
$plugins[$module][$type] = array();
// We assume we don't need to build a cache.
$build_cache = FALSE;
// If the plugin info says this can be cached, check cache first.
if ($info[$module][$type]['cache'] && empty($setup[$module][$type])) {
// @todo Maybe this should use our own table but free wiping
// with content updates is convenient.
$cache = cache_get("plugins:$module:$type", $info[$module][$type]['cache table']);
// if cache load successful, set $all_hooks and $all_files to true.
if (!empty($cache->data)) {
$plugins[$module][$type] = $cache->data;
// Set $setup to true so we know things where loaded.
$setup[$module][$type] = TRUE;
}
else {
// Cache load failed so store that we need to build and write the cache.
$build_cache = TRUE;
}
}
// Always load all hooks if we need them. Note we only need them now if the
// plugin asks for them. We can assume that if we have plugins we've already
// called the global hook.
if (!empty($info[$module][$type]['use hooks']) && empty($plugins[$module][$type])) {
$plugins[$module][$type] = ctools_plugin_load_hooks($info[$module][$type]);
}
// Then see if we should load all files. We only do this if we
// want a list of all plugins or there was a cache miss.
if (empty($setup[$module][$type]) && ($build_cache || !$id)) {
$setup[$module][$type] = TRUE;
$plugins[$module][$type] = array_merge($plugins[$module][$type], ctools_plugin_load_includes($info[$module][$type]));
}
// If we were told earlier that this is cacheable and the cache was
// empty, give something back.
if ($build_cache) {
cache_set("plugins:$module:$type", $plugins[$module][$type], $info[$module][$type]['cache table']);
}
// If no id was requested, we are finished here:
if (!$id) {
return $plugins[$module][$type];
}
// Check to see if we need to look for the file
if (!array_key_exists($id, $plugins[$module][$type])) {
$result = ctools_plugin_load_includes($info[$module][$type], $id);
// Set to either what was returned or NULL.
$plugins[$module][$type][$id] = isset($result[$id]) ? $result[$id] : NULL;
}
// At this point we should either have the plugin, or a NULL.
return $plugins[$module][$type][$id];
Earl Miles
committed
/**
* Load plugins from a directory.
*
* @param $info
* The plugin info as returned by ctools_plugin_get_info()
* @param $file
* The file to load if we're looking for just one particular plugin.
*
* @return
* An array of information created for this plugin.
*/
function ctools_plugin_load_includes($info, $file = NULL) {
// Load all our plugins.
$directories = ctools_plugin_get_directories($info);
$file_list = array();
if (isset($info['extension'])) {
$extension = $info['extension'];
}
else if (isset($info['info file'])) {
$extension = 'info';
}
else {
$extension = 'inc';
}
Earl Miles
committed
foreach ($directories as $module => $path) {
$file_list[$module] = drupal_system_listing("/$file." . $extension . '$/', $path, 'name', 0);
Earl Miles
committed
}
$plugins = array();
// Iterate through all the plugin .inc files, load them and process the hook
// that should now be available.
foreach (array_filter($file_list) as $module => $files) {
foreach ($files as $file) {
if (isset($info['info file'])) {
// Parse a .info file
$result = ctools_plugin_process_info($info, $module, $file);
}
else {
// Parse a hook.
Earl Miles
committed
$plugin = NULL; // ensure that we don't have something leftover from earlier.
require_once DRUPAL_ROOT . '/' . $file->uri;
// .inc files have a special format for the hook identifier.
// For example, 'foo.inc' in the module 'mogul' using the plugin
// whose hook is named 'borg_type' should have a function named (deep breath)
// mogul_foo_borg_type()
Earl Miles
committed
// If, however, the .inc file set the quasi-global $plugin array, we
// can use that and not even call a function. Set the $identifier
// appropriately and ctools_plugin_process() will handle it.
$identifier = isset($plugin) ? $plugin : $module . '_' . $file->name;
$result = ctools_plugin_process($info, $module, $identifier, dirname($file->uri), basename($file->uri), $file->name);
Earl Miles
committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
if (is_array($result)) {
$plugins = array_merge($plugins, $result);
}
}
}
return $plugins;
}
/**
* Get a list of directories to search for plugins of the given type.
*
* This utilizes hook_ctools_plugin_directory() to determine a complete list of
* directories. Only modules that implement this hook and return a string
* value will have their directories included.
*
* @param $info
* The $info array for the plugin as returned by ctools_plugin_get_info().
*
* @return array $directories
* An array of directories to search.
*/
function ctools_plugin_get_directories($info) {
$directories = array();
foreach (module_implements('ctools_plugin_directory') as $module) {
$function = $module . '_ctools_plugin_directory';
$result = $function($info['module'], $info['type']);
Earl Miles
committed
if ($result && is_string($result)) {
$directories[$module] = drupal_get_path('module', $module) . '/' . $result;
}
}
if (!empty($info['load themes'])) {
$themes = _ctools_list_themes();
foreach ($themes as $name => $theme) {
if (!empty($theme->info['plugins'][$info['module']][$info['type']])) {
$directories[$name] = drupal_get_path('theme', $name) . '/' . $theme->info['plugins'][$info['module']][$info['type']];
}
}
}
Earl Miles
committed
return $directories;
}
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/**
* Helper function to build a ctools-friendly list of themes capable of
* providing plugins.
*
* @return array $themes
* A list of themes that can act as plugin providers, sorted parent-first with
* the active theme placed last.
*/
function _ctools_list_themes() {
static $themes;
if (is_null($themes)) {
$current = variable_get('theme_default', FALSE);
$themes = $active = array();
$all_themes = list_themes();
foreach ($all_themes as $name => $theme) {
// Only search from active themes
if (empty($theme->status) && $theme->name != $current) {
continue;
}
$active[$name] = $theme;
// Prior to drupal 6.14, $theme->base_themes does not exist. Build it.
if (!isset($theme->base_themes) && !empty($theme->base_theme)) {
$active[$name]->base_themes = ctools_find_base_themes($all_themes, $name);
}
}
// Construct a parent-first list of all themes
foreach ($active as $name => $theme) {
$base_themes = isset($theme->base_themes) ? $theme->base_themes : array();
$themes = array_merge($themes, $base_themes, array($name => $theme->info['name']));
}
// Put the actual theme info objects into the array
foreach (array_keys($themes) as $name) {
$themes[$name] = $all_themes[$name];
}
// Make sure the current default theme always gets the last word
if ($current_key = array_search($current, array_keys($themes))) {
$themes += array_splice($themes, $current_key, 1);
}
}
return $themes;
}
/**
* Find all the base themes for the specified theme.
*
* Themes can inherit templates and function implementations from earlier themes.
*
* NOTE: this is a verbatim copy of system_find_base_themes(), which was not
* implemented until 6.14. It is included here only as a fallback for outdated
* versions of drupal core.
*
* @param $themes
* An array of available themes.
* @param $key
* The name of the theme whose base we are looking for.
* @param $used_keys
* A recursion parameter preventing endless loops.
* @return
* Returns an array of all of the theme's ancestors; the first element's value
* will be NULL if an error occurred.
*/
function ctools_find_base_themes($themes, $key, $used_keys = array()) {
$base_key = $themes[$key]->info['base theme'];
// Does the base theme exist?
if (!isset($themes[$base_key])) {
return array($base_key => NULL);
}
$current_base_theme = array($base_key => $themes[$base_key]->info['name']);
// Is the base theme itself a child of another theme?
if (isset($themes[$base_key]->info['base theme'])) {
// Do we already know the base themes of this theme?
if (isset($themes[$base_key]->base_themes)) {
return $themes[$base_key]->base_themes + $current_base_theme;
}
// Prevent loops.
if (!empty($used_keys[$base_key])) {
return array($base_key => NULL);
}
$used_keys[$base_key] = TRUE;
return ctools_find_base_themes($themes, $base_key, $used_keys) + $current_base_theme;
}
// If we get here, then this is our parent theme.
return $current_base_theme;
}
Earl Miles
committed
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
/**
* Load plugin info for the provided hook; this is handled separately from
* plugins from files.
*
* @param $info
* The info array about the plugin as created by ctools_plugin_get_info()
*
* @return
* An array of info supplied by any hook implementations.
*/
function ctools_plugin_load_hooks($info) {
$hooks = array();
foreach (module_implements($info['hook']) as $module) {
$result = ctools_plugin_process($info, $module, $module, drupal_get_path('module', $module));
if (is_array($result)) {
$hooks = array_merge($hooks, $result);
}
}
return $hooks;
}
/**
* Process a single hook implementation of a ctools plugin.
*
* @param $info
* The $info array about the plugin as returned by ctools_plugin_get_info()
* @param $module
* The module that implements the plugin being processed.
* @param $identifier
* The plugin identifier, which is used to create the name of the hook
* function being called.
* @param $path
* The path where files utilized by this plugin will be found.
* @param $file
* The file that was loaded for this plugin, if it exists.
* @param $base
* The base plugin name to use. If a file was loaded for the plugin, this
* is the plugin to assume must be present. This is used to automatically
* translate the array to make the syntax more friendly to plugin
* implementors.
Earl Miles
committed
*/
function ctools_plugin_process($info, $module, $identifier, $path, $file = NULL, $base = NULL) {
Earl Miles
committed
if (is_array($identifier)) {
$result = $identifier;
Earl Miles
committed
}
Earl Miles
committed
else {
$function = $identifier . '_' . $info['hook'];
if (!function_exists($function)) {
return NULL;
}
$result = $function();
if (!isset($result) || !is_array($result)) {
return NULL;
}
Earl Miles
committed
}
// Automatically convert to the proper format that lets plugin implementations
// not nest arrays as deeply as they used to, but still support the older
// format where they do:
if ($base && (!isset($result[$base]) || !is_array($result[$base]))) {
$result = array($base => $result);
}
return _ctools_process_data($result, $info, $module, $path, $file);
}
/**
* Fill in default values and run hooks for data loaded for one or
* more plugins.
*/
function _ctools_process_data($result, $info, $module, $path, $file) {
Earl Miles
committed
// Fill in defaults.
foreach ($result as $name => $plugin) {
$result[$name] += array(
'module' => $module,
'name' => $name,
'path' => $path,
Earl Miles
committed
'plugin module' => $info['module'],
'plugin type' => $info['type'],
Earl Miles
committed
);
// Fill in plugin specific defaults, if they exist.
Earl Miles
committed
if (!empty($info['defaults'])) {
if (is_array($info['defaults'])) {
$result[$name] += $info['defaults'];
}
else if (function_exists($info['defaults'])) {
$info['defaults']($info, $result[$name]);
}
Earl Miles
committed
}
// Allow the plugin owner to do additional processing.
if (!empty($info['process']) && function_exists($info['process'])) {
$info['process']($result[$name], $info);
}
Earl Miles
committed
}
return $result;
}
/**
* Process an info file for plugin information, rather than
* a hook.
*/
function ctools_plugin_process_info($info, $module, $file) {
$result = drupal_parse_info_file($file->filename);
if ($result) {
$result = array($file->name => $result);
return _ctools_process_data($result, $info, $module, dirname($file->filename), basename($file->filename));
}
}
Earl Miles
committed
/**
* Ask a module for info about a particular plugin type.
*/
function ctools_plugin_get_info($module, $type) {
$info = array();
$function = $module . '_ctools_plugin_' . $type;
if (function_exists($function)) {
$info = $function();
}
// Apply defaults. Array addition will not overwrite pre-existing keys.
$info += array(
'module' => $module,
'type' => $type,
'cache' => FALSE,
'cache table' => 'cache',
'use hooks' => FALSE,
Earl Miles
committed
'defaults' => array(),
'hook' => $module . '_' . $type,
'load themes' => FALSE,
Earl Miles
committed
);
return $info;
}
/**
* Get a function from a plugin, if it exists. If the plugin is not already
* loaded, try ctools_plugin_load_function() instead.
*
Earl Miles
committed
* The loaded plugin type.
* @param $function_name
* The identifier of the function. For example, 'settings form'.
*
* @return
* The actual name of the function to call, or NULL if the function
* does not exist.
*/
function ctools_plugin_get_function($plugin_definition, $function_name) {
Earl Miles
committed
// If cached the .inc file may not have been loaded. require_once is quite safe
// and fast so it's okay to keep calling it.
if (isset($plugin_definition['file'])) {
Earl Miles
committed
// Plugins that are loaded from info files have the info file as
// $plugin['file']. Don't try to run those.
$info = ctools_plugin_get_info($plugin_definition['plugin module'], $plugin_definition['plugin type']);
Earl Miles
committed
if (empty($info['info file'])) {
require_once DRUPAL_ROOT . '/' . $plugin_definition['path'] . '/' . $plugin_definition['file'];
Earl Miles
committed
}
Earl Miles
committed
}
Earl Miles
committed
if (!isset($plugin_definition[$function_name])) {
Earl Miles
committed
return;
}
if (is_array($plugin_definition[$function_name]) && isset($plugin_definition[$function_name]['function'])) {
$function = $plugin_definition[$function_name]['function'];
if (isset($plugin_definition[$function_name]['file'])) {
$file = $plugin_definition[$function_name]['file'];
if (isset($plugin_definition[$function_name]['path'])) {
$file = $plugin_definition[$function_name]['path'] . '/' . $file;
Earl Miles
committed
}
require_once DRUPAL_ROOT . '/' . $file;
Earl Miles
committed
}
}
else {
$function = $plugin_definition[$function_name];
Earl Miles
committed
}
if (function_exists($function)) {
return $function;
Earl Miles
committed
}
}
/**
* Load a plugin and get a function name from it, returning success only
* if the function exists.
*
* @param $module
* The module that owns the plugin type.
* @param $type
* The type of plugin.
* @param $id
* The id of the specific plugin to load.
* @param $function_name
* The identifier of the function. For example, 'settings form'.
*
* @return
* The actual name of the function to call, or NULL if the function
* does not exist.
*/
function ctools_plugin_load_function($module, $type, $id, $function_name) {
$plugin = ctools_get_plugins($module, $type, $id);
Earl Miles
committed
return ctools_plugin_get_function($plugin, $function_name);
}
Earl Miles
committed
/**
* Get a class from a plugin, if it exists. If the plugin is not already
* loaded, try ctools_plugin_load_class() instead.
*
Earl Miles
committed
* The loaded plugin type.
* @param $class_name
* The identifier of the class. For example, 'handler'.
* @param $abstract
* If true, will return abstract classes. Otherwise, parents will be included but nothing will be returned.
Earl Miles
committed
*
* @return
* The actual name of the class to call, or NULL if the class does not exist.
*/
function ctools_plugin_get_class($plugin_definition, $class_name, $abstract = FALSE) {
Earl Miles
committed
// If cached the .inc file may not have been loaded. require_once is quite safe
// and fast so it's okay to keep calling it.
if (isset($plugin_definition['file'])) {
Earl Miles
committed
// Plugins that are loaded from info files have the info file as
// $plugin['file']. Don't try to run those.
$info = ctools_plugin_get_info($plugin_definition['plugin module'], $plugin_definition['plugin type']);
Earl Miles
committed
if (empty($info['info file'])) {
require_once DRUPAL_ROOT . '/' . $plugin_definition['path'] . '/' . $plugin_definition['file'];
Earl Miles
committed
}
Earl Miles
committed
}
if (!isset($plugin_definition[$class_name])) {
Earl Miles
committed
return;
}
if (is_array($plugin_definition[$class_name]) && isset($plugin_definition[$class_name]['class'])) {
if (isset($plugin_definition[$class_name]['parent'])) {
Earl Miles
committed
// Make sure parents are included.
// TODO parent-loading needs to be better documented; the 'parent' designated
// on the plugin actually corresponds not to the name of the parent CLASS,
// but the name of the parent PLUGIN (and it then loads loads whatever
// class is in the same $class_name slot). Initially unintuitive.
ctools_plugin_load_class($plugin_definition['plugin module'], $plugin_definition['plugin type'], $plugin_definition[$class_name]['parent'], $class_name);
Earl Miles
committed
}
$class = $plugin_definition[$class_name]['class'];
if (isset($plugin_definition[$class_name]['file'])) {
$file = $plugin_definition[$class_name]['file'];
if (isset($plugin_definition[$class_name]['path'])) {
$file = $plugin_definition[$class_name]['path'] . '/' . $file;
Earl Miles
committed
}
require_once DRUPAL_ROOT . '/' . $file;
Earl Miles
committed
}
}
else {
$class = $plugin_definition[$class_name];
Earl Miles
committed
}
Earl Miles
committed
// If we didn't explicitly include a file above, try autoloading a file
// based on the class' name.
$default_file = DRUPAL_ROOT . '/' . $plugin_definition['path'] . "/$class.class.php";
if (!isset($file) && file_exists($default_file)) {
require_once $default_file;
Earl Miles
committed
}
if (class_exists($class) &&
(!is_array($plugin_definition[$class_name])
|| empty($plugin_definition[$class_name]['abstract'])
|| $abstract)) {
Earl Miles
committed
return $class;
}
}
/**
* Load a plugin and get a class name from it, returning success only if the
* class exists.
*
* @param $module
* The module that owns the plugin type.
* @param $type
* The type of plugin.
* @param $id
* The id of the specific plugin to load.
* @param $class_name
* The identifier of the class. For example, 'handler'.
* @param $abstract
* If true, will tell ctools_plugin_get_class to allow the return of abstract classes.
Earl Miles
committed
*
* @return
* The actual name of the class to call, or NULL if the class does not exist.
*/
function ctools_plugin_load_class($module, $type, $id, $class_name, $abstract = FALSE) {
Earl Miles
committed
$plugin = ctools_get_plugins($module, $type, $id);
return ctools_plugin_get_class($plugin, $class_name, $abstract);