$t('MODULENAME required Chaos Tool Suite (CTools) API Version'),
* 'value' => t('Between @a and @b', array('@a' => MODULENAME_MINIMUM_CTOOLS_API_VERSION, '@b' => MODULENAME_MAXIMUM_CTOOLS_API_VERSION)),
* 'severity' => REQUIREMENT_ERROR,
* );
* }
* return $requirements;
* }
* @endcode
*
* Please note that the version is a string, not an floating point number.
* This will matter once CTools reaches version 1.10.
*
* A CTools API changes history will be kept in API.txt. Not every new
* version of CTools will necessarily update the API version.
* @param $minimum
* The minimum version of CTools necessary for your software to run with it.
* @param $maximum
* The maximum version of CTools allowed for your software to run with it.
*/
function ctools_api_version($minimum, $maximum = NULL) {
if (version_compare(CTOOLS_API_VERSION, $minimum, '<')) {
return FALSE;
}
if (isset($maximum) && version_compare(CTOOLS_API_VERSION, $maximum, '>')) {
return FALSE;
}
return TRUE;
}
/**
* Include .inc files as necessary.
*
* This fuction is helpful for including .inc files for your module. The
* general case is including ctools funcitonality like this:
*
* @code
* ctools_include('plugins');
* @endcode
*
* Similar funcitonality can be used for other modules by providing the $module
* and $dir arguments like this:
*
* @code
* // include mymodule/includes/import.inc
* ctools_include('import', 'mymodule');
* // include mymodule/plugins/foobar.inc
* ctools_include('foobar', 'mymodule', 'plugins');
* @endcode
*
* @param $file
* The base file name to be included.
* @param $module
* Optional module containing the include.
* @param $dir
* Optional subdirectory containing the include file.
*/
function ctools_include($file, $module = 'ctools', $dir = 'includes') {
static $used = array();
if (!isset($used[$module][$dir][$file])) {
require_once './' . drupal_get_path('module', $module) . "/$dir/$file.inc";
}
$used[$file] = TRUE;
}
/**
* Provide the proper path to an image as necessary.
*
* This helper function is used by ctools but can also be used in other
* modules in the same way as explained in the comments of ctools_include.
*
* @param $image
* The base file name (with extension) of the image to be included.
* @param $module
* Optional module containing the include.
* @param $dir
* Optional subdirectory containing the include file.
*/
function ctools_image_path($image, $module = 'ctools', $dir = 'images') {
return drupal_get_path('module', $module) . "/$dir/" . $image;
}
/**
* Include css files as necessary.
*
* This helper function is used by ctools but can also be used in other
* modules in the same way as explained in the comments of ctools_include.
*
* @param $file
* The base file name to be included.
* @param $module
* Optional module containing the include.
* @param $dir
* Optional subdirectory containing the include file.
*/
function ctools_add_css($file, $module = 'ctools', $dir = 'css') {
drupal_add_css(drupal_get_path('module', $module) . "/$dir/$file.css");
}
/**
* Include js files as necessary.
*
* This helper function is used by ctools but can also be used in other
* modules in the same way as explained in the comments of ctools_include.
*
* @param $file
* The base file name to be included.
* @param $module
* Optional module containing the include.
* @param $dir
* Optional subdirectory containing the include file.
*/
function ctools_add_js($file, $module = 'ctools', $dir = 'js') {
drupal_add_js(drupal_get_path('module', $module) . "/$dir/$file.js");
}
/**
* Simple function to add a page id to Drupal.settings.CTools.
*/
function ctools_add_page_id() {
static $page_id = NULL;
if (!isset($page_id)) {
$page_id = 'page-' . md5(mt_rand());
drupal_add_js(array('CTools' => array('pageId' => $page_id)), 'setting');
}
return $page_id;
}
/**
* Implement hook_init to keep our global CSS at the ready.
*/
function ctools_init() {
ctools_add_css('ctools');
// If we are sure that CTools' AJAX is in use, change the error handling.
if (!empty($_REQUEST['ctools_ajax'])) {
ini_set('display_errors', 0);
register_shutdown_function('ctools_shutdown_handler');
}
else {
// Add a setting into the JS that identifies this page.
ctools_add_page_id();
}
}
/**
* Shutdown handler used during ajax operations to help catch fatal errors.
*/
function ctools_shutdown_handler() {
if (function_exists('error_get_last') AND ($error = error_get_last())){
switch($error['type']){
case E_ERROR:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
// Do this manually because including files here is dangerous.
$commands = array(
array(
'command' => 'alert',
'title' => t('Error'),
'text' => t('Unable to complete operation. Fatal error in @file on line @line: @message', array(
'@file' => $error['file'],
'@line' => $error['line'],
'@message' => $error['message'],
)),
),
);
// Change the status code so that the client will read the AJAX returned.
header('HTTP/1.1 200 OK');
drupal_json($commands);
}
}
}
/**
* Central static variable storage. Modeled after Drupal 7's drupal_static().
*
* @param $name
* Globally unique name for the variable. For a function with only one static,
* variable, the function name (e.g. via the PHP magic __FUNCTION__ constant)
* is recommended. For a function with multiple static variables add a
* distinguishing suffix to the function name for each one.
* @param $default_value
* Optional default value.
* @return $reset
* TRUE to reset a specific named variable, or all variables if $name is NULL.
* Resetting every variable should only be used, for example, for running unit
* tests with a clean environment. Should be used only though via function
* ctools_static_reset().
*/
function &ctools_static($name, $default_value = NULL, $reset = FALSE) {
static $data = array();
// Reset a single value, or all values.
if ($reset) {
if (isset($name)) {
unset($data[$name]);
}
else {
$data = array();
}
// We must return a reference to a variable.
$dummy = NULL;
return $dummy;
}
if (!isset($data[$name])) {
$data[$name] = $default_value;
}
return $data[$name];
}
/**
* Reset one or all centrally stored static variable(s).
* Modeled after Drupal 7's drupal_static_reset().
*
* @param $name
* Name of the static variable to reset. Omit to reset all variables.
*/
function ctools_static_reset($name) {
ctools_static($name, NULL, TRUE);
}
/**
* Provide a hook passthrough to included files.
*
* To organize things neatly, each CTools tool gets its own toolname.$type.inc
* file. If it exists, it's loaded and ctools_$tool_$type() is executed.
* To save time we pass the $items array in so we don't need to do array
* addition. It modifies the array by reference and doesn't need to return it.
*/
function _ctools_passthrough(&$items, $type = 'theme') {
$files = drupal_system_listing('.' . $type . '.inc$', drupal_get_path('module', 'ctools') . '/includes', 'name', 0);
foreach ($files as $file) {
require_once './' . $file->filename;
list($tool) = explode('.', $file->name, 2);
$function = 'ctools_' . $tool . '_' . $type;
if (function_exists($function)) {
$function($items);
}
}
}
function ctools_theme_registry_alter(&$registry) {
if ($registry['menu_local_tasks']['function'] == 'theme_menu_local_tasks') {
$registry['menu_local_tasks'] = array(
'function' => 'ctools_theme_menu_local_tasks',
'path' => drupal_get_path('module', 'ctools') . '/includes',
'file' => 'menu.inc',
) + $registry['menu_local_tasks'];
}
if (isset($registry['help']['function']) && $registry['help']['function'] == 'theme_help') {
$registry['help'] = array(
'function' => 'ctools_menu_help',
'path' => drupal_get_path('module', 'ctools') . '/includes',
'file' => 'menu.inc',
) + $registry['help'];
}
// Handle a special override for garland because it's cute and does its own
// thing with tabs and we can't ask users to edit a core theme for us.
if ($registry['menu_local_tasks']['function'] == 'phptemplate_menu_local_tasks' &&
$registry['menu_local_tasks']['theme paths'][1] == 'themes/garland') {
$registry['menu_local_tasks'] = array(
'function' => 'ctools_garland_menu_local_tasks',
'path' => drupal_get_path('module', 'ctools') . '/includes',
'file' => 'menu.inc',
) + $registry['menu_local_tasks'];
}
if (isset($registry['page']['preprocess functions'][2]) &&
$registry['page']['preprocess functions'][2] == 'phptemplate_preprocess_page' &&
$registry['page']['theme paths'][1] == 'themes/garland') {
$registry['page']['preprocess functions'][2] = 'ctools_garland_preprocess_page';
}
}
/**
* Override or insert PHPTemplate variables into the templates.
*
* This needs to be in the .module file to ensure availability; we can't change the
* paths or it won't be able to find templates.
*/
function ctools_garland_preprocess_page(&$vars) {
$vars['tabs2'] = ctools_menu_secondary_local_tasks();
// Hook into color.module
if (module_exists('color')) {
_color_page_alter($vars);
}
}
/**
* Implementation of hook_theme().
*/
function ctools_theme() {
$items = array();
_ctools_passthrough($items, 'theme');
return $items;
}
/**
* Implementation of hook_menu().
*/
function ctools_menu() {
$items = array();
_ctools_passthrough($items, 'menu');
return $items;
}
/**
* Implementation of hook_ctools_plugin_dierctory() to let the system know
* we implement task and task_handler plugins.
*/
function ctools_ctools_plugin_directory($module, $plugin) {
if ($module == 'ctools') {
return 'plugins/' . $plugin;
}
}
/**
* Implementation of hook_js_replacements().
* This is a hook that is not a standard yet. We hope jquery_update and others
* will expose this hook to inform modules which scripts they are modifying
* in the theme layer.
* The return format is $scripts[$type][$old_path] = $new_path.
*/
function ctools_js_replacements() {
$replacements = array();
// Until jquery_update is released with its own replacement hook, we will
// take those replacements into account here.
if (module_exists('jquery_update')) {
$replacements = array_merge_recursive($replacements, jquery_update_get_replacements());
foreach ($replacements as $type => $type_replacements) {
foreach ($type_replacements as $old_path => $new_filename) {
$replacements[$type][$old_path] = JQUERY_UPDATE_REPLACE_PATH . "/$new_filename";
}
}
$replacements['core']['misc/jquery.js'] = jquery_update_jquery_path();
}
return $replacements;
}
/**
* Get a list of roles in the system.
*/
function ctools_get_roles() {
static $roles = NULL;
if (!isset($roles)) {
$roles = array();
$result = db_query("SELECT r.rid, r.name FROM {role} r ORDER BY r.name");
while ($obj = db_fetch_object($result)) {
$roles[$obj->rid] = $obj->name;
}
}
return $roles;
}
/**
* Determine if the current user has access via a plugin.
*
* This function is meant to be embedded in the Drupal menu system, and
* therefore is in the .module file since sub files can't be loaded, and
* takes arguments a little bit more haphazardly than ctools_access().
*
* @param $access
* An access control array which contains the following information:
* - 'logic': and or or. Whether all tests must pass or one must pass.
* - 'plugins': An array of access plugins. Each contains:
* - - 'name': The name of the plugin
* - - 'settings': The settings from the plugin UI.
* - - 'context': Which context to use.
* @param ...
* zero or more context arguments generated from argument plugins. These
* contexts must have an 'id' attached to them so that they can be
* properly associated. The argument plugin system should set this, but
* if the context is coming from elsewhere it will need to be set manually.
*
* @return
* TRUE if access is granted, false if otherwise.
*/
function ctools_access_menu($access) {
// Short circuit everything if there are no access tests.
if (empty($access['plugins'])) {
return TRUE;
}
$contexts = array();
foreach (func_get_args() as $arg) {
if (is_object($arg) && get_class($arg) == 'ctools_context') {
$contexts[$arg->id] = $arg;
}
}
ctools_include('context');
return ctools_access($access, $contexts);
}
/**
* Determine if the current user has access via checks to multiple different
* permissions.
*
* This function is a thin wrapper around user_access that allows multiple
* permissions to be easily designated for use on, for example, a menu callback.
*
* @param ...
* An indexed array of zero or more permission strings to be checked by
* user_access().
*
* @return
* Iff all checks pass will this function return TRUE. If an invalid argument
* is passed (e.g., not a string), this function errs on the safe said and
* returns FALSE.
*/
function ctools_access_multiperm() {
foreach (func_get_args() as $arg) {
if (!is_string($arg) || !user_access($arg)) {
return FALSE;
}
}
return TRUE;
}
/**
* Implementation of hook_cron. Clean up old caches.
*/
function ctools_cron() {
if (variable_get('ctools_last_cron', 0) < time() - 86400) {
variable_set('ctools_last_cron', time());
ctools_include('object-cache');
ctools_object_cache_clean();
}
}
/*
* Break x,y,z and x+y+z into an array. Numeric only.
*
* @param $str
* The string to parse.
*
* @return $object
* An object containing
* - operator: Either 'and' or 'or'
* - value: An array of numeric values.
*/
function ctools_break_phrase($str) {
$object = new stdClass();
if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
// The '+' character in a query string may be parsed as ' '.
$object->operator = 'or';
$object->value = preg_split('/[+ ]/', $str);
}
else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
$object->operator = 'and';
$object->value = explode(',', $str);
}
// Keep an 'error' value if invalid strings were given.
if (!empty($str) && (empty($object->value) || !is_array($object->value))) {
$object->value = array(-1);
$object->invalid_input = TRUE;
return $object;
}
if (empty($object->value)) {
$object->value = array();
}
// Doubly ensure that all values are numeric only.
foreach ($object->value as $id => $value) {
$object->value[$id] = intval($value);
}
return $object;
}
/**
* A theme preprocess function to automatically allow panels-based node
* templates based upon input when the panel was configured.
*/
function ctools_preprocess_node(&$vars) {
// The 'panel_identifier' attribute of the node is added when the pane is
// rendered.
if (!empty($vars['node']->panel_identifier)) {
$vars['panel_identifier'] = check_plain($vars['node']->panel_identifier);
$vars['template_files'][] = 'node-panel-' . check_plain($vars['node']->panel_identifier);
}
}
/**
* Ensure the CTools CSS cache is flushed whenever hook_flush_caches is invoked.
*/
function ctools_flush_caches() {
ctools_include('css');
ctools_css_flush_caches();
}
/**
* Check to see if the incoming menu item is js capable or not.
*
* This can be used as %ctools_js as part of a path in hook menu. CTools
* ajax functions will automatically change the phrase 'nojs' to 'ajax'
* when it attaches ajax to a link. This can be used to autodetect if
* that happened.
*/
function ctools_js_load($js) {
if ($js == 'ajax') {
return TRUE;
}
return 0;
}
/**
* A theme preprocess function to allow content type plugins to use page
* template variables which are not yet available when the content type is
* rendered.
*/
function ctools_preprocess_page(&$variables) {
$tokens = ctools_set_page_token();
if (!empty($tokens)) {
foreach ($tokens as $token => $key) {
list($type, $argument) = $key;
switch ($type) {
case 'variable':
$tokens[$token] = isset($variables[$argument]) ? $variables[$argument] : '';
break;
case 'callback':
if (is_string($argument) && function_exists($argument)) {
$tokens[$token] = $argument();
}
if (is_array($argument) && function_exists($argument[0])) {
$tokens[$token] = call_user_func_array($argument);
}
break;
}
}
$variables['content'] = strtr($variables['content'], $tokens);
}
// Go through all the JS files being added to the page and catalog them.
$js_files = array();
foreach (drupal_add_js() as $type => $data) {
if ($type != 'inline' && $type != 'setting') {
foreach ($data as $path => $info) {
$js_files[$path] = TRUE;
}
}
}
// Go through all CSS files that are being added to the page and catalog them.
$css_files = array();
$css = drupal_add_css();
foreach ($css as $media => $types) {
// If CSS preprocessing is off, we still need to output the styles.
// Additionally, go through any remaining styles if CSS preprocessing is on and output the non-cached ones.
foreach ($types as $type => $files) {
if ($type == 'module') {
// Setup theme overrides for module styles.
$theme_styles = array();
foreach (array_keys($css[$media]['theme']) as $theme_style) {
$theme_styles[] = basename($theme_style);
}
}
foreach ($types[$type] as $file => $preprocess) {
// If the theme supplies its own style using the name of the module style, skip its inclusion.
// This includes any RTL styles associated with its main LTR counterpart.
if ($type == 'module' && in_array(str_replace('-rtl.css', '.css', basename($file)), $theme_styles)) {
// Unset the file to prevent its inclusion when CSS aggregation is enabled.
unset($types[$type][$file]);
continue;
}
// Only include the stylesheet if it exists.
if (file_exists($file)) {
$css_files[$file] = TRUE;
}
}
}
}
// Cache the cataloged JS and CSS so they can be found by an ajax request.
$page_id = ctools_add_page_id();
$expire = CACHE_TEMPORARY;
// If the page cache is enabled, respect its cache lifetime.
// @TODO: do this for only user_is_anonymous()?
if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
$expire = variable_get('cache_lifetime', 0);
}
cache_set($page_id, array('js' => $js_files, 'css' => $css_files), 'cache', $expire);
}
/**
* Set a token/value pair to be replaced later in the request, specifically in
* ctools_preprocess_page().
*
* @param $token
* The token to be replaced later, during page rendering. This should
* ideally be a string inside of an HTML comment, so that if there is
* no replacement, the token will not render on the page.
* @param $type
* The type of the token. Can be either 'variable', which will pull data
* directly from the page variables
* @param $argument
* If $type == 'variable' then argument should be the key to fetch from
* the $variables. If $type == 'callback' then it should either be the
* callback, or an array that will be sent to call_user_func_array().
*
* @return
* A array of token/variable names to be replaced.
*/
function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
static $tokens = array();
if (isset($token)) {
$tokens[$token] = array($type, $argument);
}
return $tokens;
}
/**
* Easily set a token from the page variables.
*
* This function can be used like this:
* $token = ctools_set_variable_token('tabs');
*
* $token will then be a simple replacement for the 'tabs' about of the
* variables available in the page template.
*/
function ctools_set_variable_token($token) {
$string = '';
ctools_set_page_token($string, 'variable', $token);
return $string;
}
/**
* Easily set a token from the page variables.
*
* This function can be used like this:
* $token = ctools_set_variable_token('id', 'mymodule_myfunction');
*/
function ctools_set_callback_token($token, $callback) {
$string = '';
ctools_set_page_token($string, 'callback', $callback);
return $string;
}
/**
* Provide a search form with a different id so that form_alters will miss it
* and thus not get advanced search settings.
*/
function ctools_forms() {
$forms['ctools_search_form']= array(
'callback' => 'search_form',
);
return $forms;
}
/**
* Check to see that a directory exists.
*
* This is an exact copy of core's, but without the stupid drupal_set_message
* because we need this to be silent.
*/
function ctools_file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
$directory = rtrim($directory, '/\\');
// Check if directory exists.
if (!is_dir($directory)) {
if (($mode & FILE_CREATE_DIRECTORY) && @mkdir($directory)) {
@chmod($directory, 0775); // Necessary for non-webserver users.
}
else {
if ($form_item) {
form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory)));
}
return FALSE;
}
}
// Check to see if the directory is writable.
if (!is_writable($directory)) {
if (($mode & FILE_MODIFY_PERMISSIONS) && @chmod($directory, 0775)) {
drupal_set_message(t('The permissions of directory %directory have been changed to make it writable.', array('%directory' => $directory)));
}
else {
form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory)));
watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR);
return FALSE;
}
}
if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) {
$htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) {
fclose($fp);
chmod($directory .'/.htaccess', 0664);
}
else {
$variables = array('%directory' => $directory, '!htaccess' => '
'. nl2br(check_plain($htaccess_lines)));
form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess
", $variables));
watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess
", $variables, WATCHDOG_ERROR);
}
}
return TRUE;
}