first import

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-08 11:40:19 +02:00
commit 1bc61b12ad
8435 changed files with 1582817 additions and 0 deletions

View File

@ -0,0 +1,33 @@
<?php
/**
* @file
* Theme function for wrapping menu local actions.
*/
/**
* Delegated implementation of hook_theme()
*/
function ctools_action_links_theme(&$items) {
$items['ctools_menu_local_actions_wrapper'] = array(
'render element' => 'links',
'file' => 'includes/action-links.theme.inc',
);
}
/**
* Render a menu local actions wrapper.
*
* @param $links
* Local actions links.
* @param $attributes
* An array of attributes to append to the wrapper.
*/
function theme_ctools_menu_local_actions_wrapper($variables) {
$links = drupal_render($variables['links']);
if (empty($links)) {
return;
}
return '<ul class="action-links">' . $links . '</ul>';
}

View File

@ -0,0 +1,129 @@
<?php
// Set this so we can tell that the file has been included at some point.
define('CTOOLS_AJAX_INCLUDED', 1);
/**
* @file
* Extend core AJAX with some of our own stuff.
*/
/**
* Render an image as a button link. This will automatically apply an AJAX class
* to the link and add the appropriate javascript to make this happen.
*
* @param $image
* The path to an image to use that will be sent to theme('image') for rendering.
* @param $dest
* The destination of the link.
* @param $alt
* The alt text of the link.
* @param $class
* Any class to apply to the link. @todo this should be a options array.
*/
function ctools_ajax_image_button($image, $dest, $alt, $class = '') {
return ctools_ajax_text_button(theme('image', array('path' => $image)), $dest, $alt, $class);
}
/**
* Render text as a link. This will automatically apply an AJAX class
* to the link and add the appropriate javascript to make this happen.
*
* Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
* not use user input so this is a very minor concern.
*
* @param $text
* The text that will be displayed as the link.
* @param $dest
* The destination of the link.
* @param $alt
* The alt text of the link.
* @param $class
* Any class to apply to the link. @todo this should be a options array.
* @param $type
* A type to use, in case a different behavior should be attached. Defaults
* to ctools-use-ajax.
*/
function ctools_ajax_text_button($text, $dest, $alt, $class = '', $type = 'use-ajax') {
drupal_add_library('system', 'drupal.ajax');
return l($text, $dest, array('html' => TRUE, 'attributes' => array('class' => array($type, $class), 'title' => $alt)));
}
/**
* Set a single property to a value, on all matched elements.
*
* @param $selector
* The CSS selector. This can be any selector jquery uses in $().
* @param $name
* The name or key: of the data attached to this selector.
* @param $value
* The value of the data.
*/
function ctools_ajax_command_attr($selector, $name, $value) {
ctools_add_js('ajax-responder');
return array(
'command' => 'attr',
'selector' => $selector,
'name' => $name,
'value' => $value,
);
}
/**
* Force a client-side redirect.
*
* @param $url
* The url to be redirected to. This can be an absolute URL or a
* Drupal path.
* @param $delay
* A delay before applying the redirection, in milliseconds.
* @param $options
* An array of options to pass to the url() function.
*/
function ctools_ajax_command_redirect($url, $delay = 0, $options = array()) {
ctools_add_js('ajax-responder');
return array(
'command' => 'redirect',
'url' => url($url, $options),
'delay' => $delay,
);
}
/**
* Force a reload of the current page.
*/
function ctools_ajax_command_reload() {
ctools_add_js('ajax-responder');
return array(
'command' => 'reload',
);
}
/**
* Submit a form.
*
* This is useful for submitting a parent form after a child form has finished
* processing in a modal overlay.
*
* @param $selector
* The CSS selector to identify the form for submission. This can be any
* selector jquery uses in $().
*/
function ctools_ajax_command_submit($selector) {
ctools_add_js('ajax-responder');
return array(
'command' => 'submit',
'selector' => $selector,
);
}
/**
* Send an error response back via AJAX and immediately exit.
*/
function ctools_ajax_render_error($error = '') {
$commands = array();
$commands[] = ajax_command_alert($error);
print ajax_render($commands);
exit;
}

View File

@ -0,0 +1,169 @@
<?php
/**
* @file
*
* Plugins to handle cache-indirection.
*
* Simple plugin management to allow clients to more tightly control where
* object caches are stored.
*
* CTools provides an object cache mechanism, and it also provides a number
* of subsystems that are designed to plug into larger systems. When doing
* caching on multi-step forms (in particular during AJAX operations) these
* subsystems often need to operate their own cache. In reality, its best
* for everyone if they are able to piggyback off of the larger cache.
*
* This system allows this by registering plugins to control where caches
* are actually stored. For the most part, the subsystems could care less
* where the data is fetched from and stored to. All it needs to know is
* that it can 'get', 'set' and 'clear' caches. Additionally, some caches
* might need extra operations such as 'lock' and 'finalize', and other
* operations may be needed based upon the specific uses for the cache
* plugins.
*
* To utilize cache plugins, there are two pieces of data. First, there is
* the mechanism, which is simply the name of the plugin to use. CTools
* provides a 'simple' mechanism which goes straight through to the object
* cache. The second piece of data is the 'key' which is a unique identifier
* that can be used to find the data needed. Keys can be generated any number
* of ways, and the plugin must be agnostic about the key itself.
*
* That said, the 'mechanism' can be specified as pluginame::data and that
* data can be used to derive additional data. For example, it is often
* desirable to NOT store any cached data until original data (i.e, user
* input) has been received. The data can be used to derive this original
* data so that when a 'get' is called, if the cache is missed it can create
* the data needed. This can help prevent unwanted cache entries from
* building up just by visiting edit UIs without actually modifying anything.
*
* Modules wishing to implement cache indirection mechanisms need to implement
* a plugin of type 'cache' for the module 'ctools' and provide the .inc file.
* It should provide callbacks for 'cache set', 'cache get', and 'cache clear'.
* It can provide callbacks for 'break' and 'finalize' if these are relevant
* to the caching mechanism (i.e, for use with locking caches such as the page
* manager cache). Other operations may be utilized but at this time are not part
* of CTools.
*/
/**
* Fetch data from an indirect cache.
*
* @param string $mechanism
* A string containing the plugin name, and an optional data element to
* send to the plugin separated by two colons.
*
* @param string $key
* The key used to identify the cache.
*
* @return mixed
* The cached data. This can be any format as the plugin does not necessarily
* have knowledge of what is being cached.
*/
function ctools_cache_get($mechanism, $key) {
return ctools_cache_operation($mechanism, $key, 'get');
}
/**
* Store data in an indirect cache.
*
* @param string $mechanism
* A string containing the plugin name, and an optional data element to
* send to the plugin separated by two colons.
*
* @param string $key
* The key used to identify the cache.
*
* @param mixed $object
* The data to cache. This can be any format as the plugin does not
* necessarily have knowledge of what is being cached.
*/
function ctools_cache_set($mechanism, $key, $object) {
return ctools_cache_operation($mechanism, $key, 'set', $object);
}
/**
* Clear data from an indirect cache.
*
* @param string $mechanism
* A string containing the plugin name, and an optional data element to
* send to the plugin separated by two colons.
*
* @param string $key
* The key used to identify the cache.
*/
function ctools_cache_clear($mechanism, $key) {
return ctools_cache_operation($mechanism, $key, 'clear');
}
/**
* Perform a secondary operation on an indirect cache.
*
* Additional operations, beyond get, set and clear may be items
* such as 'break' and 'finalize', which are needed to support cache
* locking. Other operations may be added by users of the indirect
* caching functions as needed.
*
* @param string $mechanism
* A string containing the plugin name, and an optional data element to
* send to the plugin separated by two colons.
*
* @param string $key
* The key used to identify the cache.
*
* @param string $op
* The operation to call, such as 'break' or 'finalize'.
*
* @param mixed $object
* The cache data being operated on, in case it is necessary. This is
* optional so no references should be used.
*
* @return mixed
* The operation may or may not return a value.
*/
function ctools_cache_operation($mechanism, $key, $op, $object = NULL) {
list($plugin, $data) = ctools_cache_find_plugin($mechanism);
if (empty($plugin)) {
return;
}
$function = ctools_plugin_get_function($plugin, "cache $op");
if (empty($function)) {
return;
}
return $function($data, $key, $object, $op);
}
/**
* Take a mechanism and return a plugin and data.
*
* @param string $mechanism
* A string containing the plugin name, and an optional data element to
* send to the plugin separated by two colons.
*
* @return array
* An array, the first element will be the plugin and the second element
* will be the data. If the plugin could not be found, the $plugin will
* be NULL.
*/
function ctools_cache_find_plugin($mechanism) {
if (strpos($mechanism, '::') !== FALSE) {
// use explode(2) to ensure that the data can contain double
// colons, just in case.
list($name, $data) = explode('::', $mechanism, 2);
}
else {
$name = $mechanism;
$data = '';
}
if (empty($name)) {
return array(NULL, $data);
}
ctools_include('plugins');
$plugin = ctools_get_plugins('ctools', 'cache', $name);
return array($plugin, $data);
}

View File

@ -0,0 +1,11 @@
<?php
/**
* @file
* Contains plugin type registration information for the cache tool.
*/
function ctools_cache_plugin_type(&$items) {
// There are no options to declare on this plugin at this time.
$items['cache'] = array();
}

View File

@ -0,0 +1,204 @@
<?php
// $Id $
/**
* @file
* Helper class to clean strings to make them URL safe and translatable.
*
* This was copied directly from pathauto and put here to be made available
* to all, because more things than just pathauto want URL safe strings.
*
* To use, simply:
* @code
* ctools_include('cleanstring');
* $output = ctools_cleanstring($string);
*
* You can add a variety of settings as an array in the second argument,
* including words to ignore, how to deal with punctuation, length
* limits, and more. See the function itself for options.
*/
/**
* Matches Unicode character classes.
*
* See: http://www.unicode.org/Public/UNIDATA/UCD.html#General_Category_Values
*
* The index only contains the following character classes:
* Lu Letter, Uppercase
* Ll Letter, Lowercase
* Lt Letter, Titlecase
* Lo Letter, Other
* Nd Number, Decimal Digit
* No Number, Other
*
* Copied from search.module's PREG_CLASS_SEARCH_EXCLUDE.
*/
define('CTOOLS_PREG_CLASS_ALNUM',
'\x{0}-\x{2f}\x{3a}-\x{40}\x{5b}-\x{60}\x{7b}-\x{bf}\x{d7}\x{f7}\x{2b0}-' .
'\x{385}\x{387}\x{3f6}\x{482}-\x{489}\x{559}-\x{55f}\x{589}-\x{5c7}\x{5f3}-' .
'\x{61f}\x{640}\x{64b}-\x{65e}\x{66a}-\x{66d}\x{670}\x{6d4}\x{6d6}-\x{6ed}' .
'\x{6fd}\x{6fe}\x{700}-\x{70f}\x{711}\x{730}-\x{74a}\x{7a6}-\x{7b0}\x{901}-' .
'\x{903}\x{93c}\x{93e}-\x{94d}\x{951}-\x{954}\x{962}-\x{965}\x{970}\x{981}-' .
'\x{983}\x{9bc}\x{9be}-\x{9cd}\x{9d7}\x{9e2}\x{9e3}\x{9f2}-\x{a03}\x{a3c}-' .
'\x{a4d}\x{a70}\x{a71}\x{a81}-\x{a83}\x{abc}\x{abe}-\x{acd}\x{ae2}\x{ae3}' .
'\x{af1}-\x{b03}\x{b3c}\x{b3e}-\x{b57}\x{b70}\x{b82}\x{bbe}-\x{bd7}\x{bf0}-' .
'\x{c03}\x{c3e}-\x{c56}\x{c82}\x{c83}\x{cbc}\x{cbe}-\x{cd6}\x{d02}\x{d03}' .
'\x{d3e}-\x{d57}\x{d82}\x{d83}\x{dca}-\x{df4}\x{e31}\x{e34}-\x{e3f}\x{e46}-' .
'\x{e4f}\x{e5a}\x{e5b}\x{eb1}\x{eb4}-\x{ebc}\x{ec6}-\x{ecd}\x{f01}-\x{f1f}' .
'\x{f2a}-\x{f3f}\x{f71}-\x{f87}\x{f90}-\x{fd1}\x{102c}-\x{1039}\x{104a}-' .
'\x{104f}\x{1056}-\x{1059}\x{10fb}\x{10fc}\x{135f}-\x{137c}\x{1390}-\x{1399}' .
'\x{166d}\x{166e}\x{1680}\x{169b}\x{169c}\x{16eb}-\x{16f0}\x{1712}-\x{1714}' .
'\x{1732}-\x{1736}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17db}\x{17dd}' .
'\x{17f0}-\x{180e}\x{1843}\x{18a9}\x{1920}-\x{1945}\x{19b0}-\x{19c0}\x{19c8}' .
'\x{19c9}\x{19de}-\x{19ff}\x{1a17}-\x{1a1f}\x{1d2c}-\x{1d61}\x{1d78}\x{1d9b}-' .
'\x{1dc3}\x{1fbd}\x{1fbf}-\x{1fc1}\x{1fcd}-\x{1fcf}\x{1fdd}-\x{1fdf}\x{1fed}-' .
'\x{1fef}\x{1ffd}-\x{2070}\x{2074}-\x{207e}\x{2080}-\x{2101}\x{2103}-\x{2106}' .
'\x{2108}\x{2109}\x{2114}\x{2116}-\x{2118}\x{211e}-\x{2123}\x{2125}\x{2127}' .
'\x{2129}\x{212e}\x{2132}\x{213a}\x{213b}\x{2140}-\x{2144}\x{214a}-\x{2b13}' .
'\x{2ce5}-\x{2cff}\x{2d6f}\x{2e00}-\x{3005}\x{3007}-\x{303b}\x{303d}-\x{303f}' .
'\x{3099}-\x{309e}\x{30a0}\x{30fb}-\x{30fe}\x{3190}-\x{319f}\x{31c0}-\x{31cf}' .
'\x{3200}-\x{33ff}\x{4dc0}-\x{4dff}\x{a015}\x{a490}-\x{a716}\x{a802}\x{a806}' .
'\x{a80b}\x{a823}-\x{a82b}\x{e000}-\x{f8ff}\x{fb1e}\x{fb29}\x{fd3e}\x{fd3f}' .
'\x{fdfc}-\x{fe6b}\x{feff}-\x{ff0f}\x{ff1a}-\x{ff20}\x{ff3b}-\x{ff40}\x{ff5b}-' .
'\x{ff65}\x{ff70}\x{ff9e}\x{ff9f}\x{ffe0}-\x{fffd}');
/**
* Clean up a string value provided by a module.
*
* Resulting string contains only alphanumerics and separators.
*
* @param $string
* A string to clean.
* @param $settings
* An optional array of settings to use.
* - 'clean slash': If set, slashes will be cleaned. Defaults to TRUE,
* so you have to explicitly set this to FALSE to not clean the
* slashes.
* - 'ignore words': Set to an array of words that will be removed
* rather than made safe. Defaults to an empty array.
* - 'separator': Change spaces and untranslatable characters to
* this character. Defaults to '-' .
* - 'replacements': An array of direct replacements to be made that will
* be implemented via strtr(). Defaults to an empty array.
* - 'transliterate': If set, use the transliteration replacements. If set
* to an array, use these replacements instead of the defaults in CTools.
* Defaults to FALSE.
* - 'reduce ascii': If set to TRUE further reduce to ASCII96 only. Defaults
* to TRUE.
* - 'max length': If set to a number, reduce the resulting string to this
* maximum length. Defaults to no maximum length.
* - 'lower case': If set to TRUE, convert the result to lower case.
* Defaults to false.
* These settings will be passed through drupal_alter.
*
* @return
* The cleaned string.
*/
function ctools_cleanstring($string, $settings = array()) {
$settings += array(
'clean slash' => TRUE,
'ignore words' => array(),
'separator' => '-',
'replacements' => array(),
'transliterate' => FALSE,
'reduce ascii' => TRUE,
'max length' => FALSE,
'lower case' => FALSE,
);
// Allow modules to make other changes to the settings.
if (isset($settings['clean id'])) {
drupal_alter('ctools_cleanstring_' . $settings['clean id'], $settings);
}
drupal_alter('ctools_cleanstring', $settings);
$output = $string;
// Do any replacements the user selected up front.
if (!empty($settings['replacements'])) {
$output = strtr($output, $settings['replacements']);
}
// Remove slashes if instructed to do so.
if ($settings['clean slash']) {
$output = str_replace('/', '', $output);
}
if (!empty($settings['transliterate']) && module_exists('transliteration')) {
$output = transliteration_get($output);
}
// Reduce to the subset of ASCII96 letters and numbers
if ($settings['reduce ascii']) {
$pattern = '/[^a-zA-Z0-9\/]+/';
$output = preg_replace($pattern, $settings['separator'], $output);
}
// Get rid of words that are on the ignore list
if (!empty($settings['ignore words'])) {
$ignore_re = '\b' . preg_replace('/,/', '\b|\b', $settings['ignore words']) . '\b';
if (function_exists('mb_eregi_replace')) {
$output = mb_eregi_replace($ignore_re, '', $output);
}
else {
$output = preg_replace("/$ignore_re/i", '', $output);
}
}
// Always replace whitespace with the separator.
$output = preg_replace('/\s+/', $settings['separator'], $output);
// In preparation for pattern matching,
// escape the separator if and only if it is not alphanumeric.
if (isset($settings['separator'])) {
if (preg_match('/^[^' . CTOOLS_PREG_CLASS_ALNUM . ']+$/uD', $settings['separator'])) {
$seppattern = $settings['separator'];
}
else {
$seppattern = '\\' . $settings['separator'];
}
// Trim any leading or trailing separators (note the need to
$output = preg_replace("/^$seppattern+|$seppattern+$/", '', $output);
// Replace multiple separators with a single one
$output = preg_replace("/$seppattern+/", $settings['separator'], $output);
}
// Enforce the maximum component length
if (!empty($settings['max length'])) {
$output = ctools_cleanstring_truncate($output, $settings['max length'], $settings['separator']);
}
if (!empty($settings['lower case'])) {
$output = drupal_strtolower($output);
}
return $output;
}
/**
* A friendly version of truncate_utf8.
*
* @param $string
* The string to be truncated.
* @param $length
* An integer for the maximum desired length.
* @param $separator
* A string which contains the word boundary such as - or _.
*
* @return
* The string truncated below the maxlength.
*/
function ctools_cleanstring_truncate($string, $length, $separator) {
if (drupal_strlen($string) > $length) {
$string = drupal_substr($string, 0, $length + 1); // leave one more character
if ($last_break = strrpos($string, $separator)) { // space exists AND is not on position 0
$string = substr($string, 0, $last_break);
}
else {
$string = drupal_substr($string, 0, $length);
}
}
return $string;
}

View File

@ -0,0 +1,79 @@
<?php
/**
* @file
* Theme function for the collapsible div tool.
*
* Call theme('ctools_collapsible', $handle, $content, $collapsed) to draw the
* div. The theme function is not necessary; you can add the classes, js and css
* yourself if you really want to.
*/
/**
* Delegated implementation of hook_theme()
*/
function ctools_collapsible_theme(&$items) {
$items['ctools_collapsible'] = array(
'variables' => array('handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
'file' => 'includes/collapsible.theme.inc',
);
$items['ctools_collapsible_remembered'] = array(
'variables' => array('id' => NULL, 'handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
'file' => 'includes/collapsible.theme.inc',
);
}
/**
* Render a collapsible div.
*
* @param $handle
* Text to put in the handle/title area of the div.
* @param $content
* Text to put in the content area of the div, this is what will get
* collapsed
* @param $collapsed = FALSE
* If true, this div will start out collapsed.
*/
function theme_ctools_collapsible($vars) {
ctools_add_js('collapsible-div');
ctools_add_css('collapsible-div');
$class = $vars['collapsed'] ? ' ctools-collapsed' : '';
$output = '<div class="ctools-collapsible-container' . $class . '">';
$output .= '<div class="ctools-collapsible-handle">' . $vars['handle'] . '</div>';
$output .= '<div class="ctools-collapsible-content">' . $vars['content'] . '</div>';
$output .= '</div>';
return $output;
}
/**
* Render a collapsible div whose state will be remembered.
*
* @param $id
* The CSS id of the container. This is required.
* @param $handle
* Text to put in the handle/title area of the div.
* @param $content
* Text to put in the content area of the div, this is what will get
* collapsed
* @param $collapsed = FALSE
* If true, this div will start out collapsed.
*/
function theme_ctools_collapsible_remembered($vars) {
$id = $vars['id'];
$handle = $vars['handle'];
$content = $vars['content'];
$collapsed = $vars['collapsed'];
ctools_add_js('collapsible-div');
ctools_add_css('collapsible-div');
$class = $collapsed ? ' ctools-collapsed' : '';
$output = '<div id="' . $id . '" class="ctools-collapsible-remember ctools-collapsible-container' . $class . '">';
$output .= '<div class="ctools-collapsible-handle">' . $handle . '</div>';
$output .= '<div class="ctools-collapsible-content">' . $content . '</div>';
$output .= '</div>';
return $output;
}

View File

@ -0,0 +1,818 @@
<?php
/**
* @file
* Contains the tools to handle pluggable content that can be used by other
* applications such as Panels or Dashboard.
*
* See the context-content.html file in advanced help for documentation
* of this tool.
*/
/**
* Provide defaults for a content type.
*
* Currently we check for automatically named callbacks to make life a little
* easier on the developer.
*/
function ctools_content_process(&$plugin, $info) {
$function_base = $plugin['module'] . '_' . $plugin['name'] . '_content_type_';
if (empty($plugin['render callback']) && function_exists($function_base . 'render')) {
$plugin['render callback'] = $function_base . 'render';
}
if (empty($plugin['admin title'])) {
if (function_exists($function_base . 'admin_title')) {
$plugin['admin title'] = $function_base . 'admin_title';
}
else {
$plugin['admin title'] = $plugin['title'];
}
}
if (empty($plugin['admin info']) && function_exists($function_base . 'admin_info')) {
$plugin['admin info'] = $function_base . 'admin_info';
}
if (!isset($plugin['edit form']) && function_exists($function_base . 'edit_form')) {
$plugin['edit form'] = $function_base . 'edit_form';
}
if (!isset($plugin['add form']) && function_exists($function_base . 'add_form')) {
$plugin['add form'] = $function_base . 'add_form';
}
if (!isset($plugin['add form']) && function_exists($function_base . 'edit_form')) {
$plugin['add form'] = $function_base . 'edit_form';
}
if (!isset($plugin['description'])) {
$plugin['description'] = '';
}
if (!isset($plugin['icon'])) {
$plugin['icon'] = ctools_content_admin_icon($plugin);
}
// Another ease of use check:
if (!isset($plugin['content types'])) {
// If a subtype plugin exists, try to use it. Otherwise assume single.
if (function_exists($function_base . 'content_types')) {
$plugin['content types'] = $function_base . 'content_types';
}
else {
$type = array(
'title' => $plugin['title'],
'description' => $plugin['description'],
'icon' => ctools_content_admin_icon($plugin),
'category' => $plugin['category'],
);
if (isset($plugin['required context'])) {
$type['required context'] = $plugin['required context'];
}
if (isset($plugin['top level'])) {
$type['top level'] = $plugin['top level'];
}
$plugin['content types'] = array($plugin['name'] => $type);
if (!isset($plugin['single'])) {
$plugin['single'] = TRUE;
}
}
}
}
/**
* Fetch metadata on a specific content_type plugin.
*
* @param $content type
* Name of a panel content type.
*
* @return
* An array with information about the requested panel content type.
*/
function ctools_get_content_type($content_type) {
ctools_include('context');
ctools_include('plugins');
return ctools_get_plugins('ctools', 'content_types', $content_type);
}
/**
* Fetch metadata for all content_type plugins.
*
* @return
* An array of arrays with information about all available panel content types.
*/
function ctools_get_content_types() {
ctools_include('context');
ctools_include('plugins');
return ctools_get_plugins('ctools', 'content_types');
}
/**
* Get all of the individual subtypes provided by a given content type. This
* would be all of the blocks for the block type, or all of the views for
* the view type.
*
* @param $type
* The content type to load.
*
* @return
* An array of all subtypes available.
*/
function ctools_content_get_subtypes($type) {
static $cache = array();
$subtypes = array();
if (is_array($type)) {
$plugin = $type;
}
else {
$plugin = ctools_get_content_type($type);
}
if (empty($plugin) || empty($plugin['name'])) {
return;
}
if (isset($cache[$plugin['name']])) {
return $cache[$plugin['name']];
}
if (isset($plugin['content types'])) {
$function = $plugin['content types'];
if (is_array($function)) {
$subtypes = $function;
}
else if (function_exists($function)) {
// Cast to array to prevent errors from non-array returns.
$subtypes = (array) $function($plugin);
}
}
// Walk through the subtypes and ensure minimal settings are
// retained.
foreach ($subtypes as $id => $subtype) {
// Use exact name since this is a modify by reference.
ctools_content_prepare_subtype($subtypes[$id], $plugin);
}
$cache[$plugin['name']] = $subtypes;
return $subtypes;
}
/**
* Given a content type and a subtype id, return the information about that
* content subtype.
*
* @param $type
* The content type being fetched.
* @param $subtype_id
* The id of the subtype being fetched.
*
* @return
* An array of information describing the content subtype.
*/
function ctools_content_get_subtype($type, $subtype_id) {
$subtype = array();
if (is_array($type)) {
$plugin = $type;
}
else {
$plugin = ctools_get_content_type($type);
}
$function = ctools_plugin_get_function($plugin, 'content type');
if ($function) {
$subtype = $function($subtype_id, $plugin);
}
else {
$subtypes = ctools_content_get_subtypes($type);
if (isset($subtypes[$subtype_id])) {
$subtype = $subtypes[$subtype_id];
}
// If there's only 1 and we somehow have the wrong subtype ID, do not
// care. Return the proper subtype anyway.
if (empty($subtype) && !empty($plugin['single'])) {
$subtype = current($subtypes);
}
}
if ($subtype) {
ctools_content_prepare_subtype($subtype, $plugin);
}
return $subtype;
}
/**
* Ensure minimal required settings on a content subtype exist.
*/
function ctools_content_prepare_subtype(&$subtype, $plugin) {
foreach (array('path', 'js', 'css') as $key) {
if (!isset($subtype[$key]) && isset($plugin[$key])) {
$subtype[$key] = $plugin[$key];
}
}
drupal_alter('ctools_content_subtype', $subtype, $plugin);
}
/**
* Get the content from a given content type.
*
* @param $type
* The content type. May be the name or an already loaded content type plugin.
* @param $subtype
* The name of the subtype being rendered.
* @param $conf
* The configuration for the content type.
* @param $keywords
* An array of replacement keywords that come from outside contexts.
* @param $args
* The arguments provided to the owner of the content type. Some content may
* wish to configure itself based on the arguments the panel or dashboard
* received.
* @param $context
* An array of context objects available for use.
* @param $incoming_content
* Any incoming content, if this display is a wrapper.
*
* @return
* The content as rendered by the plugin. This content should be an array
* with the following possible keys:
* - title: The safe to render title of the content.
* - content: The safe to render HTML content.
* - links: An array of links associated with the content suitable for
* theme('links').
* - more: An optional 'more' link (destination only)
* - admin_links: Administrative links associated with the content, suitable
* for theme('links').
* - feeds: An array of feed icons or links associated with the content.
* Each member of the array is rendered HTML.
* - type: The content type.
* - subtype: The content subtype. These two may be used together as
* module-delta for block style rendering.
*/
function ctools_content_render($type, $subtype, $conf, $keywords = array(), $args = array(), $context = array(), $incoming_content = '') {
if (is_array($type)) {
$plugin = $type;
}
else {
$plugin = ctools_get_content_type($type);
}
$subtype_info = ctools_content_get_subtype($plugin, $subtype);
$function = ctools_plugin_get_function($subtype_info, 'render callback');
if (!$function) {
$function = ctools_plugin_get_function($plugin, 'render callback');
}
if ($function) {
$pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
if ($pane_context === FALSE) {
return;
}
$content = $function($subtype, $conf, $args, $pane_context, $incoming_content);
if (empty($content)) {
return;
}
// Set up some defaults and other massaging on the content before we hand
// it back to the caller.
if (!isset($content->type)) {
$content->type = $plugin['name'];
}
if (!isset($content->subtype)) {
$content->subtype = $subtype;
}
// Override the title if configured to
if (!empty($conf['override_title'])) {
// Give previous title as an available substitution here.
$keywords['%title'] = empty($content->title) ? '' : $content->title;
$content->original_title = $keywords['%title'];
$content->title = $conf['override_title_text'];
}
if (!empty($content->title)) {
// Perform substitutions
if (!empty($keywords) || !empty($context)) {
$content->title = ctools_context_keyword_substitute($content->title, $keywords, $context);
}
// Sterilize the title
$content->title = filter_xss_admin($content->title);
// If a link is specified, populate.
if (!empty($content->title_link)) {
if (!is_array($content->title_link)) {
$url = array('href' => $content->title_link);
}
else {
$url = $content->title_link;
}
// set defaults so we don't bring up notices
$url += array('href' => '', 'attributes' => array(), 'query' => array(), 'fragment' => '', 'absolute' => NULL, 'html' => TRUE);
$content->title = l($content->title, $url['href'], $url);
}
}
return $content;
}
}
/**
* Determine if a content type can be edited or not.
*
* Some content types simply have their content and no options. This function
* lets a UI determine if it should display an edit link or not.
*/
function ctools_content_editable($type, $subtype, $conf) {
if (empty($type['edit form']) && empty($subtype['edit form'])) {
return FALSE;
}
if ($function = ctools_plugin_get_function($subtype, 'check editable')) {
return $function($type, $subtype, $conf);
}
return TRUE;
}
/**
* Get the administrative title from a given content type.
*
* @param $type
* The content type. May be the name or an already loaded content type object.
* @param $subtype
* The subtype being rendered.
* @param $conf
* The configuration for the content type.
* @param $context
* An array of context objects available for use. These may be placeholders.
*/
function ctools_content_admin_title($type, $subtype, $conf, $context = NULL) {
if (is_array($type)) {
$plugin = $type;
}
else if (is_string($type)) {
$plugin = ctools_get_content_type($type);
}
else {
return;
}
if ($function = ctools_plugin_get_function($plugin, 'admin title')) {
$pane_context = ctools_content_select_context($plugin, $subtype, $conf, $context);
if ($pane_context === FALSE) {
if ($plugin['name'] == $subtype) {
return t('@type will not display due to missing context', array('@type' => $plugin['name']));
}
return t('@type:@subtype will not display due to missing context', array('@type' => $plugin['name'], '@subtype' => $subtype));
}
return $function($subtype, $conf, $pane_context);
}
else if (isset($plugin['admin title'])) {
return $plugin['admin title'];
}
else if (isset($plugin['title'])) {
return $plugin['title'];
}
}
/**
* Get the proper icon path to use, falling back to default icons if no icon exists.
*
* $subtype
* The loaded subtype info.
*/
function ctools_content_admin_icon($subtype) {
$icon = '';
if (isset($subtype['icon'])) {
$icon = $subtype['icon'];
if (!file_exists($icon)) {
$icon = $subtype['path'] . '/' . $icon;
}
}
if (empty($icon) || !file_exists($icon)) {
$icon = ctools_image_path('no-icon.png');
}
return $icon;
}
/**
* Set up the default $conf for a new instance of a content type.
*/
function ctools_content_get_defaults($plugin, $subtype) {
if (isset($plugin['defaults'])) {
$defaults = $plugin['defaults'];
}
else if (isset($subtype['defaults'])) {
$defaults = $subtype['defaults'];
}
if (isset($defaults)) {
if (is_string($defaults) && function_exists($defaults)) {
if ($return = $defaults($pane)) {
return $return;
}
}
else if (is_array($defaults)) {
return $defaults;
}
}
return array();
}
/**
* Get the administrative title from a given content type.
*
* @param $type
* The content type. May be the name or an already loaded content type object.
* @param $subtype
* The subtype being rendered.
* @param $conf
* The configuration for the content type.
* @param $context
* An array of context objects available for use. These may be placeholders.
*/
function ctools_content_admin_info($type, $subtype, $conf, $context = NULL) {
if (is_array($type)) {
$plugin = $type;
}
else {
$plugin = ctools_get_content_type($type);
}
if ($function = ctools_plugin_get_function($plugin, 'admin info')) {
$output = $function($subtype, $conf, $context);
}
if (empty($output) || !is_object($output)) {
$output = new stdClass();
// replace the _ with " " for a better output
$subtype = check_plain(str_replace("_", " ", $subtype));
$output->title = $subtype;
$output->content = t('No info available.');
}
return $output;
}
/**
* Add the default FAPI elements to the content type configuration form
*/
function ctools_content_configure_form_defaults($form, &$form_state) {
$plugin = $form_state['plugin'];
$subtype = $form_state['subtype'];
$contexts = isset($form_state['contexts']) ? $form_state['contexts'] : NULL;
$conf = $form_state['conf'];
$add_submit = FALSE;
if (!empty($subtype['required context']) && is_array($contexts)) {
$form['context'] = ctools_context_selector($contexts, $subtype['required context'], isset($conf['context']) ? $conf['context'] : array());
$add_submit = TRUE;
}
ctools_include('dependent');
// Unless we're not allowed to override the title on this content type, add this
// gadget to all panes.
if (empty($plugin['no title override']) && empty($subtype['no title override'])) {
$form['aligner_start'] = array(
'#markup' => '<div class="option-text-aligner clearfix">',
);
$form['override_title'] = array(
'#type' => 'checkbox',
'#default_value' => isset($conf['override_title']) ? $conf['override_title'] : '',
'#title' => t('Override title'),
'#id' => 'override-title-checkbox',
);
$form['override_title_text'] = array(
'#type' => 'textfield',
'#default_value' => isset($conf['override_title_text']) ? $conf['override_title_text'] : '',
'#size' => 35,
'#id' => 'override-title-textfield',
'#dependency' => array('override-title-checkbox' => array(1)),
'#dependency_type' => 'disable',
);
$form['aligner_stop'] = array(
'#markup' => '</div>',
);
if (is_array($contexts)) {
$form['override_title_markup'] = array(
'#prefix' => '<div class="description">',
'#suffix' => '</div>',
'#markup' => t('You may use %keywords from contexts, as well as %title to contain the original title.'),
);
}
$add_submit = TRUE;
}
if ($add_submit) {
// '#submit' is already set up due to the wizard.
$form['#submit'][] = 'ctools_content_configure_form_defaults_submit';
}
return $form;
}
/**
* Submit handler to store context/title override info.
*/
function ctools_content_configure_form_defaults_submit(&$form, &$form_state) {
if (isset($form_state['values']['context'])) {
$form_state['conf']['context'] = $form_state['values']['context'];
}
if (isset($form_state['values']['override_title'])) {
$form_state['conf']['override_title'] = $form_state['values']['override_title'];
$form_state['conf']['override_title_text'] = $form_state['values']['override_title_text'];
}
}
/**
* Get the config form.
*
* The $form_info and $form_state need to be preconfigured with data you'll need
* such as whether or not you're using ajax, or the modal. $form_info will need
* your next/submit callbacks so that you can cache your data appropriately.
*
* @return
* If this function returns false, no form exists.
*/
function ctools_content_form($op, $form_info, &$form_state, $plugin, $subtype_name, $subtype, &$conf, $step = NULL) {
$form_state += array(
'plugin' => $plugin,
'subtype' => $subtype,
'subtype_name' => $subtype_name,
'conf' => &$conf,
'op' => $op,
);
$form_info += array(
'id' => 'ctools_content_form',
'show back' => TRUE,
);
// Turn the forms defined in the plugin into the format the wizard needs.
if ($op == 'add') {
if (!empty($subtype['add form'])) {
_ctools_content_create_form_info($form_info, $subtype['add form'], $subtype, $subtype, $op);
}
else if (!empty($plugin['add form'])) {
_ctools_content_create_form_info($form_info, $plugin['add form'], $plugin, $subtype, $op);
}
}
if (empty($form_info['order'])) {
// Use the edit form for the add form if add form was completely left off.
if (!empty($subtype['edit form'])) {
_ctools_content_create_form_info($form_info, $subtype['edit form'], $subtype, $subtype, $op);
}
else if (!empty($plugin['edit form'])) {
_ctools_content_create_form_info($form_info, $plugin['edit form'], $plugin, $subtype, $op);
}
}
if (empty($form_info['order'])) {
return FALSE;
}
ctools_include('wizard');
return ctools_wizard_multistep_form($form_info, $step, $form_state);
}
function _ctools_content_create_form_info(&$form_info, $info, $plugin, $subtype, $op) {
if (is_string($info)) {
if (empty($subtype['title'])) {
$title = t('Configure');
}
else if ($op == 'add') {
$title = t('Configure new !subtype_title', array('!subtype_title' => $subtype['title']));
}
else {
$title = t('Configure !subtype_title', array('!subtype_title' => $subtype['title']));
}
$form_info['order'] = array('form' => $title);
$form_info['forms'] = array(
'form' => array(
'title' => $title,
'form id' => $info,
'wrapper' => 'ctools_content_configure_form_defaults',
),
);
}
else if (is_array($info)) {
$form_info['order'] = array();
$form_info['forms'] = array();
$count = 0;
$base = 'step';
$wrapper = NULL;
foreach ($info as $form_id => $title) {
// @todo -- docs say %title can be used to sub for the admin title.
$step = $base . ++$count;
if (empty($wrapper)) {
$wrapper = $step;
}
if (is_array($title)) {
if (!empty($title['default'])) {
$wrapper = $step;
}
$title = $title['title'];
}
$form_info['order'][$step] = $title;
$form_info['forms'][$step] = array(
'title' => $title,
'form id' => $form_id,
);
}
if ($wrapper) {
$form_info['forms'][$wrapper]['wrapper'] = 'ctools_content_configure_form_defaults';
}
}
}
/**
* Get an array of all available content types that can be fed into the
* display editor for the add content list.
*
* @param $context
* If a context is provided, content that requires that context can apepar.
* @param $has_content
* Whether or not the display will have incoming content
* @param $allowed_types
* An array of allowed content types (pane types) keyed by content_type . '-' . sub_type
* @param $default_types
* A default allowed/denied status for content that isn't known about
*/
function ctools_content_get_available_types($contexts = NULL, $has_content = FALSE, $allowed_types = NULL, $default_types = NULL) {
$plugins = ctools_get_content_types();
$available = array();
foreach ($plugins as $id => $plugin) {
foreach (ctools_content_get_subtypes($plugin) as $subtype_id => $subtype) {
// exclude items that require content if we're saying we don't
// provide it.
if (!empty($subtype['requires content']) && !$has_content) {
continue;
}
// Check to see if the content type can be used in this context.
if (!empty($subtype['required context'])) {
if (!ctools_context_match_requirements($contexts, $subtype['required context'])) {
continue;
}
}
// Check to see if the passed-in allowed types allows this content.
if ($allowed_types) {
$key = $id . '-' . $subtype_id;
if (!isset($allowed_types[$key])) {
$allowed_types[$key] = isset($default_types[$id]) ? $default_types[$id] : $default_types['other'];
}
if (!$allowed_types[$key]) {
continue;
}
}
// If we made it through all the tests, then we can use this content.
$available[$id][$subtype_id] = $subtype;
}
}
return $available;
}
/**
* Get an array of all content types that can be fed into the
* display editor for the add content list, regardless of
* availability.
*
*/
function ctools_content_get_all_types() {
$plugins = ctools_get_content_types();
$available = array();
foreach ($plugins as $id => $plugin) {
foreach (ctools_content_get_subtypes($plugin) as $subtype_id => $subtype) {
// If we made it through all the tests, then we can use this content.
$available[$id][$subtype_id] = $subtype;
}
}
return $available;
}
/**
* Select the context to be used for a piece of content, based upon config.
*
* @param $plugin
* The content plugin
* @param $subtype
* The subtype of the content.
* @param $conf
* The configuration array that should contain the context.
* @param $contexts
* A keyed array of available contexts.
*
* @return
* The matching contexts or NULL if none or necessary, or FALSE if
* requirements can't be met.
*/
function ctools_content_select_context($plugin, $subtype, $conf, $contexts) {
// Identify which of our possible contexts apply.
if (empty($subtype)) {
return;
}
$subtype_info = ctools_content_get_subtype($plugin, $subtype);
if (empty($subtype_info)) {
return;
}
if (!empty($subtype_info['all contexts']) || !empty($plugin['all contexts'])) {
return $contexts;
}
// If the content requires a context, fetch it; if no context is returned,
// do not display the pane.
if (empty($subtype_info['required context'])) {
return;
}
// Deal with dynamic required contexts not getting updated in the panes.
// For example, Views let you dynamically change context info. While
// we cannot be perfect, one thing we can do is if no context at all
// was asked for, and then was later added but none is selected, make
// a best guess as to what context should be used. THis is right more
// than it's wrong.
if (is_array($subtype_info['required context'])) {
if (empty($conf['context']) || count($subtype_info['required context']) != count($conf['context'])) {
foreach ($subtype_info['required context'] as $index => $required) {
if (!isset($conf['context'][$index])) {
$filtered = ctools_context_filter($contexts, $required);
if ($filtered) {
$keys = array_keys($filtered);
$conf['context'][$index] = array_shift($keys);
}
}
}
}
}
else {
if (empty($conf['context'])) {
$filtered = ctools_context_filter($contexts, $subtype_info['required context']);
if ($filtered) {
$keys = array_keys($filtered);
$conf['context'] = array_shift($keys);
}
}
}
if (empty($conf['context'])) {
return;
}
$context = ctools_context_select($contexts, $subtype_info['required context'], $conf['context']);
return $context;
}
/**
* Fetch a piece of content from the addressable content system.
*
* @param $address
* A string or an array representing the address of the content.
* @param $type
* The type of content to return. The type is dependent on what
* the content actually is. The default, 'content' means a simple
* string representing the content. However, richer systems may
* offer more options. For example, Panels might allow the
* fetching of 'display' and 'pane' objects. Page Manager
* might allow for the fetching of 'task_handler' objects
* (AKA variants).
*/
function ctools_get_addressable_content($address, $type = 'content') {
if (!is_array($address)) {
$address = explode('::', $address);
}
if (!$address) {
return;
}
// This removes the module from the address so the
// implementor is not responsible for that part.
$module = array_shift($address);
return module_invoke($module, 'addressable_content', $address, $type);
}

View File

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains menu item registration for the content tool.
*
* The menu items registered are AJAX callbacks for the things like
* autocomplete and other tools needed by the content types.
*/
function ctools_content_menu(&$items) {
$base = array(
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
'file' => 'includes/content.menu.inc',
);
$items['ctools/autocomplete/%'] = array(
'page callback' => 'ctools_content_autocomplete_entity',
'page arguments' => array(2),
) + $base;
}
/**
* Helper function for autocompletion of entity titles.
*/
function ctools_content_autocomplete_entity($type, $string = '') {
$entity = entity_get_info($type);
if ($string != '') {
// @todo verify the query logic here, it's untested.
// Set up the query
$query = db_select($entity['base table'], 'b');
if ($entity['entity keys']['label']) {
$query->fields('b', array($entity['entity keys']['id'], $entity['entity keys']['label']))->range(0, 10);
}
else {
$query->fields('b', array($entity['entity keys']['id']))->range(0, 10);
}
$preg_matches = array();
$match = preg_match('/\[id: (\d+)\]/', $string, $preg_matches);
if (!$match) {
$match = preg_match('/^id: (\d+)/', $string, $preg_matches);
}
if ($match) {
$query->condition('b.' . $entity['entity keys']['id'], $preg_matches[1]);
}
elseif ($entity['entity keys']['label']) {
$query->condition('b.' . $entity['entity keys']['label'], '%' . db_like($string) . '%', 'LIKE');
}
$matches = array();
if ($type == 'node') {
$query->addTag('node_access');
$query->join('users', 'u', 'b.uid = u.uid');
$query->addField('u', 'name', 'name');
foreach ($query->execute() as $nodeish) {
$name = empty($nodeish->name) ? variable_get('anonymous', t('Anonymous')) : check_plain($nodeish->name);
$matches[$nodeish->title . " [id: $nodeish->nid]"] = '<span class="autocomplete_title">' . check_plain($nodeish->title) . '</span> <span class="autocomplete_user">(' . t('by @user', array('@user' => $name)) . ')</span>';
}
}
else {
foreach ($query->execute() as $item) {
$id = $item->{$entity['entity keys']['id']};
if ($entity['entity keys']['label']) {
$matches[$item->{$entity['entity keys']['label']} . " [id: $id]"] = '<span class="autocomplete_title">' . check_plain($item->{$entity['entity keys']['label']}) . '</span>';
}
else {
$matches["[id: $id]"] = '<span class="autocomplete_title">' . check_plain($item->{$entity['entity keys']['id']}) . '</span>';
}
}
}
drupal_json_output($matches);
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains plugin type registration information for the content tool.
*/
function ctools_content_plugin_type(&$items) {
$items['content_types'] = array(
'cache' => FALSE,
'process' => array(
'function' => 'ctools_content_process',
'file' => 'export-ui.inc',
'path' => drupal_get_path('module', 'ctools') . '/includes',
),
);
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @file
* Contains theme registry and theme implementations for the content types.
*/
/**
* Implements hook_theme to load all content plugins and pass thru if
* necessary.
*/
function ctools_content_theme(&$theme) {
ctools_include('content');
$plugins = ctools_get_content_types();
foreach ($plugins as $plugin) {
if ($function = ctools_plugin_get_function($plugin, 'hook theme')) {
$function($theme, $plugin);
}
}
}

View File

@ -0,0 +1,486 @@
<?php
/**
* @file
* Contains administrative screens for the access control plugins.
*
* Access control can be implemented by creating a list of 0 or more access
* plugins, each with settings. This list can be ANDed together or ORed
* together. When testing access, each plugin is tested until success
* or failure can be determined. We use short circuiting techniques to
* ensure we are as efficient as possible.
*
* Access plugins are part of the context system, and as such can require
* contexts to work. That allows the use of access based upon visibility
* of an object, or even more esoteric things such as node type, node language
* etc. Since a lot of access depends on the logged in user, the logged in
* user should always be provided as a context.
*
* In the UI, the user is presented with a table and a 'add access method' select.
* When added, the user will be presented with the config wizard and, when
* confirmed, table will be refreshed via AJAX to show the new access method.
* Each item in the table will have controls to change the settings or remove
* the item. Changing the settings will invoke the modal for update.
*
* Currently the modal is not degradable, but it could be with only a small
* amount of work.
*
* A simple radio
* control is used to let the user pick the and/or logic.
*
* Access control is stored in an array:
* @code
* array(
* 'plugins' => array(
* 0 => array(
* 'name' => 'name of access plugin',
* 'settings' => array(), // These will be set by the form
* ),
* // ... as many as needed
* ),
* 'logic' => 'AND', // or 'OR',
* ),
* @endcode
*
* To add this widget to your UI, you need to do a little bit of setup.
*
* The form will utilize two callbacks, one to get the cached version
* of the access settings, and one to store the cached version of the
* access settings. These will be used from AJAX forms, so they will
* be completely out of the context of this page load and will not have
* knowledge of anything sent to this form (the 'module' and 'argument'
* will be preserved through the URL only).
*
* The 'module' is used to determine the location of the callback. It
* does not strictly need to be a module, so that if your module defines
* multiple systems that use this callback, it can use anything within the
* module's namespace it likes.
*
* When retrieving the cache, the cache may not have already been set up;
* In order to efficiently use cache space, we want to cache the stored
* settings *only* when they have changed. Therefore, the get access cache
* callback should first look for cache, and if it finds nothing, return
* the original settings.
*
* The callbacks:
* - $module . _ctools_access_get($argument) -- get the 'access' settings
* from cache. Must return array($access, $contexts); This callback can
* perform access checking to make sure this URL is not being gamed.
* - $module . _ctools_access_set($argument, $access) -- set the 'access'
* settings in cache.
* - $module . _ctools_access_clear($argument) -- clear the cache.
*
* The ctools_object_cache is recommended for this purpose, but you can use
* any caching mechanism you like. An example:
*
* @code{
* ctools_include('object-cache');
* ctools_object_cache_set("$module:argument", $access);
* }
*
* To utilize this form:
* @code
* ctools_include('context-access-admin');
* $form_state = array(
* 'access' => $access,
* 'module' => 'module name',
* 'callback argument' => 'some string',
* 'contexts' => $contexts, // an array of contexts. Optional if no contexts.
* // 'logged-in-user' will be added if not present as the access system
* // requires this context.
* ),
* $output = drupal_build_form('ctools_access_admin_form', $form_state);
* if (!empty($form_state['executed'])) {
* // save $form_state['access'] however you like.
* }
* @endcode
*
* Additionally, you may add 'no buttons' => TRUE if you wish to embed this
* form into your own, and instead call
*
* @code{
* $form = ctools_access_admin_form($form, $form_state);
* }
*
* You'll be responsible for adding a submit button.
*
* You may use ctools_access($access, $contexts) which will return
* TRUE if access is passed or FALSE if access is not passed.
*/
/**
* Administrative form for access control.
*/
function ctools_access_admin_form($form, &$form_state) {
ctools_include('context');
$argument = isset($form_state['callback argument']) ? $form_state['callback argument'] : '';
$fragment = $form_state['module'];
if ($argument) {
$fragment .= '-' . $argument;
}
$contexts = isset($form_state['contexts']) ? $form_state['contexts'] : array();
$form['access_table'] = array(
'#markup' => ctools_access_admin_render_table($form_state['access'], $fragment, $contexts),
);
$form['add-button'] = array(
'#theme' => 'ctools_access_admin_add',
);
// This sets up the URL for the add access modal.
$form['add-button']['add-url'] = array(
'#attributes' => array('class' => array("ctools-access-add-url")),
'#type' => 'hidden',
'#value' => url("ctools/context/ajax/access/add/$fragment", array('absolute' => TRUE)),
);
$plugins = ctools_get_relevant_access_plugins($contexts);
$options = array();
foreach ($plugins as $id => $plugin) {
$options[$id] = $plugin['title'];
}
asort($options);
$form['add-button']['type'] = array(
// This ensures that the form item is added to the URL.
'#attributes' => array('class' => array("ctools-access-add-url")),
'#type' => 'select',
'#options' => $options,
'#required' => FALSE,
);
$form['add-button']['add'] = array(
'#type' => 'submit',
'#attributes' => array('class' => array('ctools-use-modal')),
'#id' => "ctools-access-add",
'#value' => t('Add'),
);
$form['logic'] = array(
'#type' => 'radios',
'#options' => array(
'and' => t('All criteria must pass.'),
'or' => t('Only one criteria must pass.'),
),
'#default_value' => isset($form_state['access']['logic']) ? $form_state['access']['logic'] : 'and',
);
if (empty($form_state['no buttons'])) {
$form['buttons']['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#submit' => array('ctools_access_admin_form_submit'),
);
}
return $form;
}
/**
* Render the table. This is used both to render it initially and to rerender
* it upon ajax response.
*/
function ctools_access_admin_render_table($access, $fragment, $contexts) {
ctools_include('ajax');
ctools_include('modal');
$rows = array();
if (empty($access['plugins'])) {
$access['plugins'] = array();
}
foreach ($access['plugins'] as $id => $test) {
$row = array();
$plugin = ctools_get_access_plugin($test['name']);
$title = isset($plugin['title']) ? $plugin['title'] : t('Broken/missing access plugin %plugin', array('%plugin' => $test['name']));
$row[] = array('data' => $title, 'class' => array('ctools-access-title'));
$description = ctools_access_summary($plugin, $contexts, $test);
$row[] = array('data' => $description, 'class' => array('ctools-access-description'));
$operations = ctools_modal_image_button(ctools_image_path('icon-configure.png'), "ctools/context/ajax/access/configure/$fragment/$id", t('Configure settings for this item.'));
$operations .= ctools_ajax_image_button(ctools_image_path('icon-delete.png'), "ctools/context/ajax/access/delete/$fragment/$id", t('Remove this item.'));
$row[] = array('data' => $operations, 'class' => array('ctools-access-operations'), 'align' => 'right');
$rows[] = $row;
}
$header = array(
array('data' => t('Title'), 'class' => array('ctools-access-title')),
array('data' => t('Description'), 'class' => array('ctools-access-description')),
array('data' => '', 'class' => array('ctools-access-operations'), 'align' => 'right'),
);
if (empty($rows)) {
$rows[] = array(array('data' => t('No criteria selected, this test will pass.'), 'colspan' => count($header)));
}
ctools_modal_add_js();
return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'ctools-access-table')));
}
/**
* Theme the 'add' portion of the access form into a table.
*/
function theme_ctools_access_admin_add($vars) {
$rows = array(array(drupal_render_children($vars['form'])));
$output = '<div class="container-inline">';
$output .= theme('table', array('rows' => $rows));
$output .= '</div>';
return $output;
}
function ctools_access_admin_form_submit($form, &$form_state) {
$form_state['access']['logic'] = $form_state['values']['logic'];
$function = $form_state['module'] . '_ctools_access_clear';
if (function_exists($function)) {
$function($form_state['callback argument']);
}
}
// --------------------------------------------------------------------------
// AJAX menu entry points.
/**
* AJAX callback to add a new access test to the list.
*/
function ctools_access_ajax_add($fragment = NULL, $name = NULL) {
ctools_include('ajax');
ctools_include('modal');
ctools_include('context');
if (empty($fragment) || empty($name)) {
ctools_ajax_render_error();
}
$plugin = ctools_get_access_plugin($name);
if (empty($plugin)) {
ctools_ajax_render_error();
}
// Separate the fragment into 'module' and 'argument'
if (strpos($fragment, '-') === FALSE) {
$module = $fragment;
$argument = NULL;
}
else {
list($module, $argument) = explode('-', $fragment, 2);
}
$function = $module . '_ctools_access_get';
if (!function_exists($function)) {
ctools_ajax_render_error(t('Missing callback hooks.'));
}
list($access, $contexts) = $function($argument);
// Make sure we have the logged in user context
if (!isset($contexts['logged-in-user'])) {
$contexts['logged-in-user'] = ctools_access_get_loggedin_context();
}
if (empty($access['plugins'])) {
$access['plugins'] = array();
}
$test = ctools_access_new_test($plugin);
$id = $access['plugins'] ? max(array_keys($access['plugins'])) + 1 : 0;
$access['plugins'][$id] = $test;
$form_state = array(
'plugin' => $plugin,
'id' => $id,
'test' => &$access['plugins'][$id],
'access' => &$access,
'contexts' => $contexts,
'title' => t('Add criteria'),
'ajax' => TRUE,
'modal' => TRUE,
'modal return' => TRUE,
);
$output = ctools_modal_form_wrapper('ctools_access_ajax_edit_item', $form_state);
if (!isset($output[0])) {
$function = $module . '_ctools_access_set';
if (function_exists($function)) {
$function($argument, $access);
}
$table = ctools_access_admin_render_table($access, $fragment, $contexts);
$output = array();
$output[] = ajax_command_replace('table#ctools-access-table', $table);
$output[] = ctools_modal_command_dismiss();
}
print ajax_render($output);
}
/**
* AJAX callback to edit an access test in the list.
*/
function ctools_access_ajax_edit($fragment = NULL, $id = NULL) {
ctools_include('ajax');
ctools_include('modal');
ctools_include('context');
if (empty($fragment) || !isset($id)) {
ctools_ajax_render_error();
}
// Separate the fragment into 'module' and 'argument'
if (strpos($fragment, '-') === FALSE) {
$module = $fragment;
$argument = NULL;
}
else {
list($module, $argument) = explode('-', $fragment, 2);
}
$function = $module . '_ctools_access_get';
if (!function_exists($function)) {
ctools_ajax_render_error(t('Missing callback hooks.'));
}
list($access, $contexts) = $function($argument);
if (empty($access['plugins'][$id])) {
ctools_ajax_render_error();
}
// Make sure we have the logged in user context
if (!isset($contexts['logged-in-user'])) {
$contexts['logged-in-user'] = ctools_access_get_loggedin_context();
}
$plugin = ctools_get_access_plugin($access['plugins'][$id]['name']);
$form_state = array(
'plugin' => $plugin,
'id' => $id,
'test' => &$access['plugins'][$id],
'access' => &$access,
'contexts' => $contexts,
'title' => t('Edit criteria'),
'ajax' => TRUE,
'ajax' => TRUE,
'modal' => TRUE,
'modal return' => TRUE,
);
$output = ctools_modal_form_wrapper('ctools_access_ajax_edit_item', $form_state);
if (!isset($output[0])) {
$function = $module . '_ctools_access_set';
if (function_exists($function)) {
$function($argument, $access);
}
$table = ctools_access_admin_render_table($access, $fragment, $contexts);
$output = array();
$output[] = ajax_command_replace('table#ctools-access-table', $table);
$output[] = ctools_modal_command_dismiss();
}
print ajax_render($output);
}
/**
* Form to edit the settings of an access test.
*/
function ctools_access_ajax_edit_item($form, &$form_state) {
$test = &$form_state['test'];
$plugin = &$form_state['plugin'];
if (isset($plugin['required context'])) {
$form['context'] = ctools_context_selector($form_state['contexts'], $plugin['required context'], $test['context']);
}
$form['settings'] = array('#tree' => TRUE);
if ($function = ctools_plugin_get_function($plugin, 'settings form')) {
$form = $function($form, $form_state, $test['settings']);
}
$form['not'] = array(
'#type' => 'checkbox',
'#title' => t('Reverse (NOT)'),
'#default_value' => !empty($test['not']),
);
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
/**
* Validate handler for argument settings.
*/
function ctools_access_ajax_edit_item_validate($form, &$form_state) {
if ($function = ctools_plugin_get_function($form_state['plugin'], 'settings form validate')) {
$function($form, $form_state);
}
}
/**
* Submit handler for argument settings.
*/
function ctools_access_ajax_edit_item_submit($form, &$form_state) {
if ($function = ctools_plugin_get_function($form_state['plugin'], 'settings form submit')) {
$function($form, $form_state);
}
$form_state['test']['settings'] = $form_state['values']['settings'];
if (isset($form_state['values']['context'])) {
$form_state['test']['context'] = $form_state['values']['context'];
}
$form_state['test']['not'] = !empty($form_state['values']['not']);
}
/**
* AJAX command to remove an access control item.
*/
function ctools_access_ajax_delete($fragment = NULL, $id = NULL) {
ctools_include('ajax');
ctools_include('modal');
ctools_include('context');
if (empty($fragment) || !isset($id)) {
ajax_render_error();
}
// Separate the fragment into 'module' and 'argument'
if (strpos($fragment, '-') === FALSE) {
$module = $fragment;
$argument = NULL;
}
else {
list($module, $argument) = explode('-', $fragment, 2);
}
$function = $module . '_ctools_access_get';
if (!function_exists($function)) {
ajax_render_error(t('Missing callback hooks.'));
}
list($access, $contexts) = $function($argument);
if (isset($access['plugins'][$id])) {
unset($access['plugins'][$id]);
}
// re-cache
$function = $module . '_ctools_access_set';
if (function_exists($function)) {
$function($argument, $access);
}
$table = ctools_access_admin_render_table($access, $fragment, $contexts);
$output = array();
$output[] = ajax_command_replace('table#ctools-access-table', $table);
print ajax_render($output);
}

View File

@ -0,0 +1,819 @@
<?php
/**
* @file includes/common-context.inc
* Provide API for adding contexts for modules that embed displays.
*
* Note that most of this code was directly copied from Panels 2, and as such
* a lot of this code is crusty. It could probably stand to be rewritten,
* and brought up to date, or at least better commented.
*/
/**
* Provide a list of the ways contexts can be embedded.
*
* This provides a full list of context types that the tool understands
* and can let modules utilize.
*/
function ctools_context_info($type = NULL) {
static $info = NULL;
// static doesn't work with functions like t().
if (empty($info)) {
$info = array(
'argument' => array(
'title' => t('Arguments'),
'singular title' => t('argument'),
'description' => '', // t("Arguments are parsed from the URL and translated into contexts that may be added to the display via the 'content' tab. These arguments are parsed in the order received, and you may use % in your URL to hold the place of an object; the rest of the arguments will come after the URL. For example, if the URL is node/%/panel and your user visits node/1/panel/foo, the first argument will be 1, and the second argument will be foo."),
'add button' => t('Add argument'),
'context function' => 'ctools_get_argument',
'key' => 'arguments', // the key that data will be stored on an object, eg $panel_page
'sortable' => TRUE,
'settings' => 'argument_settings',
),
'relationship' => array(
'title' => t('Relationships'),
'singular title' => t('relationship'),
'description' => '', // t('Relationships are contexts that are created from already existing contexts; the add relationship button will only appear once there is another context available. Relationships can load objects based upon how they are related to each other; for example, the author of a node, or a taxonomy term attached to a node, or the vocabulary of a taxonomy term.'),
'add button' => t('Add relationship'),
'context function' => 'ctools_get_relationship',
'key' => 'relationships',
'sortable' => FALSE,
'settings' => 'relationship_settings',
),
'context' => array(
'title' => t('Contexts'),
'singular title' => t('context'),
'description' => '', // t('Contexts are embedded directly into the panel; you generally must select an object in the panel. For example, you could select node 5, or the term "animals" or the user "administrator"'),
'add button' => t('Add context'),
'context function' => 'ctools_get_context',
'key' => 'contexts',
'sortable' => FALSE,
'settings' => 'context_settings',
),
'requiredcontext' => array(
'title' => t('Required contexts'),
'singular title' => t('required context'),
'description' => '', // t('Required contexts are passed in from some external source, such as a containing panel. If a mini panel has required contexts, it can only appear when that context is available, and therefore will not show up as a standard Drupal block.'),
'add button' => t('Add required context'),
'context function' => 'ctools_get_context',
'key' => 'requiredcontexts',
'sortable' => FALSE,
),
);
}
if ($type === NULL) {
return $info;
}
return $info[$type];
}
/**
* Get the data belonging to a particular context.
*/
function ctools_context_get_plugin($type, $name) {
$info = ctools_context_info($type);
if (function_exists($info['context function'])) {
return $info['context function']($name);
}
}
/**
* Add the argument table plus gadget plus javascript to the form.
*/
function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
if (empty($cache_key)) {
$cache_key = $object->name;
}
$form_location = array(
'#prefix' => '<div id="ctools-arguments-table">',
'#suffix' => '</div>',
'#theme' => 'ctools_context_item_form',
'#cache_key' => $cache_key,
'#ctools_context_type' => 'argument',
'#ctools_context_module' => $module,
);
$args = ctools_get_arguments();
$choices = array();
foreach ($args as $name => $arg) {
if (empty($arg['no ui'])) {
$choices[$name] = $arg['title'];
}
}
asort($choices);
if (!empty($choices) || !empty($object->arguments)) {
ctools_context_add_item_table('argument', $form_location, $choices, $object->arguments);
}
}
function ctools_context_add_context_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
if (empty($cache_key)) {
$cache_key = $object->name;
}
$form_location = array(
'#prefix' => '<div id="ctools-contexts-table">',
'#suffix' => '</div>',
'#theme' => 'ctools_context_item_form',
'#cache_key' => $cache_key,
'#ctools_context_type' => 'context',
'#ctools_context_module' => $module,
);
// Store the order the choices are in so javascript can manipulate it.
$form_location['markup'] = array(
'#markup' => '&nbsp;',
);
$choices = array();
foreach (ctools_get_contexts() as $name => $arg) {
if (empty($arg['no ui'])) {
$choices[$name] = $arg['title'];
}
}
asort($choices);
if (!empty($choices) || !empty($object->contexts)) {
ctools_context_add_item_table('context', $form_location, $choices, $object->contexts);
}
}
function ctools_context_add_required_context_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
if (empty($cache_key)) {
$cache_key = $object->name;
}
$form_location = array(
'#prefix' => '<div id="ctools-requiredcontexts-table">',
'#suffix' => '</div>',
'#theme' => 'ctools_context_item_form',
'#cache_key' => $cache_key,
'#ctools_context_type' => 'requiredcontext',
'#ctools_context_module' => $module,
);
// Store the order the choices are in so javascript can manipulate it.
$form_location['markup'] = array(
'#value' => '&nbsp;',
);
$choices = array();
foreach (ctools_get_contexts() as $name => $arg) {
if (empty($arg['no required context ui'])) {
$choices[$name] = $arg['title'];
}
}
asort($choices);
if (!empty($choices) || !empty($object->contexts)) {
ctools_context_add_item_table('requiredcontext', $form_location, $choices, $object->requiredcontexts);
}
}
function ctools_context_add_relationship_form($module, &$form, &$form_state, &$form_location, $object, $cache_key = NULL) {
if (empty($cache_key)) {
$cache_key = $object->name;
}
$form_location = array(
'#prefix' => '<div id="ctools-relationships-table">',
'#suffix' => '</div>',
'#theme' => 'ctools_context_item_form',
'#cache_key' => $cache_key,
'#ctools_context_type' => 'relationship',
'#ctools_context_module' => $module,
);
// Store the order the choices are in so javascript can manipulate it.
$form_location['markup'] = array(
'#value' => '&nbsp;',
);
$base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
$available_relationships = ctools_context_get_relevant_relationships(ctools_context_load_contexts($object, TRUE, $base_contexts));
ctools_context_add_item_table('relationship', $form_location, $available_relationships, $object->relationships);
}
/**
* Include all context administrative include files, css, javascript.
*/
function ctools_context_admin_includes() {
ctools_include('context');
ctools_include('modal');
ctools_include('ajax');
ctools_include('object-cache');
ctools_modal_add_js();
ctools_modal_add_plugin_js(ctools_get_contexts());
ctools_modal_add_plugin_js(ctools_get_relationships());
}
/**
* Add the context table to the page.
*/
function ctools_context_add_item_table($type, &$form, $available_contexts, $items) {
$form[$type] = array(
'#tree' => TRUE,
);
$module = $form['#ctools_context_module'];
$cache_key = $form['#cache_key'];
if (isset($items) && is_array($items)) {
foreach ($items as $position => $context) {
ctools_context_add_item_to_form($module, $type, $cache_key, $form[$type][$position], $position, $context);
}
}
$type_info = ctools_context_info($type);
$form['description'] = array(
'#prefix' => '<div class="description">',
'#suffix' => '</div>',
'#markup' => $type_info['description'],
);
ctools_context_add_item_table_buttons($type, $module, $form, $available_contexts);
}
function ctools_context_add_item_table_buttons($type, $module, &$form, $available_contexts) {
drupal_add_library('system', 'drupal.ajax');
$form['buttons'] = array(
'#tree' => TRUE,
);
if (!empty($available_contexts)) {
$type_info = ctools_context_info($type);
$module = $form['#ctools_context_module'];
$cache_key = $form['#cache_key'];
// The URL for this ajax button
$form['buttons'][$type]['add-url'] = array(
'#attributes' => array('class' => array("ctools-$type-add-url")),
'#type' => 'hidden',
'#value' => url("ctools/context/ajax/add/$module/$type/$cache_key", array('absolute' => TRUE)),
);
asort($available_contexts);
// This also will be in the URL.
$form['buttons'][$type]['item'] = array(
'#attributes' => array('class' => array("ctools-$type-add-url")),
'#type' => 'select',
'#options' => $available_contexts,
'#required' => FALSE,
);
$form['buttons'][$type]['add'] = array(
'#type' => 'submit',
'#attributes' => array('class' => array('ctools-use-modal')),
'#id' => "ctools-$type-add",
'#value' => $type_info['add button'],
);
}
}
/**
* Add a row to the form. Used both in the main form and by
* the ajax to add an item.
*/
function ctools_context_add_item_to_form($module, $type, $cache_key, &$form, $position, $item) {
// This is the single function way to load any plugin by variable type.
$info = ctools_context_get_plugin($type, $item['name']);
$form['title'] = array(
'#markup' => check_plain($item['identifier']),
);
// Relationships not sortable.
$type_info = ctools_context_info($type);
if (!empty($type_info['sortable'])) {
$form['position'] = array(
'#type' => 'weight',
'#default_value' => $position,
'#attributes' => array('class' => array('drag-position')),
);
}
$form['remove'] = array(
'#markup' => ctools_ajax_image_button(ctools_image_path('icon-delete.png'), "ctools/context/ajax/delete/$module/$type/$cache_key/$position", t('Remove this item.')),
);
$form['settings'] = array(
'#markup' => ctools_modal_image_button(ctools_image_path('icon-configure.png'), "ctools/context/ajax/configure/$module/$type/$cache_key/$position", t('Configure settings for this item.')),
);
}
// ---------------------------------------------------------------------------
// AJAX forms and stuff.
/**
* Ajax entry point to add an context
*/
function ctools_context_ajax_item_add($mechanism = NULL, $type = NULL, $cache_key = NULL, $name = NULL, $step = NULL) {
ctools_include('ajax');
ctools_include('modal');
ctools_include('context');
ctools_include('cache');
ctools_include('plugins-admin');
if (!$name) {
return ctools_ajax_render_error();
}
// Load stored object from cache.
if (!($object = ctools_cache_get($mechanism, $cache_key))) {
ctools_ajax_render_error(t('Invalid object name.'));
}
// Get info about what we're adding, i.e, relationship, context, argument, etc.
$plugin_definition = ctools_context_get_plugin($type, $name);
if (empty($plugin_definition)) {
ctools_ajax_render_error(t('Invalid context type'));
}
// Set up the $conf array for this plugin
if (empty($step) || empty($object->temporary)) {
// Create the basis for our new context.
$conf = ctools_context_get_defaults($plugin_definition, $object, $type);
$object->temporary = &$conf;
}
else {
$conf = &$object->temporary;
}
// Load the contexts that may be used.
$base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
$contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
$type_info = ctools_context_info($type);
$form_state = array(
'ajax' => TRUE,
'modal' => TRUE,
'modal return' => TRUE,
'object' => &$object,
'conf' => &$conf,
'plugin' => $plugin_definition,
'type' => $type,
'contexts' => $contexts,
'title' => t('Add @type "@context"', array('@type' => $type_info['singular title'], '@context' => $plugin_definition['title'])),
'type info' => $type_info,
'op' => 'add',
'step' => $step,
);
$form_info = array(
'path' => "ctools/context/ajax/add/$mechanism/$type/$cache_key/$name/%step",
'show cancel' => TRUE,
'default form' => 'ctools_edit_context_form_defaults',
'auto caching' => TRUE,
'cache mechanism' => $mechanism,
'cache key' => $cache_key,
// This is stating what the cache will be referred to in $form_state
'cache storage' => 'object',
);
if ($type == 'requiredcontext') {
$form_info += array(
'add form name' => 'required context add form',
'edit form name' => 'required context edit form',
);
}
$output = ctools_plugin_configure_form($form_info, $form_state);
if (!empty($form_state['cancel'])) {
$output = array(ctools_modal_command_dismiss());
}
else if (!empty($form_state['complete'])) {
// Successful submit -- move temporary data to location.
// Create a reference to the place our context lives. Since this is fairly
// generic, this is the easiest way to get right to the place of the
// object without knowing precisely what data we're poking at.
$ref = &$object->{$type_info['key']};
// Figure out the position for our new context.
$position = empty($ref) ? 0 : max(array_keys($ref)) + 1;
$conf['id'] = ctools_context_next_id($ref, $name);
$ref[$position] = $conf;
if (isset($object->temporary)) {
unset($object->temporary);
}
ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
// Very irritating way to update the form for our contexts.
$arg_form_state = array('values' => array());
$arg_form = array(
'#post' => array(),
'#programmed' => FALSE,
'#tree' => FALSE,
);
// Build a chunk of the form to merge into the displayed form
$arg_form[$type] = array(
'#tree' => TRUE,
);
$arg_form[$type][$position] = array(
'#tree' => TRUE,
);
ctools_context_add_item_to_form($mechanism, $type, $cache_key, $arg_form[$type][$position], $position, $ref[$position]);
$arg_form = form_builder('ctools_context_form', $arg_form, $arg_form_state);
// Build the relationships table so we can ajax it in.
// This is an additional thing that goes in here.
$rel_form = array(
'#theme' => 'ctools_context_item_form',
'#cache_key' => $cache_key,
'#ctools_context_type' => 'relationship',
'#ctools_context_module' => $mechanism,
'#only_buttons' => TRUE,
'#post' => array(),
'#programmed' => FALSE,
'#tree' => FALSE,
);
$rel_form['relationship'] = array(
'#tree' => TRUE,
);
// Allow an object to set some 'base' contexts that come from elsewhere.
$rel_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
$all_contexts = ctools_context_load_contexts($object, TRUE, $rel_contexts);
$available_relationships = ctools_context_get_relevant_relationships($all_contexts);
$output = array();
if (!empty($available_relationships)) {
ctools_context_add_item_table_buttons('relationship', $mechanism, $rel_form, $available_relationships);
$rel_form = form_builder('dummy_form_id', $rel_form, $arg_form_state);
$output[] = ajax_command_replace('div#ctools-relationships-table div.buttons', drupal_render($rel_form));
}
$theme_vars = array();
$theme_vars['type'] = $type;
$theme_vars['form'] = $arg_form[$type][$position];
$theme_vars['position'] = $position;
$theme_vars['count'] = $position;
$text = theme('ctools_context_item_row', $theme_vars);
$output[] = ajax_command_append('#' . $type . '-table tbody', $text);
$output[] = ajax_command_changed('#' . $type . '-row-' . $position, '.title');
$output[] = ctools_modal_command_dismiss();
}
else {
$output = ctools_modal_form_render($form_state, $output);
}
print ajax_render($output);
exit;
}
/**
* Ajax entry point to edit an item
*/
function ctools_context_ajax_item_edit($mechanism = NULL, $type = NULL, $cache_key = NULL, $position = NULL, $step = NULL) {
ctools_include('ajax');
ctools_include('modal');
ctools_include('context');
ctools_include('cache');
ctools_include('plugins-admin');
if (!isset($position)) {
return ctools_ajax_render_error();
}
// Load stored object from cache.
if (!($object = ctools_cache_get($mechanism, $cache_key))) {
ctools_ajax_render_error(t('Invalid object name.'));
}
$type_info = ctools_context_info($type);
// Create a reference to the place our context lives. Since this is fairly
// generic, this is the easiest way to get right to the place of the
// object without knowing precisely what data we're poking at.
$ref = &$object->{$type_info['key']};
if (empty($step) || empty($object->temporary)) {
// Create the basis for our new context.
$conf = $object->{$type_info['key']}[$position];
$object->temporary = &$conf;
}
else {
$conf = &$object->temporary;
}
$name = $ref[$position]['name'];
if (empty($name)) {
ctools_ajax_render_error();
}
// load the plugin definition
$plugin_definition = ctools_context_get_plugin($type, $name);
if (empty($plugin_definition)) {
ctools_ajax_render_error(t('Invalid context type'));
}
// Load the contexts
$base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
$contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
$form_state = array(
'ajax' => TRUE,
'modal' => TRUE,
'modal return' => TRUE,
'object' => &$object,
'conf' => &$conf,
'position' => $position,
'plugin' => $plugin_definition,
'type' => $type,
'contexts' => $contexts,
'title' => t('Edit @type "@context"', array('@type' => $type_info['singular title'], '@context' => $plugin_definition['title'])),
'type info' => $type_info,
'op' => 'add',
'step' => $step,
);
$form_info = array(
'path' => "ctools/context/ajax/configure/$mechanism/$type/$cache_key/$position/%step",
'show cancel' => TRUE,
'default form' => 'ctools_edit_context_form_defaults',
'auto caching' => TRUE,
'cache mechanism' => $mechanism,
'cache key' => $cache_key,
// This is stating what the cache will be referred to in $form_state
'cache storage' => 'object',
);
if ($type == 'requiredcontext') {
$form_info += array(
'add form name' => 'required context add form',
'edit form name' => 'required context edit form',
);
}
$output = ctools_plugin_configure_form($form_info, $form_state);
if (!empty($form_state['cancel'])) {
$output = array(ctools_modal_command_dismiss());
}
else if (!empty($form_state['complete'])) {
// successful submit
$ref[$position] = $conf;
if (isset($object->temporary)) {
unset($object->temporary);
}
ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
$output = array();
$output[] = ctools_modal_command_dismiss();
$arg_form = array(
'#post' => array(),
'#programmed' => FALSE,
'#tree' => FALSE,
);
// Build a chunk of the form to merge into the displayed form
$arg_form[$type] = array(
'#tree' => TRUE,
);
$arg_form[$type][$position] = array(
'#tree' => TRUE,
);
ctools_context_add_item_to_form($mechanism, $type, $cache_key, $arg_form[$type][$position], $position, $ref[$position]);
$arg_form = form_builder('ctools_context_form', $arg_form, $arg_form_state);
$theme_vars = array();
$theme_vars['type'] = $type;
$theme_vars['form'] = $arg_form[$type][$position];
$theme_vars['position'] = $position;
$theme_vars['count'] = $position;
$output[] = ajax_command_replace('#' . $type . '-row-' . $position, theme('ctools_context_item_row', $theme_vars));
$output[] = ajax_command_changed('#' . $type . '-row-' . $position, '.title');
}
else {
$output = ctools_modal_form_render($form_state, $output);
}
print ajax_render($output);
exit;
}
/**
* Get the defaults for a new instance of a context plugin.
*
* @param $plugin_definition
* The metadata definition of the plugin from ctools_get_plugins().
* @param $object
* The object the context plugin will be added to.
* @param $type
* The type of context plugin. i.e, context, requiredcontext, relationship
*/
function ctools_context_get_defaults($plugin_definition, $object, $type) {
// Fetch the potential id of the plugin so we can append
// title and keyword information for new ones.
$type_info = ctools_context_info($type);
$id = ctools_context_next_id($object->{$type_info['key']}, $plugin_definition['name']);
$conf = array(
'identifier' => $plugin_definition['title'] . ($id > 1 ? ' ' . $id : ''),
'keyword' => ctools_get_keyword($object, $plugin_definition['keyword']),
'name' => $plugin_definition['name'],
);
if (isset($plugin_definition['defaults'])) {
$defaults = $plugin_definition['defaults'];
}
else if (isset($subtype['defaults'])) {
$defaults = $subtype['defaults'];
}
if (isset($defaults)) {
if (is_string($defaults) && function_exists($defaults)) {
if ($settings = $defaults($plugin_definition)) {
$conf += $settings;
}
}
else if (is_array($defaults)) {
$conf += $defaults;
}
}
return $conf;
}
/**
* Form wrapper for the edit context form.
*
* @todo: We should uncombine these.
*/
function ctools_edit_context_form_defaults($form, &$form_state) {
// Basic values required to orient ourselves
$object = $form_state['object'];
$plugin_definition = $form_state['plugin'];
$type_info = $form_state['type info'];
$contexts = $form_state['contexts'];
$conf = $form_state['conf'];
if ($type_info['key'] == 'arguments' && !isset($conf['default'])) {
$conf['default'] = 'ignore';
$conf['title'] = '';
}
$form['description'] = array(
'#prefix' => '<div class="description">',
'#suffix' => '</div>',
'#markup' => check_plain($plugin_definition['description']),
);
if ($type_info['key'] == 'relationships') {
$form['context'] = ctools_context_selector($contexts, $plugin_definition['required context'], isset($conf['context']) ? $conf['context'] : '');
}
if ($type_info['key'] == 'arguments') {
$form['default'] = array(
'#type' => 'select',
'#title' => t('Default'),
'#options' => array(
'ignore' => t('Ignore it; content that requires this context will not be available.'),
'404' => t('Display page not found or display nothing at all.'),
),
'#default_value' => $conf['default'],
'#description' => t('If the argument is missing or is not valid, select how this should behave.'),
);
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $conf['title'],
'#description' => t('Enter a title to use when this argument is present. You may use %KEYWORD substitution, where the keyword is specified below.'),
);
}
$form['identifier'] = array(
'#type' => 'textfield',
'#title' => t('Identifier'),
'#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' => t('context'))),
'#default_value' => $conf['identifier'],
);
$form['keyword'] = array(
'#type' => 'textfield',
'#title' => t('Keyword'),
'#description' => t('Enter a keyword to use for substitution in titles.'),
'#default_value' => $conf['keyword'],
);
$form['#submit'][] = 'ctools_edit_context_form_defaults_submit';
return $form;
}
/**
* Submit handler to store context identifier and keyword info.
*/
function ctools_edit_context_form_defaults_submit(&$form, &$form_state) {
if ($form_state['type info']['key'] == 'relationships') {
$form_state['conf']['context'] = $form_state['values']['context'];
}
if ($form_state['type info']['key'] == 'arguments') {
$form_state['conf']['default'] = $form_state['values']['default'];
$form_state['conf']['title'] = $form_state['values']['title'];
}
$form_state['conf']['identifier'] = $form_state['values']['identifier'];
$form_state['conf']['keyword'] = $form_state['values']['keyword'];
}
/**
* Ajax entry point to edit an item
*/
function ctools_context_ajax_item_delete($mechanism = NULL, $type = NULL, $cache_key = NULL, $position = NULL) {
ctools_include('ajax');
ctools_include('context');
ctools_include('cache');
if (!isset($position)) {
return ctools_ajax_render_error();
}
// Load stored object from cache.
if (!($object = ctools_cache_get($mechanism, $cache_key))) {
ctools_ajax_render_error(t('Invalid object name.'));
}
$type_info = ctools_context_info($type);
// Create a reference to the place our context lives. Since this is fairly
// generic, this is the easiest way to get right to the place of the
// object without knowing precisely what data we're poking at.
$ref = &$object->{$type_info['key']};
if (!array_key_exists($position, $ref)) {
ctools_ajax_render_error(t('Unable to delete missing item!'));
}
unset($ref[$position]);
ctools_cache_operation($mechanism, $cache_key, 'finalize', $object);
$output = array();
$output[] = ajax_command_replace('#' . $type . '-row-' . $position, '');
$output[] = ajax_command_restripe("#$type-table");
print ajax_render($output);
exit;
}
// --- End of contexts
function ctools_save_context($type, &$ref, $form_values) {
$type_info = ctools_context_info($type);
// Organize arguments
$new = array();
$order = array();
foreach ($ref as $id => $context) {
$position = $form_values[$type][$id]['position'];
$order[$position] = $id;
}
ksort($order);
foreach ($order as $id) {
$new[] = $ref[$id];
}
$ref = $new;
}
function ctools_get_keyword($page, $word) {
// Create a complete set of keywords
$keywords = array();
foreach (array('arguments', 'relationships', 'contexts', 'requiredcontexts') as $type) {
if (!empty($page->$type) && is_array($page->$type)) {
foreach ($page->$type as $info) {
$keywords[$info['keyword']] = TRUE;
}
}
}
$keyword = $word;
$count = 1;
while (!empty($keywords[$keyword])) {
$keyword = $word . '_' . ++$count;
}
return $keyword;
}

View File

@ -0,0 +1,536 @@
<?php
/**
* @file
* Support for creating 'context' type task handlers.
*
* Context task handlers expect the task to provide 0 or more contexts. The
* task handler should use those contexts as selection rules, as well as
* rendering with them.
*
* The functions and forms in this file should be common to every context type
* task handler made.
*
* Forms:
* - ...
*/
/**
* Render a context type task handler given a list of handlers
* attached to a type.
*
* @param $task
* The $task object in use.
* @param $subtask
* The id of the subtask in use.
* @param $contexts
* The context objects in use.
* @param $args
* The raw arguments behind the contexts.
* @param $page
* If TRUE then this renderer owns the page and can use theme('page')
* for no blocks; if false, output is returned regardless of any no
* blocks settings.
* @return
* Either the output or NULL if there was output, FALSE if no handler
* accepted the task. If $page is FALSE then the $info block is returned instead.
*/
function ctools_context_handler_render($task, $subtask, $contexts, $args) {
// Load the landlers, choosing only enabled handlers.
$handlers = page_manager_load_sorted_handlers($task, $subtask ? $subtask['name'] : '', TRUE);
$id = ctools_context_handler_get_render_handler($task, $subtask, $handlers, $contexts, $args);
if ($id) {
return ctools_context_handler_render_handler($task, $subtask, $handlers[$id], $contexts, $args);
}
return FALSE;
}
/**
* Figure out which of the listed handlers should be used to render.
*/
function ctools_context_handler_get_render_handler($task, $subtask, $handlers, $contexts, $args) {
// Try each handler.
foreach ($handlers as $id => $handler) {
$plugin = page_manager_get_task_handler($handler->handler);
// First, see if the handler has a tester.
$function = ctools_plugin_get_function($plugin, 'test');
if ($function) {
$test = $function($handler, $contexts, $args);
if ($test) {
return $id;
}
}
else {
// If not, if it's a 'context' type handler, use the default tester.
if ($plugin['handler type'] == 'context') {
$test = ctools_context_handler_default_test($handler, $contexts, $args);
if ($test) {
return $id;
}
}
}
}
return FALSE;
}
/**
* Default test function to see if a task handler should be rendered.
*
* This tests against the standard selection criteria that most task
* handlers should be implementing.
*/
function ctools_context_handler_default_test($handler, $base_contexts, $args) {
ctools_include('context');
// Add my contexts
$contexts = ctools_context_handler_get_handler_contexts($base_contexts, $handler);
// Test.
return ctools_context_handler_select($handler, $contexts);
}
/**
* Render a task handler.
*/
function ctools_context_handler_render_handler($task, $subtask, $handler, $contexts, $args, $page = TRUE) {
$function = page_manager_get_renderer($handler);
if (!$function) {
return NULL;
}
if ($page) {
if ($subtask) {
$task_name = page_manager_make_task_name($task['name'], $subtask['name']);
}
else {
$task_name = $task['name'];
}
page_manager_get_current_page(array(
'name' => $task_name,
'task' => $task,
'subtask' => $subtask,
'contexts' => $contexts,
'arguments' => $args,
'handler' => $handler,
));
}
$info = $function($handler, $contexts, $args);
if (!$info) {
return NULL;
}
$context = array(
'args' => $args,
'contexts' => $contexts,
'task' => $task,
'subtask' => $subtask,
'handler' => $handler
);
drupal_alter('ctools_render', $info, $page, $context);
// If we don't own the page, let the caller deal with rendering.
if (!$page) {
return $info;
}
if (!empty($info['response code']) && $info['response code'] != 200) {
switch ($info['response code']) {
case 403:
return MENU_ACCESS_DENIED;
case 404:
return MENU_NOT_FOUND;
case 301:
case 302:
case 303:
case 304:
case 305:
case 307:
$info += array(
'query' => array(),
'fragment' => '',
);
$options = array(
'query' => $info['query'],
'fragment' => $info['fragment'],
);
return drupal_goto($info['destination'], $options, $info['response code']);
// @todo -- should other response codes be supported here?
}
}
$plugin = page_manager_get_task_handler($handler->handler);
if (module_exists('contextual') && user_access('access contextual links') && isset($handler->task)) {
// Provide a contextual link to edit this, if we can:
$callback = isset($plugin['contextual link']) ? $plugin['contextual link'] : 'ctools_task_handler_default_contextual_link';
if ($callback && function_exists($callback)) {
$links = $callback($handler, $plugin, $contexts, $args);
}
if (!empty($links) && is_array($links)) {
$build = array(
'#theme_wrappers' => array('container'),
'#attributes' => array('class' => array('contextual-links-region')),
);
if (!is_array($info['content'])) {
$build['content']['#markup'] = $info['content'];
}
else {
$build['content'] = $info['content'];
}
$build['contextual_links'] = array(
'#prefix' => '<div class="contextual-links-wrapper">',
'#suffix' => '</div>',
'#theme' => 'links__contextual',
'#links' => $links,
'#attributes' => array('class' => array('contextual-links')),
'#attached' => array(
'library' => array(array('contextual', 'contextual-links')),
),
);
$info['content'] = $build;
}
}
foreach (ctools_context_handler_get_task_arguments($task, $subtask) as $id => $argument) {
$plugin = ctools_get_argument($argument['name']);
$cid = ctools_context_id($argument, 'argument');
if (!empty($contexts[$cid]) && ($function = ctools_plugin_get_function($plugin, 'breadcrumb'))) {
$function($argument['settings'], $contexts[$cid]);
}
}
if (isset($info['title'])) {
drupal_set_title($info['title'], PASS_THROUGH);
}
// Only directly output if $page was set to true.
if (!empty($info['no_blocks'])) {
ctools_set_no_blocks(FALSE);
}
return $info['content'];
}
/**
* Default function to provide contextual link for a task as defined by the handler.
*
* This provides a simple link to th main content operation and is suitable
* for most normal handlers. Setting 'contextual link' to a function overrides
* this and setting it to FALSE will prevent a contextual link from appearing.
*/
function ctools_task_handler_default_contextual_link($handler, $plugin, $contexts, $args) {
if (!user_access('administer page manager')) {
return;
}
$task = page_manager_get_task($handler->task);
$title = !empty($task['tab title']) ? $task['tab title'] : t('Edit @type', array('@type' => $plugin['title']));
$trail = array();
if (!empty($plugin['tab operation'])) {
if (is_array($plugin['tab operation'])) {
$trail = $plugin['tab operation'];
}
else if (function_exists($plugin['tab operation'])) {
$trail = $plugin['tab operation']($handler, $contexts, $args);
}
}
$path = page_manager_edit_url(page_manager_make_task_name($handler->task, $handler->subtask), $trail);
$links = array(array(
'href' => $path,
'title' => $title,
'query' => drupal_get_destination(),
));
return $links;
}
/**
* Called to execute actions that should happen before a handler is rendered.
*/
function ctools_context_handler_pre_render($handler, $contexts, $args) { }
/**
* Compare arguments to contexts for selection purposes.
*
* @param $handler
* The handler in question.
* @param $contexts
* The context objects provided by the task.
*
* @return
* TRUE if these contexts match the selection rules. NULL or FALSE
* otherwise.
*/
function ctools_context_handler_select($handler, $contexts) {
if (empty($handler->conf['access'])) {
return TRUE;
}
ctools_include('context');
return ctools_access($handler->conf['access'], $contexts);
}
/**
* Get the array of summary strings for the arguments.
*
* These summary strings are used to communicate to the user what
* arguments the task handlers are selecting.
*
* @param $task
* The loaded task plugin.
* @param $subtask
* The subtask id.
* @param $handler
* The handler to be checked.
*/
function ctools_context_handler_summary($task, $subtask, $handler) {
if (empty($handler->conf['access']['plugins'])) {
return array();
}
ctools_include('context');
$strings = array();
$contexts = ctools_context_handler_get_all_contexts($task, $subtask, $handler);
foreach ($handler->conf['access']['plugins'] as $test) {
$plugin = ctools_get_access_plugin($test['name']);
if ($string = ctools_access_summary($plugin, $contexts, $test)) {
$strings[] = $string;
}
}
return $strings;
}
// --------------------------------------------------------------------------
// Tasks and Task handlers can both have their own sources of contexts.
// Sometimes we need all of these contexts at once (when editing
// the task handler, for example) but sometimes we need them separately
// (when a task has contexts loaded and is trying out the task handlers,
// for example). Therefore there are two paths we can take to getting contexts.
/**
* Load the contexts for a task, using arguments.
*
* This creates the base array of contexts, loaded from arguments, suitable
* for use in rendering.
*/
function ctools_context_handler_get_task_contexts($task, $subtask, $args) {
$contexts = ctools_context_handler_get_base_contexts($task, $subtask);
$arguments = ctools_context_handler_get_task_arguments($task, $subtask);
ctools_context_get_context_from_arguments($arguments, $contexts, $args);
return $contexts;
}
/**
* Load the contexts for a task handler.
*
* This expands a base set of contexts passed in from a task with the
* contexts defined on the task handler. The contexts from the task
* must already have been loaded.
*/
function ctools_context_handler_get_handler_contexts($contexts, $handler) {
$object = ctools_context_handler_get_handler_object($handler);
return ctools_context_load_contexts($object, FALSE, $contexts);
}
/**
* Load the contexts for a task and task handler together.
*
* This pulls the arguments from a task and everything else from a task
* handler and loads them as a group. Since there is no data, this loads
* the contexts as placeholders.
*/
function ctools_context_handler_get_all_contexts($task, $subtask, $handler) {
$contexts = array();
$object = ctools_context_handler_get_task_object($task, $subtask, $handler);
$contexts = ctools_context_load_contexts($object, TRUE, $contexts);
ctools_context_handler_set_access_restrictions($task, $subtask, $handler, $contexts);
return $contexts;
}
/**
* Create an object suitable for use with the context system that kind of
* expects things in a certain, kind of clunky format.
*/
function ctools_context_handler_get_handler_object($handler) {
$object = new stdClass;
$object->name = $handler->name;
$object->contexts = isset($handler->conf['contexts']) ? $handler->conf['contexts'] : array();
$object->relationships = isset($handler->conf['relationships']) ? $handler->conf['relationships'] : array();
return $object;
}
/**
* Create an object suitable for use with the context system that kind of
* expects things in a certain, kind of clunky format. This one adds in
* arguments from the task.
*/
function ctools_context_handler_get_task_object($task, $subtask, $handler) {
$object = new stdClass;
$object->name = !empty($handler->name) ? $handler->name : 'temp';
$object->base_contexts = ctools_context_handler_get_base_contexts($task, $subtask, TRUE);
$object->arguments = ctools_context_handler_get_task_arguments($task, $subtask);
$object->contexts = isset($handler->conf['contexts']) ? $handler->conf['contexts'] : array();
$object->relationships = isset($handler->conf['relationships']) ? $handler->conf['relationships'] : array();
return $object;
}
/**
* Get base contexts from a task, if it has any.
*
* Tasks can get their contexts either from base contexts or arguments; base
* contexts extract their information from the environment.
*/
function ctools_context_handler_get_base_contexts($task, $subtask, $placeholders = FALSE) {
if ($function = ctools_plugin_get_function($task, 'get base contexts')) {
return $function($task, $subtask, $placeholders);
}
return array();
}
/**
* Get the arguments from a task that are used to load contexts.
*/
function ctools_context_handler_get_task_arguments($task, $subtask) {
if ($function = ctools_plugin_get_function($task, 'get arguments')) {
return $function($task, $subtask);
}
return array();
}
/**
* Set any access restrictions on the contexts for a handler.
*
* Both the task and the handler could add restrictions to the contexts
* based upon the access control. These restrictions might be useful
* to limit what kind of content appears in the add content dialog;
* for example, if we have an access item that limits a node context
* to only 'story' and 'page' types, there is no need for content that
* only applies to the 'poll' type to appear.
*/
function ctools_context_handler_set_access_restrictions($task, $subtask, $handler, &$contexts) {
// First, for the task:
if ($function = ctools_plugin_get_function($task, 'access restrictions')) {
$function($task, $subtask, $contexts);
}
// Then for the handler:
if (isset($handler->conf['access'])) {
ctools_access_add_restrictions($handler->conf['access'], $contexts);
}
}
/**
* Form to choose context based selection rules for a task handler.
*
* The configuration will be assumed to go simply in $handler->conf and
* will be keyed by the argument ID.
*/
function ctools_context_handler_edit_criteria($form, &$form_state) {
if (!isset($form_state['handler']->conf['access'])) {
$form_state['handler']->conf['access'] = array();
}
ctools_include('context');
ctools_include('modal');
ctools_include('ajax');
ctools_modal_add_plugin_js(ctools_get_access_plugins());
ctools_include('context-access-admin');
$form_state['module'] = (isset($form_state['module'])) ? $form_state['module'] : 'page_manager_task_handler';
// Encode a bunch of info into the argument so we can get our cache later
$form_state['callback argument'] = $form_state['task_name'] . '*' . $form_state['handler']->name;
$form_state['access'] = $form_state['handler']->conf['access'];
$form_state['no buttons'] = TRUE;
$form_state['contexts'] = ctools_context_handler_get_all_contexts($form_state['task'], $form_state['subtask'], $form_state['handler']);
$form['markup'] = array(
'#markup' => '<div class="description">' .
t('If there is more than one variant on a page, when the page is visited each variant is given an opportunity to be displayed. Starting from the first variant and working to the last, each one tests to see if its selection rules will pass. The first variant that meets its criteria (as specified below) will be used.') .
'</div>',
);
$form = ctools_access_admin_form($form, $form_state);
return $form;
}
/**
* Submit handler for rules selection
*/
function ctools_context_handler_edit_criteria_submit(&$form, &$form_state) {
$form_state['handler']->conf['access']['logic'] = $form_state['values']['logic'];
}
/**
* Edit contexts that go with this panel.
*/
function ctools_context_handler_edit_context($form, &$form_state) {
ctools_include('context-admin');
ctools_context_admin_includes();
$handler = $form_state['handler'];
$page = $form_state['page'];
$cache_name = $handler->name ? $handler->name : 'temp';
if (isset($page->context_cache[$cache_name])) {
$cache = $page->context_cache[$cache_name];
}
else {
$cache = ctools_context_handler_get_task_object($form_state['task'], $form_state['subtask'], $form_state['handler']);
$form_state['page']->context_cache[$cache_name] = $cache;
}
$form['right'] = array(
'#prefix' => '<div class="clearfix"><div class="right-container">',
'#suffix' => '</div>',
);
$form['left'] = array(
'#prefix' => '<div class="left-container">',
'#suffix' => '</div></div>',
);
$module = 'page_manager_context::' . $page->task_name;
ctools_context_add_context_form($module, $form, $form_state, $form['right']['contexts_table'], $cache);
ctools_context_add_relationship_form($module, $form, $form_state, $form['right']['relationships_table'], $cache);
$theme_vars = array();
$theme_vars['object'] = $cache;
$theme_vars['header'] = t('Summary of contexts');
$form['left']['summary'] = array(
'#prefix' => '<div class="page-manager-contexts">',
'#suffix' => '</div>',
'#markup' => theme('ctools_context_list', $theme_vars),
);
$form_state['context_object'] = &$cache;
return $form;
}
/**
* Process submission of the context edit form.
*/
function ctools_context_handler_edit_context_submit(&$form, &$form_state) {
$handler = &$form_state['handler'];
$cache_name = $handler->name ? $handler->name : 'temp';
$handler->conf['contexts'] = $form_state['context_object']->contexts;
$handler->conf['relationships'] = $form_state['context_object']->relationships;
if (isset($form_state['page']->context_cache[$cache_name])) {
unset($form_state['page']->context_cache[$cache_name]);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains menu item registration for the context tool.
*
* The menu items registered are AJAX callbacks for the context configuration
* popups. They are kept separately for organizational purposes.
*/
function ctools_context_menu(&$items) {
$base = array(
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
'file' => 'includes/context-admin.inc',
'theme callback' => 'ajax_base_page_theme',
);
$items['ctools/context/ajax/add'] = array(
'page callback' => 'ctools_context_ajax_item_add',
) + $base;
$items['ctools/context/ajax/configure'] = array(
'page callback' => 'ctools_context_ajax_item_edit',
) + $base;
$items['ctools/context/ajax/delete'] = array(
'page callback' => 'ctools_context_ajax_item_delete',
) + $base;
// For the access system
$base['file'] = 'includes/context-access-admin.inc';
$items['ctools/context/ajax/access/add'] = array(
'page callback' => 'ctools_access_ajax_add',
) + $base;
$items['ctools/context/ajax/access/configure'] = array(
'page callback' => 'ctools_access_ajax_edit',
) + $base;
$items['ctools/context/ajax/access/delete'] = array(
'page callback' => 'ctools_access_ajax_delete',
) + $base;
}

View File

@ -0,0 +1,24 @@
<?php
/**
* @file
* Contains plugin type registration information for the context tool.
*
* Don't actually need to declare anything for these plugin types right now,
* apart from the fact that they exist. So, an empty array.
*/
function ctools_context_plugin_type(&$items) {
$items['contexts'] = array(
'child plugins' => TRUE,
);
$items['arguments'] = array(
'child plugins' => TRUE,
);
$items['relationships'] = array(
'child plugins' => TRUE,
);
$items['access'] = array(
'child plugins' => TRUE,
);
}

View File

@ -0,0 +1,344 @@
<?php
/**
* @file
* Contains theme registry and theme implementations for the context tool.
*/
/**
* Implements hook_theme()
*/
function ctools_context_theme(&$theme) {
$theme['ctools_context_list'] = array(
'variables' => array('object' => NULL),
'file' => 'includes/context.theme.inc',
);
$theme['ctools_context_list_no_table'] = array(
'variables' => array('object' => NULL),
'file' => 'includes/context.theme.inc',
);
$theme['ctools_context_item_form'] = array(
'render element' => 'form',
// 'variables' => array('form' => NULL),
'file' => 'includes/context.theme.inc',
);
$theme['ctools_context_item_row'] = array(
'variables' => array('type' => NULL, 'form' => NULL, 'position' => NULL, 'count' => NULL, 'with_tr' => TRUE),
'file' => 'includes/context.theme.inc',
);
// For the access plugin
$theme['ctools_access_admin_add'] = array(
'render element' => 'form',
'file' => 'includes/context-access-admin.inc',
);
}
/**
* Theme the form item for the context entry.
*/
function theme_ctools_context_item_row($vars) {
$type = $vars['type'];
$form = $vars['form'];
$position = $vars['position'];
$count = $vars['count'];
$with_tr = $vars['with_tr'];
$output = '<td class="title">&nbsp;' . render($form['title']) . '</td>';
if (!empty($form['position'])) {
$output .= '<td class="position">&nbsp;' . render($form['position']) . '</td>';
}
$output .= '<td class="operation">' . render($form['settings']);
$output .= render($form['remove']) . '</td>';
if ($with_tr) {
$output = '<tr id="' . $type . '-row-' . $position . '" class="draggable ' . $type . '-row ' . ($count % 2 ? 'even' : 'odd') . '">' . $output . '</tr>';
}
return $output;
}
/**
* Display the context item.
*/
function theme_ctools_context_item_form($vars) {
$form = $vars['form'];
$output = '';
$type = $form['#ctools_context_type'];
$module = $form['#ctools_context_module'];
$cache_key = $form['#cache_key'];
$type_info = ctools_context_info($type);
if (!empty($form[$type]) && empty($form['#only_buttons'])) {
$count = 0;
$rows = '';
foreach (array_keys($form[$type]) as $id) {
if (!is_numeric($id)) {
continue;
}
$theme_vars = array();
$theme_vars['type'] = $type;
$theme_vars['form'] = $form[$type][$id];
$theme_vars['position'] = $id;
$theme_vars['count'] = $count++;
$rows .= theme('ctools_context_item_row', $theme_vars);
}
$output .= '<table id="' . $type . '-table">';
$output .= '<thead>';
$output .= '<tr>';
$output .= '<th class="title">' . $type_info['title'] . '</th>';
if (!empty($type_info['sortable']) && $count) {
$output .= '<th class="position">' . t('Weight') . '</th>';
}
$output .= '<th class="operation">' . t('Operation') . '</th>';
$output .= '</tr>';
$output .= '</thead>';
$output .= '<tbody>';
$output .= $rows;
$output .= '</tbody>';
$output .= '</table>';
}
if (!empty($form['buttons'])) {
// Display the add context item.
$row = array();
$row[] = array('data' => render($form['buttons'][$type]['item']), 'class' => array('title'));
$row[] = array('data' => render($form['buttons'][$type]['add']), 'class' => array('add'), 'width' => "60%");
$output .= '<div class="buttons">';
$output .= render($form['buttons'][$type]);
$theme_vars = array();
$theme_vars['header'] = array();
$theme_vars['rows'] = array($row);
$theme_vars['attributes'] = array('id' => $type . '-add-table');
$output .= theme('table', $theme_vars);
$output .= '</div>';
}
if (!empty($form['description'])) {
$output .= render($form['description']);
}
if (!empty($type_info['sortable'])) {
drupal_add_tabledrag($type . '-table', 'order', 'sibling', 'drag-position');
}
return $output;
}
/**
* Create a visible list of all the contexts available on an object.
* Assumes arguments, relationships and context objects.
*
* Contexts must be preloaded.
*/
function theme_ctools_context_list($vars) {
$object = $vars['object'];
$header = $vars['header'];
$description = (!empty($vars['description'])) ? $vars['description'] : NULL;
$titles = array();
$output = '';
$count = 1;
$contexts = ctools_context_load_contexts($object);
// Describe 'built in' contexts.
if (!empty($object->base_contexts)) {
foreach ($object->base_contexts as $id => $context) {
$output .= '<tr>';
$output .= '<td valign="top"><em>' . t('Built in context') . '</em></td>';
$desc = check_plain($context->identifier);
if (isset($context->keyword)) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context->keyword));
foreach (ctools_context_get_converters('%' . $context->keyword . ':', $context) as $keyword => $title) {
$desc .= '<br />' . t('@keyword --&gt; @title', array('@keyword' => $keyword, '@title' => $title));
}
$desc .= '</div>';
}
if (isset($context->description)) {
$desc .= '<div class="description">' . filter_xss_admin($context->description) . '</div>';
}
$output .= '<td>' . $desc . '</td>';
$output .= '</tr>';
$titles[$id] = $context->identifier;
}
}
// First, make a list of arguments. Arguments are pretty simple.
if (!empty($object->arguments)) {
foreach ($object->arguments as $argument) {
$output .= '<tr>';
$output .= '<td valign="top"><em>' . t('Argument @count', array('@count' => $count)) . '</em></td>';
$desc = check_plain($argument['identifier']);
if (isset($argument['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $argument['keyword']));
if (isset($contexts[ctools_context_id($argument, 'argument')])) {
foreach (ctools_context_get_converters('%' . $argument['keyword'] . ':', $contexts[ctools_context_id($argument, 'argument')]) as $keyword => $title) {
$desc .= '<br />' . t('@keyword --&gt; @title', array('@keyword' => $keyword, '@title' => $title));
}
}
$desc .= '</div>';
}
$output .= '<td>' . $desc . '</td>';
$output .= '</tr>';
$titles[ctools_context_id($argument, 'argument')] = $argument['identifier'];
$count++;
}
}
$count = 1;
// Then, make a nice list of contexts.
if (!empty($object->contexts)) {
foreach ($object->contexts as $context) {
$output .= '<tr>';
$output .= '<td valign="top"><em>' . t('Context @count', array('@count' => $count)) . '</em></td>';
$desc = check_plain($context['identifier']);
if (isset($context['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context['keyword']));
foreach (ctools_context_get_converters('%' . $context['keyword'] . ':', $contexts[ctools_context_id($context, 'context')]) as $keyword => $title) {
$desc .= '<br />' . t('@keyword --&gt; @title', array('@keyword' => $keyword, '@title' => $title));
}
$desc .= '</div>';
}
$output .= '<td>' . $desc . '</td>';
$output .= '</tr>';
$titles[ctools_context_id($context)] = $context['identifier'];
$count++;
}
}
// And relationships
if (!empty($object->relationships)) {
foreach ($object->relationships as $relationship) {
$output .= '<tr>';
if (is_array($relationship['context'])) {
$rtitles = array();
foreach ($relationship['context'] as $cid) {
$rtitles[$cid] = $titles[$cid];
}
$title = implode(' + ', $rtitles);
}
else {
$title = $titles[$relationship['context']];
}
$output .= '<td valign="top"><em>' . t('From "@title"', array('@title' => $title)) . '</em></td>';
$desc = check_plain($relationship['identifier']);
if (isset($relationship['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $relationship['keyword']));
foreach (ctools_context_get_converters('%' . $relationship['keyword'] . ':', $contexts[ctools_context_id($relationship, 'relationship')]) as $keyword => $title) {
$desc .= '<br />' . t('@keyword --&gt; @title', array('@keyword' => $keyword, '@title' => $title));
}
$desc .= '</div>';
}
$output .= '<td>' . $desc . '</td>';
$output .= '</tr>';
$titles[ctools_context_id($relationship, 'relationship')] = $relationship['identifier'];
$count++;
}
}
$head = '';
if ($header) {
if ($description) {
$header .= '<div class="description">' . $description . '</div>';
}
$head .= '<thead><tr>';
$head .= '<th colspan="2">' . $header . '</th>';
$head .= '</tr></thead>';
}
return $output ? "<table>$head<tbody>$output</tbody></table>\n" : "<table>$head</table>\n";
}
/**
* ctools_context_list() but not in a table format because tabledrag
* won't let us have tables within tables and still drag.
*/
function theme_ctools_context_list_no_table($vars) {
$object = $vars['object'];
ctools_add_css('context');
$titles = array();
$output = '';
$count = 1;
// Describe 'built in' contexts.
if (!empty($object->base_contexts)) {
foreach ($object->base_contexts as $id => $context) {
$output .= '<div class="ctools-context-holder clearfix">';
$output .= '<div class="ctools-context-title">' . t('Built in context') . '</div>';
$desc = check_plain($context->identifier);
if (isset($context->keyword)) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context->keyword)) . '</div>';
}
if (isset($context->description)) {
$desc .= '<div class="description">' . filter_xss_admin($context->description) . '</div>';
}
$output .= '<div class="ctools-context-content">' . $desc . '</div>';
$output .= '</div>';
$titles[$id] = $context->identifier;
$count++;
}
}
// First, make a list of arguments. Arguments are pretty simple.
if (!empty($object->arguments)) {
foreach ($object->arguments as $argument) {
$output .= '<div class="ctools-context-holder clearfix">';
$output .= '<div class="ctools-context-title">' . t('Argument @count', array('@count' => $count)) . '</div>';
$desc = check_plain($argument['identifier']);
if (isset($argument['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $argument['keyword'])) . '</div>';
}
$output .= '<div class="ctools-context-content">' . $desc . '</div>';
$output .= '</div>';
$titles[ctools_context_id($argument, 'argument')] = $argument['identifier'];
$count++;
}
}
$count = 1;
// Then, make a nice list of contexts.
if (!empty($object->contexts)) {
foreach ($object->contexts as $context) {
$output .= '<div class="ctools-context-holder clearfix">';
$output .= '<div class="ctools-context-title">' . t('Context @count', array('@count' => $count)) . '</div>';
$desc = check_plain($context['identifier']);
if (isset($context['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context['keyword'])) . '</div>';
}
$output .= '<div class="ctools-context-content">' . $desc . '</div>';
$output .= '</div>';
$titles[ctools_context_id($context)] = $context['identifier'];
$count++;
}
}
// And relationships
if (!empty($object->relationships)) {
foreach ($object->relationships as $relationship) {
$output .= '<div class="ctools-context-holder clearfix">';
if (is_array($relationship['context'])) {
$rtitles = array();
foreach ($relationship['context'] as $cid) {
$rtitles[$cid] = $titles[$cid];
}
$title = implode(' + ', $rtitles);
}
else {
$title = $titles[$relationship['context']];
}
$output .= '<div class="ctools-context-title">' . t('From "@title"', array('@title' => $title)) . '</div>';
$desc = check_plain($relationship['identifier']);
if (isset($relationship['keyword'])) {
$desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $relationship['keyword'])) . '</div>';
}
$output .= '<div class="ctools-context-content">' . $desc . '</div>';
$output .= '</div>';
$titles[ctools_context_id($relationship, 'relationship')] = $relationship['identifier'];
$count++;
}
}
return $output;
}

View File

@ -0,0 +1,573 @@
<?php
/*
* @file
* CSS filtering functions. Contains a disassembler, filter, compressor, and
* decompressor.
*
* The general usage of this tool is:
*
* To simply filter CSS:
* @code
* $filtered_css = ctools_css_filter($css, TRUE);
* @endcode
*
* In the above, if the second argument is TRUE, the returned CSS will
* be compressed. Otherwise it will be returned in a well formatted
* syntax.
*
* To cache unfiltered CSS in a file, which will be filtered:
*
* @code
* $filename = ctools_css_cache($css, TRUE);
* @endcode
*
* In the above, if the second argument is FALSE, the CSS will not be filtered.
*
* This file will be cached within the Drupal files system. This system cannot
* detect when this file changes, so it is YOUR responsibility to remove and
* re-cache this file when the CSS is changed. Your system should also contain
* a backup method of re-generating the CSS cache in case it is removed, so
* that it is easy to force a re-cache by simply deleting the contents of the
* directory.
*
* Finally, if for some reason your application cannot store the filename
* (which is true of Panels where the style can't force the display to
* resave unconditionally) you can use the ctools storage mechanism. You
* simply have to come up with a unique Id:
*
* @code
* $filename = ctools_css_store($id, $css, TRUE);
* @endcode
*
* Then later on:
* @code
* $filename = ctools_css_retrieve($id);
* drupal_add_css($filename);
* @endcode
*
* The CSS that was generated will be stored in the database, so even if the
* file was removed the cached CSS will be used. If the CSS cache is
* cleared you may be required to regenerate your CSS. This will normally
* only be cleared by an administrator operation, not during normal usage.
*
* You may remove your stored CSS this way:
*
* @code
* ctools_css_clear($id);
* @endcode
*/
/**
* Store CSS with a given id and return the filename to use.
*
* This function associates a piece of CSS with an id, and stores the
* cached filename and the actual CSS for later use with
* ctools_css_retrieve.
*/
function ctools_css_store($id, $css, $filter = TRUE) {
$filename = db_query('SELECT filename FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchField();
if ($filename && file_exists($filename)) {
file_unmanaged_delete($filename);
}
// Remove any previous records.
db_delete('ctools_css_cache')
->condition('cid', $id)
->execute();
$filename = ctools_css_cache($css, $filter);
db_insert('ctools_css_cache')
->fields(array(
'cid' => $id,
'filename' => $filename,
'css' => $css,
'filter' => intval($filter),
))
->execute();
return $filename;
}
/**
* Retrieve a filename associated with an id of previously cached CSS.
*
* This will ensure the file still exists and, if not, create it.
*/
function ctools_css_retrieve($id) {
$cache = db_query('SELECT * FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchObject();
if (!$cache) {
return;
}
if (!file_exists($cache->filename)) {
$filename = ctools_css_cache($cache->css, $cache->filter);
if ($filename != $cache->filename) {
db_update('ctools_css_cache')
->fields(array('filename' => $filename))
->condition('cid', $id)
->execute();
$cache->filename = $filename;
}
}
return $cache->filename;
}
/**
* Remove stored CSS and any associated file.
*/
function ctools_css_clear($id) {
$cache = db_query('SELECT * FROM {ctools_css_cache} WHERE cid = :cid', array(':cid' => $id))->fetchObject();
if (!$cache) {
return;
}
if (file_exists($cache->filename)) {
file_unmanaged_delete($cache->filename);
// If we remove an existing file, there may be cached pages that refer
// to it. We must get rid of them: FIXME same format in D7?
cache_clear_all();
}
db_delete('ctools_css_cache')
->condition('cid', $id)
->execute();
}
/**
* Write a chunk of CSS to a temporary cache file and return the file name.
*
* This function optionally filters the CSS (always compressed, if so) and
* generates a unique filename based upon md5. It returns that filename that
* can be used with drupal_add_css(). Note that as a cache file, technically
* this file is volatile so it should be checked before it is used to ensure
* that it exists.
*
* You can use file_exists() to test for the file and file_delete() to remove
* it if it needs to be cleared.
*
* @param $css
* A chunk of well-formed CSS text to cache.
* @param $filter
* If TRUE the css will be filtered. If FALSE the text will be cached
* as-is.
*
* @return $filename
* The filename the CSS will be cached in.
*/
function ctools_css_cache($css, $filter = TRUE) {
if ($filter) {
$css = ctools_css_filter($css);
}
// Create the css/ within the files folder.
$path = 'public://ctools/css';
if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
// if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
drupal_set_message(t('Unable to create CTools CSS cache directory. Check the permissions on your files directory.'), 'error');
return;
}
// @todo Is this slow? Does it matter if it is?
$filename = $path . '/' . md5($css) . '.css';
// This will do renames if the file already exists, ensuring we don't
// accidentally overwrite other files who share the same md5. Yes this
// is a very miniscule chance but it's safe.
$filename = file_unmanaged_save_data($css, $filename);
return $filename;
}
/**
* Filter a chunk of CSS text.
*
* This function disassembles the CSS into a raw format that makes it easier
* for our tool to work, then runs it through the filter and reassembles it.
* If you find that you want the raw data for some reason or another, you
* can use the disassemble/assemble functions yourself.
*
* @param $css
* The CSS text to filter.
* @param $compressed
* If true, generate compressed output; if false, generate pretty output.
* Defaults to TRUE.
*/
function ctools_css_filter($css, $compressed = TRUE) {
$css_data = ctools_css_disassemble($css);
// Note: By using this function yourself you can control the allowed
// properties and values list.
$filtered = ctools_css_filter_css_data($css_data);
return $compressed ? ctools_css_compress($filtered) : ctools_css_assemble($filtered);
}
/**
* Re-assemble a css string and format it nicely.
*
* @param array $css_data
* An array of css data, as produced by @see ctools_css_disassemble()
* disassembler and the @see ctools_css_filter_css_data() filter.
*
* @return string $css
* css optimized for human viewing.
*/
function ctools_css_assemble($css_data) {
// Initialize the output.
$css = '';
// Iterate through all the statements.
foreach ($css_data as $selector_str => $declaration) {
// Add the selectors, separating them with commas and line feeds.
$css .= strpos($selector_str, ',') === FALSE ? $selector_str : str_replace(", ", ",\n", $selector_str);
// Add the opening curly brace.
$css .= " {\n";
// Iterate through all the declarations.
foreach ($declaration as $property => $value) {
$css .= " " . $property . ": " . $value . ";\n";
}
// Add the closing curly brace.
$css .= "}\n\n";
}
// Return the output.
return $css;
}
/**
* Compress css data (filter it first!) to optimize for use on view.
*
* @param array $css_data
* An array of css data, as produced by @see ctools_css_disassemble()
* disassembler and the @see ctools_css_filter_css_data() filter.
*
* @return string $css
* css optimized for use.
*/
function ctools_css_compress($css_data) {
// Initialize the output.
$css = '';
// Iterate through all the statements.
foreach ($css_data as $selector_str => $declaration) {
if (empty($declaration)) {
// Skip this statement if filtering removed all parts of the declaration.
continue;
}
// Add the selectors, separating them with commas.
$css .= $selector_str;
// And, the opening curly brace.
$css .= "{";
// Iterate through all the statement properties.
foreach ($declaration as $property => $value) {
$css .= $property . ':' . $value . ';';
}
// Add the closing curly brace.
$css .= "}";
}
// Return the output.
return $css;
}
/**
* Disassemble the css string.
*
* Strip the css of irrelevant characters, invalid/malformed selectors and
* declarations, and otherwise prepare it for processing.
*
* @param string $css
* A string containing the css to be disassembled.
*
* @return array $disassembled_css
* An array of disassembled, slightly cleaned-up/formatted css statements.
*/
function ctools_css_disassemble($css) {
$disassembled_css = array();
// Remove comments.
$css = preg_replace("/\/\*(.*)?\*\//Usi", "", $css);
// Split out each statement. Match either a right curly brace or a semi-colon
// that precedes a left curly brace with no right curly brace separating them.
$statements = preg_split('/}|;(?=[^}]*{)/', $css);
// If we have any statements, parse them.
if (!empty($statements)) {
// Iterate through all of the statements.
foreach ($statements as $statement) {
// Get the selector(s) and declaration.
if (empty($statement) || !strpos($statement, '{')) {
continue;
}
list($selector_str, $declaration) = explode('{', $statement);
// If the selector exists, then disassemble it, check it, and regenerate
// the selector string.
$selector_str = empty($selector_str) ? FALSE : _ctools_css_disassemble_selector($selector_str);
if (empty($selector_str)) {
// No valid selectors. Bomb out and start the next item.
continue;
}
// Disassemble the declaration, check it and tuck it into an array.
if (!isset($disassembled_css[$selector_str])) {
$disassembled_css[$selector_str] = array();
}
$disassembled_css[$selector_str] += _ctools_css_disassemble_declaration($declaration);
}
}
return $disassembled_css;
}
function _ctools_css_disassemble_selector($selector_str) {
// Get all selectors individually.
$selectors = explode(",", trim($selector_str));
// Iterate through all the selectors, sanity check them and return if they
// pass. Note that this handles 0, 1, or more valid selectors gracefully.
foreach ($selectors as $key => $selector) {
// Replace un-needed characters and do a little cleanup.
$selector = preg_replace("/[\n|\t|\\|\s]+/", ' ', trim($selector));
// Make sure this is still a real selector after cleanup.
if (!empty($selector)) {
$selectors[$key] = $selector;
}
else {
// Selector is no good, so we scrap it.
unset($selectors[$key]);
}
}
// Check for malformed selectors; if found, we skip this declaration.
if (empty($selectors)) {
return FALSE;
}
return implode(', ', $selectors);
}
function _ctools_css_disassemble_declaration($declaration) {
$formatted_statement = array();
$propval_pairs = explode(";", $declaration);
// Make sure we actually have some properties to work with.
if (!empty($propval_pairs)) {
// Iterate through the remains and parse them.
foreach ($propval_pairs as $key => $propval_pair) {
// Check that we have a ':', otherwise it's an invalid pair.
if (strpos($propval_pair, ':') === FALSE) {
continue;
}
// Clean up the current property-value pair.
$propval_pair = preg_replace("/[\n|\t|\\|\s]+/", ' ', trim($propval_pair));
// Explode the remaining fragements some more, but clean them up first.
list($property, $value) = explode(':', $propval_pair, 2);
// If the property survived, toss it onto the stack.
if (!empty($property)) {
$formatted_statement[trim($property)] = trim($value);
}
}
}
return $formatted_statement;
}
/**
* Run disassembled $css through the filter.
*
* @param $css
* CSS code disassembled by ctools_dss_disassemble().
* @param $allowed_properties
* A list of properties that are allowed by the filter. If empty
* ctools_css_filter_default_allowed_properties() will provide the
* list.
* @param $allowed_values
* A list of values that are allowed by the filter. If empty
* ctools_css_filter_default_allowed_values() will provide the
* list.
*
* @return
* An array of disassembled, filtered CSS.
*/
function ctools_css_filter_css_data($css, $allowed_properties = array(), $allowed_values = array(), $allowed_values_regex = '', $disallowed_values_regex = '') {
//function ctools_css_filter_css_data($css, &$filtered = NULL, $allowed_properties = array(), $allowed_values = array(), $allowed_values_regex = '', $disallowed_values_regex = '') {
// Retrieve the default list of allowed properties if none is provided.
$allowed_properties = !empty($allowed_properties) ? $allowed_properties : ctools_css_filter_default_allowed_properties();
// Retrieve the default list of allowed values if none is provided.
$allowed_values = !empty($allowed_values) ? $allowed_values : ctools_css_filter_default_allowed_values();
// Define allowed values regex if none is provided.
$allowed_values_regex = !empty($allowed_values_regex) ? $allowed_values_regex : '/(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)/';
// Define disallowed url() value contents, if none is provided.
// $disallowed_values_regex = !empty($disallowed_values_regex) ? $disallowed_values_regex : '/[url|expression]\s*\(\s*[^\s)]+?\s*\)\s*/';
$disallowed_values_regex = !empty($disallowed_values_regex) ? $disallowed_values_regex : '/(url|expression)/';
foreach ($css as $selector_str => $declaration) {
foreach ($declaration as $property => $value) {
if (!in_array($property, $allowed_properties)) {
// $filtered['properties'][$selector_str][$property] = $value;
unset($css[$selector_str][$property]);
continue;
}
$value = str_replace('!important', '', $value);
if (preg_match($disallowed_values_regex, $value) || !(in_array($value, $allowed_values) || preg_match($allowed_values_regex, $value))) {
// $filtered['values'][$selector_str][$property] = $value;
unset($css[$selector_str][$property]);
continue;
}
}
}
return $css;
}
/**
* Provide a deafult list of allowed properties by the filter.
*/
function ctools_css_filter_default_allowed_properties() {
return array(
'azimuth',
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'border',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-color',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-style',
'border-top',
'border-right',
'border-bottom',
'border-left',
'clear',
'color',
'cursor',
'direction',
'display',
'elevation',
'float',
'font',
'font-family',
'font-size',
'font-style',
'font-variant',
'font-weight',
'height',
'letter-spacing',
'line-height',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'overflow',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'pause',
'pause-after',
'pause-before',
'pitch',
'pitch-range',
'richness',
'speak',
'speak-header',
'speak-numeral',
'speak-punctuation',
'speech-rate',
'stress',
'text-align',
'text-decoration',
'text-indent',
'text-transform',
'unicode-bidi',
'vertical-align',
'voice-family',
'volume',
'white-space',
'width',
'fill',
'fill-opacity',
'fill-rule',
'stroke',
'stroke-width',
'stroke-linecap',
'stroke-linejoin',
'stroke-opacity',
);
}
/**
* Provide a default list of allowed values by the filter.
*/
function ctools_css_filter_default_allowed_values() {
return array(
'auto',
'aqua',
'black',
'block',
'blue',
'bold',
'both',
'bottom',
'brown',
'capitalize',
'center',
'collapse',
'dashed',
'dotted',
'fuchsia',
'gray',
'green',
'italic',
'inherit',
'left',
'lime',
'lowercase',
'maroon',
'medium',
'navy',
'normal',
'nowrap',
'olive',
'pointer',
'purple',
'red',
'right',
'solid',
'silver',
'teal',
'top',
'transparent',
'underline',
'uppercase',
'white',
'yellow',
);
}
/**
* Delegated implementation of hook_flush_caches()
*/
function ctools_css_flush_caches() {
// Remove all generated files.
// @see http://drupal.org/node/573292
// file_unmanaged_delete_recursive('public://render');
$filedir = file_default_scheme() . '://ctools/css';
if (drupal_realpath($filedir) && file_exists($filedir)) {
// We use the @ because it's possible that files created by the webserver
// cannot be deleted while using drush to clear the cache. We don't really
// care that much about that, to be honest, so we use the @ to suppress
// the error message.
@file_unmanaged_delete_recursive($filedir);
}
db_delete('ctools_css_cache')->execute();
}

View File

@ -0,0 +1,181 @@
<?php
/**
* @file
* Provide dependent checkboxes that can be easily used in forms.
*
* This system will ensure that form items are invisible if the dependency is
* not met. What this means is that you set the #dependency of an item to a
* list of form ids that must be set, and the list of values that qualify.
*
* For a simple use, setting an item to be dependent upon a select box, if
* any of the listed values are selected, the item will be visible. Otherwise,
* the item will be invisible.
*
* If dependent upon multiple items, use #dependency_count = X to set the
* number of items that must be set in order to make this item visible. This
* defaults to 1. If set to 2, then at least 2 form items in the list must
* have their items set for the item to become visible.
*
* When hiding checkboxes and radios you need to add their id in a div
* manually via #prefix and #suffix since they don't have their own id. You
* actually need to add TWO divs because it's the parent that gets hidden.
*
* Fieldsets can not be hidden by default. Adding '#input' => TRUE to the
* fieldset works around that.
*
* For radios, because they are selected a little bit differently, instead of
* using the CSS id, use: radio:NAME where NAME is the #name of the property.
* This can be quickly found by looking at the HTML of the generated form, but
* it is usually derived from the array which contains the item. For example,
* $form['menu']['type'] would have a name of menu[type]. This name is the same
* field that is used to determine where in $form_state['values'] you will find
* the value of the form.
*
* The item that is dependent on, should be set to #tree = TRUE.
*
* Usage:
*
* First, ensure this tool is loaded:
* @code { ctools_include('dependent'); }
*
* On any form item, add
* - @code '#dependency' => array('id-of-form-without-the-#' => array(list, of, values, that, make, this, gadget, visible)), @endcode
*
* A fuller example, that hides the menu title when no menu is selected:
* @code
*function ctools_dependent_example() {
* $form = array();
* $form['menu'] = array(
* '#type' => 'fieldset',
* '#title' => t('Menu settings'),
* '#tree' => TRUE,
* );
* $form['menu']['type'] = array(
* '#title' => t('Menu type'),
* '#type' => 'radios',
* '#options' => array(
* 'none' => t('No menu entry'),
* 'normal' => t('Normal menu entry'),
* 'tab' => t('Menu tab'),
* 'default tab' => t('Default menu tab'),
* ),
* '#default_value' => 'none',
* );
*
* $form['menu']['title'] = array(
* '#title' => t('Title'),
* '#type' => 'textfield',
* '#default_value' => '',
* '#description' => t('If set to normal or tab, enter the text to use for the menu item.'),
* '#dependency' => array('radio:menu[type]' => array('normal', 'tab', 'default tab')),
* );
*
* return system_settings_form($form);
*}
* @endcode
*
* An example for hiding checkboxes using #prefix and #suffix:
* @code
*function ctools_dependent_example_checkbox() {
* $form = array();
* $form['object'] = array(
* '#type' => 'fieldset',
* '#title' => t('Select object type'),
* '#tree' => TRUE,
* );
* $form['object']['type'] = array(
* '#title' => t('Object type'),
* '#type' => 'radios',
* '#options' => array(
* 'view' => t('View'),
* 'node' => t('Node'),
* 'field' => t('Field'),
* 'term' => t('Term'),
* ),
* '#default_value' => 'view',
* );
*
* $form['object']['elements'] = array(
* '#title' => t('Select the elements to load from the node.'),
* '#type' => 'checkboxes',
* '#prefix' => '<div id="edit-elements-wrapper"><div id="edit-elements">',
* '#suffix' => '</div></div>',
* '#dependency' => array('radio:menu[type]' => array('node')),
* '#options' => array(
* 'body' => t('Body'),
* 'fields' => t('Fields'),
* 'taxonomy' => t('Taxonomy'),
* ),
* '#default_value' => array('body', 'fields'),
* );
*
* return system_settings_form($form);
*}
* @endcode
*
* Deprecated:
*
* You no longer use ctools_dependent_process(), and it should be removed
* completely.
*
* If you have a form element which isn't listed in ctools_dependent_element_info_alter
* you have to add [#pre_render'][] => 'ctools_dependent_pre_render' to your form.
*/
/**
* Process callback to add dependency to form items.
*
*/
function ctools_dependent_process($element, &$form_state, &$form) {
return $element;
}
function ctools_dependent_pre_render($element) {
// Preprocess only items with #dependency set.
if (isset($element['#dependency'])) {
if (!isset($element['#dependency_count'])) {
$element['#dependency_count'] = 1;
}
if (!isset($element['#dependency_type'])) {
$element['#dependency_type'] = 'hide';
}
$js = array(
'values' => $element['#dependency'],
'num' => $element['#dependency_count'],
'type' => $element['#dependency_type'],
);
// Add a additional wrapper id around fieldsets, textareas to support depedency on it.
if (in_array($element['#type'], array('textarea', 'fieldset', 'text_format'))) {
$element['#theme_wrappers'][] = 'container';
$element['#attributes']['id'] = $element['#id'] . '-wrapper';
}
// Text formats need to unset the dependency on the textarea
// or it gets applied twice.
if ($element['#type'] == 'text_format') {
unset($element['value']['#dependency']);
}
$element['#attached']['js'][] = ctools_attach_js('dependent');
$options['CTools']['dependent'][$element['#id']] = $js;
$element['#attached']['js'][] = array('type' => 'setting', 'data' => $options);
}
return $element;
}
/**
* CTools alters the element_info to be able to add #process functions to
* every major form element to make it much more handy to use #dependency,
* because you don't have to add #process.
*/
function ctools_dependent_element_info_alter(&$type) {
$form_elements = array('checkbox', 'checkboxes', 'date', 'fieldset', 'item', 'machine_name', 'markup', 'radio', 'radios', 'select', 'textarea', 'textfield', 'text_format');
foreach ($form_elements as $element) {
$type[$element]['#pre_render'][] = 'ctools_dependent_pre_render';
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* @file
* Provide a javascript based dropbutton menu.
*
* The dropbutton menu will show up as a button with a clickable twisty pointer
* to the right. When clicked the button will expand, showing the list of links.
*
* The dropbutton will stay open until either the user has moved the mouse
* away from the box for > .5 seconds, or can be immediately closed by
* clicking the twisty again. The code is smart enough that if the mouse
* moves away and then back within the .5 second window, it will not
* re-close.
*
* Multiple dropbuttons can be placed per page.
*
* If only one link is passed to the theme function, the function will render
* a ctools-button with no twisty. The twisty is only rendered when 2 or more
* links are provided. The wrapping element will be classed with both
* ctools-button and ctools-dropbutton when a dropbutton is rendered.
*
* If the user does not have javascript enabled, the link will not appear,
* and instead by default the list of links will appear as a normal inline
* list.
*
* The menu is minimally styled by default, and to make it look different
* will require your own CSS. You can apply your own class to the
* dropbutton to help style it.
*
* The twisty is rendered as a border on a widthless and heightless element.
* There is no image for the twisty.
* The color of the twisty is the color of the link by default. To adjust the
* size of the twisty, adjust the border widths on .ctools-twisty. The adjust
* the color of the twisty, assign a new color to the .ctools-button class or
* assign a color to .ctools-twisty. You shouldn't need to adjust border-color.
*
* Use the theme() function to render dropbutton e.g.
* theme('links__ctools_dropbutton', array()) where array contains a renderable
* array of links.
*/
/**
* Delegated implementation of hook_theme()
*/
function ctools_dropbutton_theme(&$items) {
$items['links__ctools_dropbutton'] = array(
'variables' => array('title' => NULL, 'links' => NULL, 'image' => FALSE, 'class' => NULL),
'file' => 'includes/dropbutton.theme.inc',
);
}
/**
* Create a dropbutton menu.
*
* @param $title
* The text to place in the clickable area to activate the dropbutton. This
* text is indented to -9999px by default.
* @param $links
* A list of links to provide within the dropbutton, suitable for use
* in via Drupal's theme('links').
* @param $image
* If true, the dropbutton link is an image and will not get extra decorations
* that a text dropbutton link will.
* @param $class
* An optional class to add to the dropbutton's container div to allow you
* to style a single dropbutton however you like without interfering with
* other dropbuttons.
*/
function theme_links__ctools_dropbutton($vars) {
// Check to see if the number of links is greater than 1;
// otherwise just treat this like a button.
if (!empty($vars['links'])) {
$is_drop_button = (count($vars['links']) > 1);
// Add needed files
if ($is_drop_button) {
ctools_add_js('dropbutton');
ctools_add_css('dropbutton');
}
ctools_add_css('button');
// Provide a unique identifier for every button on the page.
static $id = 0;
$id++;
// Wrapping div
$class = 'ctools-no-js';
$class .= ($is_drop_button) ? ' ctools-dropbutton' : '';
$class .= ' ctools-button';
if (!empty($vars['class'])) {
$class .= ($vars['class']) ? (' ' . implode(' ', $vars['class'])) : '';
}
$output = '';
$output .= '<div class="' . $class . '" id="ctools-button-' . $id . '">';
// Add a twisty if this is a dropbutton
if ($is_drop_button) {
$vars['title'] = ($vars['title'] ? check_plain($vars['title']) : t('open'));
$output .= '<div class="ctools-link">';
if ($vars['image']) {
$output .= '<a href="#" class="ctools-twisty ctools-image">' . $vars['title'] . '</a>';
}
else {
$output .= '<a href="#" class="ctools-twisty ctools-text">' . $vars['title'] . '</a>';
}
$output .= '</div>'; // ctools-link
}
// The button content
$output .= '<div class="ctools-content">';
// Check for attributes. theme_links expects an array().
$vars['attributes'] = (!empty($vars['attributes'])) ? $vars['attributes'] : array();
// Remove the inline and links classes from links if they exist.
// These classes are added and styled by Drupal core and mess up the default
// styling of any link list.
if (!empty($vars['attributes']['class'])) {
$classes = $vars['attributes']['class'];
foreach ($classes as $key => $class) {
if ($class === 'inline' || $class === 'links') {
unset($vars['attributes']['class'][$key]);
}
}
}
// Call theme_links to render the list of links.
$output .= theme_links(array('links' => $vars['links'], 'attributes' => $vars['attributes'], 'heading' => ''));
$output .= '</div>'; // ctools-content
$output .= '</div>'; // ctools-dropbutton
return $output;
}
else {
return '';
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* @file
* Provide a javascript based dropdown menu.
*
* The dropdown menu will show up as a clickable link; when clicked,
* a small menu will slide down beneath it, showing the list of links.
*
* The dropdown will stay open until either the user has moved the mouse
* away from the box for > .5 seconds, or can be immediately closed by
* clicking the link again. The code is smart enough that if the mouse
* moves away and then back within the .5 second window, it will not
* re-close.
*
* Multiple dropdowns can be placed per page.
*
* If the user does not have javascript enabled, the link will not appear,
* and instead by default the list of links will appear as a normal inline
* list.
*
* The menu is heavily styled by default, and to make it look different
* will require a little bit of CSS. You can apply your own class to the
* dropdown to help ensure that your CSS can override the dropdown's CSS.
*
* In particular, the text, link, background and border colors may need to
* be changed. Please see dropdown.css for information about the existing
* styling.
*/
/**
* Delegated implementation of hook_theme()
*/
function ctools_dropdown_theme(&$items) {
$items['ctools_dropdown'] = array(
'variables' => array('title' => NULL, 'links' => NULL, 'image' => FALSE, 'class' => ''),
'file' => 'includes/dropdown.theme.inc',
);
}
/**
* Create a dropdown menu.
*
* @param $title
* The text to place in the clickable area to activate the dropdown.
* @param $links
* A list of links to provide within the dropdown, suitable for use
* in via Drupal's theme('links').
* @param $image
* If true, the dropdown link is an image and will not get extra decorations
* that a text dropdown link will.
* @param $class
* An optional class to add to the dropdown's container div to allow you
* to style a single dropdown however you like without interfering with
* other dropdowns.
*/
function theme_ctools_dropdown($vars) {
// Provide a unique identifier for every dropdown on the page.
static $id = 0;
$id++;
$class = 'ctools-dropdown-no-js ctools-dropdown' . ($vars['class'] ? (' ' . $vars['class']) : '');
ctools_add_js('dropdown');
ctools_add_css('dropdown');
$output = '';
$output .= '<div class="' . $class . '" id="ctools-dropdown-' . $id . '">';
$output .= '<div class="ctools-dropdown-link-wrapper">';
if ($vars['image']) {
$output .= '<a href="#" class="ctools-dropdown-link ctools-dropdown-image-link">' . $vars['title'] . '</a>';
}
else {
$output .= '<a href="#" class="ctools-dropdown-link ctools-dropdown-text-link">' . check_plain($vars['title']) . '</a>';
}
$output .= '</div>'; // wrapper
$output .= '<div class="ctools-dropdown-container-wrapper">';
$output .= '<div class="ctools-dropdown-container">';
$output .= theme_links(array('links' => $vars['links'], 'attributes' => array(), 'heading' => ''));
$output .= '</div>'; // container
$output .= '</div>'; // container wrapper
$output .= '</div>'; // dropdown
return $output;
}

View File

@ -0,0 +1,475 @@
<?php
/**
* @file
* Provide a tool for creating UIs for exportable objects.
*
* See Advanced Help for documentation.
*/
/**
* Process an export-ui plugin to provide it with defaults.
*/
function ctools_export_ui_process(&$plugin, $info) {
ctools_include('export');
$plugin += array(
'has menu' => TRUE,
'title' => $plugin['name'],
'export' => array(),
'allowed operations' => array(),
'menu' => array(),
'redirect' => array(),
'form' => array(),
'strings' => array(),
'list' => NULL,
'access' => 'administer site configuration',
);
// Provide CRUD access defaults based on the base 'access' setting:
$plugin += array(
'create access' => $plugin['access'],
'delete access' => $plugin['access'],
);
if (empty($plugin['has menu'])) {
return;
}
// The following keys are required and the plugin cannot be processed
// without them.
$keys = array(
'title singular',
'title plural',
'title singular proper',
'title plural proper',
'schema',
);
foreach ($keys as $key) {
if (empty($plugin[$key])) {
drupal_set_message(t('The plugin definition of @plugin is missing the %key key.', array('%key' => $key, '@plugin' => $plugin['name'])), 'error');
}
}
// If we're on the modules page and building a menu, there is a design flaw
// in Drupal core that causes modules to be installed but the schema does
// not become available until AFTER menu rebuild. This helps smooth that
// out. This is a HACK but it should work:
$schema = ctools_export_get_schema($plugin['schema']);
if (empty($schema)) {
// If we're updating the schema may not have been read yet, so don't report this error in that case.
if (!defined('MAINTENANCE_MODE')) {
drupal_set_message(t('The plugin definition of @plugin cannot locate schema %schema.', array('%schema' => $plugin['schema'], '@plugin' => $plugin['name'])), 'error');
}
return;
}
if (empty($schema['export'])) {
drupal_set_message(t('The plugin definition of @plugin uses %schema, but it has no export section.', array('%schema' => $plugin['schema'], '@plugin' => $plugin['name'])), 'error');
return;
}
$plugin['export'] += $schema['export'];
$plugin['export'] += array(
// Add the identifier key from the schema so we don't have to call
// ctools_export_get_schema() just for that.
'key' => $schema['export']['key'],
);
// Add some default fields that appear often in exports
// If these use different keys they can easily be specified in the
// $plugin.
if (empty($plugin['export']['admin_title']) && !empty($schema['fields']['admin_title'])) {
$plugin['export']['admin_title'] = 'admin_title';
}
if (empty($plugin['export']['admin_description']) && !empty($schema['fields']['admin_description'])) {
$plugin['export']['admin_description'] = 'admin_description';
}
// Define allowed operations, and the name of the operations.
$plugin['allowed operations'] += array(
'edit' => array('title' => t('Edit')),
'enable' => array('title' => t('Enable'), 'ajax' => TRUE, 'token' => TRUE),
'disable' => array('title' => t('Disable'), 'ajax' => TRUE, 'token' => TRUE),
'revert' => array('title' => t('Revert')),
'delete' => array('title' => t('Delete')),
'clone' => array('title' => t('Clone')),
'import' => array('title' => t('Import')),
'export' => array('title' => t('Export')),
);
$plugin['menu'] += array(
'menu item' => str_replace(' ', '-', $plugin['name']),
'menu prefix' => 'admin/structure',
'menu title' => $plugin['title'],
'menu description' => '',
);
$base_path = ctools_export_ui_plugin_base_path($plugin);
$prefix_count = count(explode('/', $plugin['menu']['menu prefix']));
$plugin['menu'] += array(
// Default menu items that should be declared.
'items' => array(),
);
$plugin['menu']['items'] += array(
'list callback' => array(),
'list' => array(),
'add' => array(),
'edit callback' => array(),
'edit' => array(),
);
$plugin['menu']['items']['list callback'] += array(
'path' => '',
// Menu items are translated by the menu system.
// TODO: We need more flexibility in title. The title of the admin page
// is not necessarily the title of the object, plus we need
// plural, singular, proper, not proper, etc.
'title' => $plugin['menu']['menu title'],
'description' => $plugin['menu']['menu description'],
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'list'),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'list'),
'type' => MENU_NORMAL_ITEM,
);
$plugin['menu']['items']['list'] += array(
'path' => 'list',
'title' => 'List',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'list'),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'list'),
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$plugin['menu']['items']['add'] += array(
'path' => 'add',
'title' => 'Add',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'add'),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'add'),
'type' => MENU_LOCAL_ACTION,
);
$plugin['menu']['items']['edit callback'] += array(
'path' => 'list/%ctools_export_ui',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
$plugin['menu']['items']['edit'] += array(
'path' => 'list/%ctools_export_ui/edit',
'title' => 'Edit',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
if ($plugin['allowed operations']['import']) {
$plugin['menu']['items'] += array('import' => array());
$plugin['menu']['items']['import'] += array(
'path' => 'import',
'title' => 'Import',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'import'),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'import'),
'type' => MENU_LOCAL_ACTION,
);
}
if ($plugin['allowed operations']['export']) {
$plugin['menu']['items'] += array('export' => array());
$plugin['menu']['items']['export'] += array(
'path' => 'list/%ctools_export_ui/export',
'title' => 'Export',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'export', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'export', $prefix_count + 2),
'type' => MENU_LOCAL_TASK,
);
}
if ($plugin['allowed operations']['revert']) {
$plugin['menu']['items'] += array('revert' => array());
$plugin['menu']['items']['revert'] += array(
'path' => 'list/%ctools_export_ui/revert',
'title' => 'Revert',
'page callback' => 'ctools_export_ui_switcher_page',
// Note: Yes, 'delete' op is correct.
'page arguments' => array($plugin['name'], 'delete', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'revert', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
}
if ($plugin['allowed operations']['delete']) {
$plugin['menu']['items'] += array('delete' => array());
$plugin['menu']['items']['delete'] += array(
'path' => 'list/%ctools_export_ui/delete',
'title' => 'Delete',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'delete', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'delete', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
}
if ($plugin['allowed operations']['clone']) {
$plugin['menu']['items'] += array('clone' => array());
$plugin['menu']['items']['clone'] += array(
'path' => 'list/%ctools_export_ui/clone',
'title' => 'Clone',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'clone', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'clone', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
}
if ($plugin['allowed operations']['enable']) {
$plugin['menu']['items'] += array('enable' => array());
$plugin['menu']['items']['enable'] += array(
'path' => 'list/%ctools_export_ui/enable',
'title' => 'Enable',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'enable', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'enable', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
}
if ($plugin['allowed operations']['disable']) {
$plugin['menu']['items'] += array('disable' => array());
$plugin['menu']['items']['disable'] += array(
'path' => 'list/%ctools_export_ui/disable',
'title' => 'Disable',
'page callback' => 'ctools_export_ui_switcher_page',
'page arguments' => array($plugin['name'], 'disable', $prefix_count + 2),
'load arguments' => array($plugin['name']),
'access callback' => 'ctools_export_ui_task_access',
'access arguments' => array($plugin['name'], 'disable', $prefix_count + 2),
'type' => MENU_CALLBACK,
);
}
// Define some redirects that should happen after edit/add/clone/delete operations.
$plugin['redirect'] += array(
'add' => $base_path,
'clone' => $base_path,
'edit' => $base_path,
'delete' => $base_path,
'revert' => $base_path,
'import' => $base_path,
);
// Define form elements.
$plugin['form'] += array(
'settings' => function_exists($plugin['name'] . '_form') ? $plugin['name'] . '_form' : '',
'validate' => function_exists($plugin['name'] . '_form_validate') ? $plugin['name'] . '_form_validate' : '',
'submit' => function_exists($plugin['name'] . '_form_submit') ? $plugin['name'] . '_form_submit' : '',
);
// Define strings.
// For all strings, %title may be filled in at a later time via str_replace
// since we do not know the title now.
$plugin['strings'] += array(
'title' => array(),
'confirmation' => array(),
'help' => array(),
'message' => array(),
);
// Strings used in drupal_set_title().
$plugin['strings']['title'] += array(
'add' => t('Add a new @plugin', array('@plugin' => $plugin['title singular'])),
// The "%title" will be replaced in ctools_export_ui_form(), as in this
// stage we dont have the specific exportable object.
'edit' => t('Edit @plugin %title', array('@plugin' => $plugin['title singular'])),
'clone' => t('Clone @plugin %title', array('@plugin' => $plugin['title singular'])),
'import' => t('Import @plugin', array('@plugin' => $plugin['title singular'])),
'export' => t('Export @plugin %title', array('@plugin' => $plugin['title singular'])),
);
// Strings used in confirmation pages.
$plugin['strings']['confirmation'] += array(
'revert' => array(),
'delete' => array(),
'add' => array(),
'edit' => array(),
);
$plugin['strings']['confirmation']['revert'] += array(
'question' => t('Are you sure you want to revert %title?'),
'information' => t('This action will permanently remove any customizations made to this item.'),
'success' => t('The item has been reverted.'),
);
$plugin['strings']['confirmation']['delete'] += array(
'question' => t('Are you sure you want to delete %title?'),
'information' => t('This action will permanently remove this item from your database..'),
'success' => t('The item has been deleted.'),
);
$plugin['strings']['confirmation']['add'] += array(
'success' => t('%title has been created.'),
'fail' => t('%title could not be created.'),
);
$plugin['strings']['confirmation']['edit'] += array(
'success' => t('%title has been updated.'),
'fail' => t('%title could not be updated.'),
);
// Strings used in $forms.
$plugin['strings']['help'] += array(
'import' => t('You can import an exported definition by pasting the exported object code into the field below.'),
);
// Strings used in drupal_set_message().
$plugin['strings']['message'] += array(
'enable' => t('@plugin %title was enabled.', array('@plugin' => $plugin['title singular proper'])),
'disable' => t('@plugin %title was disabled.', array('@plugin' => $plugin['title singular proper'])),
'no items' => t('There are no @titles to display.', array('@titles' => $plugin['title plural'])),
);
}
/**
* Get the class to handle creating a list of exportable items.
*
* If a plugin does not define a lister class at all, then the default
* lister class will be used.
*
* @return
* Either the lister class or FALSE if one could not be had.
*/
function ctools_export_ui_get_handler($plugin) {
$cache = &drupal_static(__FUNCTION__, array());
if (empty($cache[$plugin['name']])) {
// If a list class is not specified by the plugin, fall back to the
// default ctools_export_ui plugin instead.
if (empty($plugin['handler'])) {
$default = ctools_get_export_ui('ctools_export_ui');
$class = ctools_plugin_get_class($default, 'handler');
}
else {
$class = ctools_plugin_get_class($plugin, 'handler');
}
if ($class) {
$cache[$plugin['name']] = new $class();
$cache[$plugin['name']]->init($plugin);
}
}
return !empty($cache[$plugin['name']]) ? $cache[$plugin['name']] : FALSE;
}
/**
* Get the base path from a plugin.
*
* @param $plugin
* The plugin.
*
* @return
* The menu path to the plugin's list.
*/
function ctools_export_ui_plugin_base_path($plugin) {
return $plugin['menu']['menu prefix'] . '/' . $plugin['menu']['menu item'];
}
/**
* Get the path to a specific menu item from a plugin.
*
* @param $plugin
* The plugin name.
* @param $item_id
* The id in the menu items from the plugin.
* @param $export_key
* The export key of the item being edited, if it exists.
* @return
* The menu path to the plugin's list.
*/
function ctools_export_ui_plugin_menu_path($plugin, $item_id, $export_key = NULL) {
$path = $plugin['menu']['items'][$item_id]['path'];
if ($export_key) {
$path = str_replace('%ctools_export_ui', $export_key, $path);
}
return ctools_export_ui_plugin_base_path($plugin) . '/' . $path;
}
/**
* Helper function to include CTools plugins and get an export-ui exportable.
*
* @param $plugin_name
* The plugin that should be laoded.
*/
function ctools_get_export_ui($plugin_name) {
ctools_include('plugins');
return ctools_get_plugins('ctools', 'export_ui', $plugin_name);
}
/**
* Helper function to include CTools plugins and get all export-ui exportables.
*/
function ctools_get_export_uis() {
ctools_include('plugins');
return ctools_get_plugins('ctools', 'export_ui');
}
/**
* Main page callback to manipulate exportables.
*
* This simply loads the object defined in the plugin and hands it off to
* a method based upon the name of the operation in use. This can easily
* be used to add more ops.
*/
function ctools_export_ui_switcher_page($plugin_name, $op) {
$args = func_get_args();
$js = !empty($_REQUEST['js']);
// Load the $plugin information
$plugin = ctools_get_export_ui($plugin_name);
$handler = ctools_export_ui_get_handler($plugin);
if ($handler) {
$method = $op . '_page';
if (method_exists($handler, $method)) {
// replace the first two arguments:
$args[0] = $js;
$args[1] = $_POST;
return call_user_func_array(array($handler, $method), $args);
}
}
else {
return t('Configuration error. No handler found.');
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* Delegated implementation of hook_menu().
*/
function ctools_export_ui_menu(&$items) {
ctools_include('export-ui');
// If a menu rebuild is triggered because of module enable/disable,
// this might be out of date. Reset the cache.
ctools_include('plugins');
ctools_get_plugins_reset();
foreach (ctools_get_export_uis() as $plugin) {
// We also need to make sure that the module hasn't been disabled. During
// the disable process, the module's plugins still still appear.
if ($plugin['has menu'] && module_exists($plugin['module'])) {
$handler = ctools_export_ui_get_handler($plugin);
if ($handler) {
$handler->hook_menu($items);
}
}
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @file
* Contains plugin type registration information for the context tool.
*
* Don't actually need to declare anything for these plugin types right now,
* apart from the fact that they exist. So, an empty array.
*/
function ctools_export_ui_plugin_type(&$items) {
$items['export_ui'] = array(
'process' => array(
'function' => 'ctools_export_ui_process',
'file' => 'export-ui.inc',
'path' => drupal_get_path('module', 'ctools') . '/includes',
),
'classes' => array('handler'),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
<?php
/**
* @file
* Extend core fields with some helper functions to reduce code complexity within views and ctools plugins.
*/
/**
* Fake an instance of a field.
*
* @param $field_name
* The unique name for this field no matter what entity/bundle it may be used on.
* @param $view_mode
* We're building a new view mode for this function. Defaults to ctools, but we expect developers to actually name this something meaningful.
* @param $formatter
* The formatter key selected from the options provided by field_ui_formatter_options().
* @param $formatter_settings
* An array of key value pairs. These will be used as #default_value for the form elements generated by a call to hook_field_formatter_settings_form() for this field type.
* Typically we'll pass an empty array to begin with and then pass this information back to ourselves on form submit so that we can set the values for later edit sessions.
*/
function ctools_fields_fake_field_instance($field_name, $view_mode = 'ctools', $formatter, $formatter_settings) {
$field = field_read_field($field_name);
$field_type = field_info_field_types($field['type']);
return array(
// Build a fake entity type and bundle.
'field_name' => $field_name,
'entity_type' => 'ctools',
'bundle' => 'ctools',
// Use the default field settings for settings and widget.
'settings' => field_info_instance_settings($field['type']),
'widget' => array(
'type' => $field_type['default_widget'],
'settings' => array(),
),
// Build a dummy display mode.
'display' => array(
$view_mode => array(
'type' => $formatter,
'settings' => $formatter_settings,
),
),
// Set the other fields to their default values.
// @see _field_write_instance().
'required' => FALSE,
'label' => $field_name,
'description' => '',
'deleted' => 0,
);
}
/**
* Helper function for calling hook_field_formatter_settings_form() without needing to load an instance of the field.
*
* @param $field
* A fully loaded field.
* @param $formatter_type
* The formatter key selected from the options provided by field_ui_formatter_options().
* @param $form
* The full form from the function that's calling this function.
* @param $form_state
* The full form_state from the function that's calling this function.
* @param $view_mode
* We're passing a view mode from this function to the fake instance we're creating. Defaults to ctools, but we expect developers to actually name this something meaningful.
*/
function ctools_fields_get_field_formatter_settings_form($field, $formatter_type, &$form, $form_state, $view_mode = 'ctools') {
$conf = $form_state['conf'];
$formatter = field_info_formatter_types($formatter_type);
if (isset($formatter['settings'])) {
$conf['formatter_settings'] += $formatter['settings'];
}
$function = $formatter['module'] . '_field_formatter_settings_form';
if (function_exists($function)) {
$instance = ctools_fields_fake_field_instance($field['field_name'], $view_mode, $formatter_type, $conf['formatter_settings']);
$settings_form = $function($field, $instance, $view_mode, $form, $form_state);
if ($settings_form) {
$form['ctools_field_list']['#value'][] = $field;
$form += $settings_form;
}
}
if (isset($field['cardinality']) && $field['cardinality'] != 1) {
list($prefix, $suffix) = explode('@count', t('Skip the first @count item(s)'));
$form['delta_offset'] = array(
'#type' => 'textfield',
'#size' => 5,
'#field_prefix' => $prefix,
'#field_suffix' => $suffix,
'#default_value' => isset($conf['delta_offset']) ? $conf['delta_offset'] : 0,
);
list($prefix, $suffix) = explode('@count', t('Then display at most @count item(s)'));
$form['delta_limit'] = array(
'#type' => 'textfield',
'#size' => 5,
'#field_prefix' => $prefix,
'#field_suffix' => $suffix,
'#description' => t('Enter 0 to display all items.'),
'#default_value' => isset($conf['delta_limit']) ? $conf['delta_limit'] : 0,
);
$form['delta_reversed'] = array(
'#title' => t('Display in reverse order'),
'#type' => 'checkbox',
'#default_value' => !empty($conf['delta_reversed']),
'#description' => t('(start from last values)'),
);
}
}
/**
* Helper function for generating all the formatter information associated with
* any fields.
* Especially useful for determining the fields that will be added to form that
* executes hook_field_formatter_settings_form().
*
* @param $fields
* An array of fully loaded fields.
*/
function ctools_fields_get_field_formatter_info($fields) {
$info = array();
$field_info = module_invoke_all('field_formatter_info');
foreach ($fields as $field) {
foreach ($field_info as $format_name => $formatter_info) {
if (in_array($field['type'], $formatter_info['field types'])) {
$info += array($format_name => $formatter_info);
}
}
}
drupal_alter('field_formatter_info', $info);
return $info;
}
/**
* Returns the label of a certain field.
*
* Cribbed from Views.
*/
function ctools_field_label($field_name) {
$label_counter = array();
// Count the amount of instances per label per field.
$instances = field_info_instances();
foreach ($instances as $entity_type) {
foreach ($entity_type as $bundle) {
if (isset($bundle[$field_name])) {
$label_counter[$bundle[$field_name]['label']] = isset($label_counter[$bundle[$field_name]['label']]) ? ++$label_counter[$bundle[$field_name]['label']] : 1;
}
}
}
if (empty($label_counter)) {
return $field_name;
}
// Sort the field lables by it most used label and return the most used one.
arsort($label_counter);
$label_counter = array_keys($label_counter);
return $label_counter[0];
}
/**
* Replacement for core _field_invoke() to invoke on a single field.
*
* Core only allows invoking field hooks via a private function for all fields
* on an entire entity. However, we very often need to invoke our hooks on
* a single field as we take things apart and only use little bits.
*
* @param $field_name
* Either a field instance object or the name of the field.
* If the 'field' key is populated it will be used as the field
* settings.
* @param $op
* Possible operations include:
* - form
* - validate
* - presave
* - insert
* - update
* - delete
* - delete revision
* - view
* - prepare translation
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entity
* The fully formed $entity_type entity.
* @param $a
* - The $form in the 'form' operation.
* - The value of $view_mode in the 'view' operation.
* - Otherwise NULL.
* @param $b
* - The $form_state in the 'submit' operation.
* - Otherwise NULL.
* @param $options
* An associative array of additional options, with the following keys:
* - 'field_name': The name of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's bundle. NOTE: This option is not compatible with
* the 'deleted' option; the 'field_id' option should be used
* instead.
* - 'field_id': The id of the field whose operation should be
* invoked. By default, the operation is invoked on all the fields
* in the entity's' bundles.
* - 'default': A boolean value, specifying which implementation of
* the operation should be invoked.
* - if FALSE (default), the field types implementation of the operation
* will be invoked (hook_field_[op])
* - If TRUE, the default field implementation of the field operation
* will be invoked (field_default_[op])
* Internal use only. Do not explicitely set to TRUE, but use
* _field_invoke_default() instead.
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* - 'language': A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @see _field_invoke()
*/
function ctools_field_invoke_field($field_name, $op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
if (is_array($field_name)) {
$instance = $field_name;
$field = empty($field_name['field']) ? field_info_field($instance['field_name']) : $field_name['field'];
$field_name = $instance['field_name'];
}
else {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instance = field_info_instance($entity_type, $field_name, $bundle);
}
if (empty($instance)) {
return;
}
// Merge default options.
$default_options = array(
'default' => FALSE,
'deleted' => FALSE,
'language' => NULL,
);
$options += $default_options;
$return = array();
// Everything from here is unmodified code from _field_invoke() formerly
// inside a foreach loop over the instances.
$function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
if (function_exists($function)) {
// Determine the list of languages to iterate on.
$available_languages = field_available_languages($entity_type, $field);
$languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
foreach ($languages as $langcode) {
$items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
$result = $function($entity_type, $entity, $field, $instance, $langcode, $items, $a, $b);
if (isset($result)) {
// For hooks with array results, we merge results together.
// For hooks with scalar results, we collect results in an array.
if (is_array($result)) {
$return = array_merge($return, $result);
}
else {
$return[] = $result;
}
}
// Populate $items back in the field values, but avoid replacing missing
// fields with an empty array (those are not equivalent on update).
if ($items !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $items;
}
}
}
return $return;
}
/**
* Replacement for core _field_invoke_default() to invoke on a single field.
*
* @see ctools_field_invoke_field()
* @see _field_invoke_default()
*/
function ctools_field_invoke_field_default($field_name, $op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
$options['default'] = TRUE;
return ctools_field_invoke_field($field_name, $op, $entity_type, $entity, $a, $b, $options);
}
/**
* Returns a list of field definitions of a specified type.
*
* @param string $field_type
* A field type name; e.g. 'text' or 'date'.
*
* @return array
* An array of field definitions of the specified type, keyed by field name.
*/
function ctools_fields_get_fields_by_type($field_type) {
$fields = array();
foreach (field_info_fields() as $field_name => $field_info) {
if ($field_info['type'] == $field_type) {
$fields[$field_name] = $field_info;
}
}
return $fields;
}
/**
* Derive the foreign keys that a field provides.
*
* @param $field_name
* The name of the field.
*
* @return
* An array of foreign keys according to Schema API.
*/
function ctools_field_foreign_keys($field_name) {
$foreign_keys = &drupal_static(__FUNCTION__, array());
if (!isset($foreign_keys[$field_name])) {
$foreign_keys[$field_name] = array();
$field = field_info_field($field_name);
if (!empty($field['foreign keys'])) {
$foreign_keys[$field_name] = $field['foreign keys'];
}
else {
// try to fetch foreign keys from schema, as not everything
// stores foreign keys properly in the field info.
$module = $field['module'];
module_load_install($module);
$schema = module_invoke($module, 'field_schema', $field);
if (!empty($schema['foreign keys'])) {
$foreign_keys[$field_name] = $schema['foreign keys'];
}
}
}
return $foreign_keys[$field_name];
}

View File

@ -0,0 +1,150 @@
<?php
/**
* @file
* Provides a simple "jump menu".
*
* A jump menu is a select box and an optional 'go' button which can be removed
* if javascript is in use. Each item is keyed to the href that the button
* should go to. With javascript, the page is immediately redirected. Without
* javascript, the form is submitted and a drupal_goto() is given.
*
*/
/**
* Generate a jump menu form.
*
* This can either be used with drupal_get_form() or directly added to a
* form. The button provides its own submit handler so by default, other
* submit handlers will not be called.
*
* One note: Do not use #tree = TRUE or it will be unable to find the
* proper value.
*
* @code
* ctools_include('jump-menu');
* $output = drupal_get_form('ctools_jump_menu', $targets, $options);
* @endcode
*
* @param $select
* An array suitable for use as the #options. The keys will be the direct
* URLs that will be jumped to, so you absolutely must encode these using
* url() in order for them to work reliably.
*
* @param $options
* $options may be an array with the following options:
* - 'title': The text to display for the #title attribute.
* - 'description': The text to display for the #description attribute.
* - 'default_value': The text to display for the #default_value attribute.
* - 'hide': If TRUE the go button will be set to hide via javascript and
* will submit on change.
* - 'button': The text to display on the button.
* - 'image': If set, an image button will be used instead, and the image
* set to this.
* - 'inline': If set to TRUE (default) the display will be forced inline.
*/
function ctools_jump_menu($form, &$form_state, $select, $options = array()) {
$options += array(
'button' => t('Go'),
'choose' => t('- Choose -'),
'inline' => TRUE,
'hide' => TRUE,
);
ctools_add_js('jump-menu');
if (!empty($options['choose'])) {
$select = array('' => $options['choose']) + $select;
}
$form['jump'] = array(
'#type' => 'select',
'#options' => $select,
'#attributes' => array(
'class' => array('ctools-jump-menu-select'),
),
);
if (!empty($options['title'])) {
$form['jump']['#title'] = $options['title'];
}
if (!empty($options['description'])) {
$form['jump']['#description'] = $options['description'];
}
if (!empty($options['default_value'])) {
$form['jump']['#default_value'] = $options['default_value'];
}
if (isset($options['image'])) {
$form['go'] = array(
'#type' => 'image_button',
'#src' => $options['image'],
'#submit' => array('ctools_jump_menu_submit'),
'#attributes' => array(
'class' => array('ctools-jump-menu-button'),
),
);
}
else {
$form['go'] = array(
'#type' => 'submit',
'#value' => $options['button'],
'#submit' => array('ctools_jump_menu_submit'),
'#attributes' => array(
'class' => array('ctools-jump-menu-button'),
),
);
}
if ($options['inline']) {
$form['jump']['#prefix'] = '<div class="container-inline">';
$form['go']['#suffix'] = '</div>';
}
if ($options['hide']) {
$form['jump']['#attributes']['class'][] = 'ctools-jump-menu-change';
$form['go']['#attributes']['class'][] = 'ctools-jump-menu-hide';
}
return $form;
}
/**
* Submit handler for the jump menu.
*
* This is normally only invoked upon submit without javascript enabled.
*/
function ctools_jump_menu_submit($form, &$form_state) {
if ($form_state['values']['jump'] === '') {
// We have nothing to do when the user has not selected any value.
return;
}
// If the path we are redirecting to contains the string :: then treat the
// the string after the double colon as the path to redirect to.
// This allows duplicate paths to be used in jump menus for multiple options.
$redirect_array = explode("::", $form_state['values']['jump']);
if(isset($redirect_array[1]) && !empty($redirect_array[1])){
$redirect = $redirect_array[1];
}
else {
$redirect = $form_state['values']['jump'];
}
// If the path we are redirecting to starts with the base path (for example,
// "/somepath/node/1"), we need to strip the base path off before passing it
// to $form_state['redirect'].
$base_path = base_path();
if (strpos($redirect, $base_path) === 0) {
$redirect = substr($redirect, strlen($base_path));
}
// Parse the URL so that query strings and fragments are preserved in the
// redirect.
$redirect = drupal_parse_url($redirect);
$redirect['path'] = urldecode($redirect['path']);
$form_state['redirect'] = array($redirect['path'], $redirect);
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Returns array of language names.
*
* This is a one to one copy of locale_language_list because we can't rely on enabled locale module.
*
* @param $field
* 'name' => names in current language, localized
* 'native' => native names
* @param $all
* Boolean to return all languages or only enabled ones
*
* @see locale_language_list
*/
function ctools_language_list($field = 'name', $all = FALSE) {
if ($all) {
$languages = language_list();
}
else {
$languages = language_list('enabled');
$languages = $languages[1];
}
$list = array();
foreach ($languages as $language) {
$list[$language->language] = ($field == 'name') ? t($language->name) : $language->$field;
}
return $list;
}
/**
* Returns an array of language names similar to ctools_language_list() except
* that additional choices have been added for ease of use.
*/
function ctools_language_list_all() {
$languages = array(
'***CURRENT_LANGUAGE***' => t("Current user's language"),
'***DEFAULT_LANGUAGE***' => t("Default site language"),
LANGUAGE_NONE => t('Language neutral'),
);
$languages = array_merge($languages, ctools_language_list());
return $languages;
}

View File

@ -0,0 +1,388 @@
<?php
/*
================================================================================
ctools_math_expr - PHP Class to safely evaluate math expressions
Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
================================================================================
NAME
ctools_math_expr - safely evaluate math expressions
SYNOPSIS
include('ctools_math_expr.class.php');
$m = new ctools_math_expr;
// basic evaluation:
$result = $m->evaluate('2+2');
// supports: order of operation; parentheses; negation; built-in functions
$result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
// create your own variables
$m->evaluate('a = e^(ln(pi))');
// or functions
$m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
// and then use them
$result = $m->evaluate('3*f(42,a)');
DESCRIPTION
Use the ctools_math_expr class when you want to evaluate mathematical expressions
from untrusted sources. You can define your own variables and functions,
which are stored in the object. Try it, it's fun!
METHODS
$m->evalute($expr)
Evaluates the expression and returns the result. If an error occurs,
prints a warning and returns false. If $expr is a function assignment,
returns true on success.
$m->e($expr)
A synonym for $m->evaluate().
$m->vars()
Returns an associative array of all user-defined variables and values.
$m->funcs()
Returns an array of all user-defined functions.
PARAMETERS
$m->suppress_errors
Set to true to turn off warnings when evaluating expressions
$m->last_error
If the last evaluation failed, contains a string describing the error.
(Useful when suppress_errors is on).
AUTHOR INFORMATION
Copyright 2005, Miles Kaufmann.
LICENSE
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1 Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class ctools_math_expr {
var $suppress_errors = false;
var $last_error = null;
var $v = array('e'=>2.71,'pi'=>3.14); // variables (and constants)
var $f = array(); // user-defined functions
var $vb = array('e', 'pi'); // constants
var $fb = array( // built-in functions
'sin','sinh','arcsin','asin','arcsinh','asinh',
'cos','cosh','arccos','acos','arccosh','acosh',
'tan','tanh','arctan','atan','arctanh','atanh',
'pow', 'exp',
'sqrt','abs','ln','log',
'time', 'ceil', 'floor', 'min', 'max', 'round');
function ctools_math_expr() {
// make the variables a little more accurate
$this->v['pi'] = pi();
$this->v['e'] = exp(1);
drupal_alter('ctools_math_expression_functions', $this->fb);
}
function e($expr) {
return $this->evaluate($expr);
}
function evaluate($expr) {
$this->last_error = null;
$expr = trim($expr);
if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
//===============
// is it a variable assignment?
if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
return $this->trigger("cannot assign to constant '$matches[1]'");
}
if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
$this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
return $this->v[$matches[1]]; // and return the resulting value
//===============
// is it a function assignment?
} elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
$fnn = $matches[1]; // get the function name
if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
return $this->trigger("cannot redefine built-in function '$matches[1]()'");
}
$args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
$token = $stack[$i];
if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
if (array_key_exists($token, $this->v)) {
$stack[$i] = $this->v[$token];
} else {
return $this->trigger("undefined variable '$token' in function definition");
}
}
}
$this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
return true;
//===============
} else {
return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
}
}
function vars() {
$output = $this->v;
unset($output['pi']);
unset($output['e']);
return $output;
}
function funcs() {
$output = array();
foreach ($this->f as $fnn=>$dat)
$output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
return $output;
}
//===================== HERE BE INTERNAL METHODS ====================\\
// Convert infix to postfix notation
function nfx($expr) {
$index = 0;
$stack = new ctools_math_expr_stack;
$output = array(); // postfix form of expression, to be passed to pfx()
$expr = trim(strtolower($expr));
$ops = array('+', '-', '*', '/', '^', '_');
$ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
$ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
$expecting_op = false; // we use this in syntax-checking the expression
// and determining when a - is a negation
if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
return $this->trigger("illegal character '{$matches[0]}'");
}
while(1) { // 1 Infinite Loop ;)
$op = substr($expr, $index, 1); // get the first character at the current index
// find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
$ex = preg_match('/^([a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
//===============
if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
$stack->push('_'); // put a negation on the stack
$index++;
} elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
return $this->trigger("illegal character '_'"); // but not in the input expression
//===============
} elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
$op = '*'; $index--; // it's an implicit multiplication
}
// heart of the algorithm:
while($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
$output[] = $stack->pop(); // pop stuff off the stack into the output
}
// many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
$stack->push($op); // finally put OUR operator onto the stack
$index++;
$expecting_op = false;
//===============
} elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
if (is_null($o2)) return $this->trigger("unexpected ')'");
else $output[] = $o2;
}
if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function?
$fnn = $matches[1]; // get the function name
$arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
$output[] = $stack->pop(); // pop the function and push onto the output
if (in_array($fnn, $this->fb)) { // check the argument count
if($arg_count > 1)
return $this->trigger("too many arguments ($arg_count given, 1 expected)");
} elseif (array_key_exists($fnn, $this->f)) {
if ($arg_count != count($this->f[$fnn]['args']))
return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)");
} else { // did we somehow push a non-function on the stack? this should never happen
return $this->trigger("internal error");
}
}
$index++;
//===============
} elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
while (($o2 = $stack->pop()) != '(') {
if (is_null($o2)) return $this->trigger("unexpected ','"); // oops, never had a (
else $output[] = $o2; // pop the argument expression stuff and push onto the output
}
// make sure there was a function
if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches))
return $this->trigger("unexpected ','");
$stack->push($stack->pop()+1); // increment the argument count
$stack->push('('); // put the ( back on, we'll need to pop back to it again
$index++;
$expecting_op = false;
//===============
} elseif ($op == '(' and !$expecting_op) {
$stack->push('('); // that was easy
$index++;
$allow_neg = true;
//===============
} elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
$expecting_op = true;
$val = $match[1];
if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
$stack->push($val);
$stack->push(1);
$stack->push('(');
$expecting_op = false;
} else { // it's a var w/ implicit multiplication
$val = $matches[1];
$output[] = $val;
}
} else { // it's a plain old var or num
$output[] = $val;
}
$index += strlen($val);
//===============
} elseif ($op == ')') { // miscellaneous error checking
return $this->trigger("unexpected ')'");
} elseif (in_array($op, $ops) and !$expecting_op) {
return $this->trigger("unexpected operator '$op'");
} else { // I don't even want to know what you did to get here
return $this->trigger("an unexpected error occured");
}
if ($index == strlen($expr)) {
if (in_array($op, $ops)) { // did we end with an operator? bad.
return $this->trigger("operator '$op' lacks operand");
} else {
break;
}
}
while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
$index++; // into implicit multiplication if no operator is there)
}
}
while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
if ($op == '(') return $this->trigger("expecting ')'"); // if there are (s on the stack, ()s were unbalanced
$output[] = $op;
}
return $output;
}
// evaluate postfix notation
function pfx($tokens, $vars = array()) {
if ($tokens == false) return false;
$stack = new ctools_math_expr_stack;
foreach ($tokens as $token) { // nice and easy
// if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
if (in_array($token, array('+', '-', '*', '/', '^'))) {
if (is_null($op2 = $stack->pop())) return $this->trigger("internal error");
if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
switch ($token) {
case '+':
$stack->push($op1+$op2); break;
case '-':
$stack->push($op1-$op2); break;
case '*':
$stack->push($op1*$op2); break;
case '/':
if ($op2 == 0) return $this->trigger("division by zero");
$stack->push($op1/$op2); break;
case '^':
$stack->push(pow($op1, $op2)); break;
}
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
} elseif ($token == "_") {
$stack->push(-1*$stack->pop());
// if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
} elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function!
$fnn = $matches[1];
if (in_array($fnn, $this->fb)) { // built-in function:
if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
$fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
if ($fnn == 'ln') $fnn = 'log';
eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
} elseif (array_key_exists($fnn, $this->f)) { // user function
// get args
$args = array();
for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger("internal error");
}
$stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
}
// if the token is a number or variable, push it on the stack
} else {
if (is_numeric($token)) {
$stack->push($token);
} elseif (array_key_exists($token, $this->v)) {
$stack->push($this->v[$token]);
} elseif (array_key_exists($token, $vars)) {
$stack->push($vars[$token]);
} else {
return $this->trigger("undefined variable '$token'");
}
}
}
// when we're out of tokens, the stack should have a single element, the final result
if ($stack->count != 1) return $this->trigger("internal error");
return $stack->pop();
}
// trigger an error, but nicely, if need be
function trigger($msg) {
$this->last_error = $msg;
if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
return false;
}
}
// for internal use
class ctools_math_expr_stack {
var $stack = array();
var $count = 0;
function push($val) {
$this->stack[$this->count] = $val;
$this->count++;
}
function pop() {
if ($this->count > 0) {
$this->count--;
return $this->stack[$this->count];
}
return null;
}
function last($n=1) {
return !empty($this->stack[$this->count-$n]) ? $this->stack[$this->count-$n] : NULL;
}
}

View File

@ -0,0 +1,98 @@
<?php
/**
* @file
* General menu helper functions.
*/
/**
* Dynamically add a tab to the current path.
*
* This function is a simplified interface for adding tabs to the current path.
* Some considerations when doing this:
*
* - First, if there is only 1 tab, Drupal will not show it. Therefore, if
* you are only adding one tab, you should find a way to make sure there is
* already tab, or instead add 2.
* - Second, the caller is responsible for providing access control to these
* links.
*
* @param $link
* An array describing this link. It must contain:
* - 'title': The printed title of the link.
* - 'href': The path of the link. This is an argument to l() so it has all
* of those features and limitations.
* - 'options': Any options that go to l, including query, fragment and html
* options necessary.
* - 'weight': The weight to use in ordering the tabs.
* - 'type': Optional. If set to MENU_DEFAULT_LOCAL_TASK this can be used to
* add a fake 'default' local task, which is useful if you have to add
* tabs to a page that has none.
*/
function ctools_menu_add_tab($link = NULL) {
$links = &drupal_static(__FUNCTION__, array());
if (isset($link)) {
$links[$link['href']] = $link;
}
return $links;
}
/**
* Re-sort menu items after we have modified them.
*/
function ctools_menu_sort($a, $b) {
$a_weight = (is_array($a) && isset($a['#link']['weight'])) ? $a['#link']['weight'] : 0;
$b_weight = (is_array($b) && isset($b['#link']['weight'])) ? $b['#link']['weight'] : 0;
if ($a_weight == $b_weight) {
$a_title = (is_array($a) && isset($a['#link']['title'])) ? $a['#link']['title'] : 0;
$b_title = (is_array($b) && isset($b['#link']['title'])) ? $b['#link']['title'] : 0;
if ($a_title == $b_title) {
return 0;
}
return ($a_title < $b_title) ? -1 : 1;
}
return ($a_weight < $b_weight) ? -1 : 1;
}
function _ctools_menu_add_dynamic_items(&$data, &$router_item, &$root_path) {
if ($additions = ctools_menu_add_tab()) {
// If none of the static local tasks are active allow one of the dynamic
// active tasks to be marked as such.
$has_active = FALSE;
if (!empty($data['tabs'][0]['output'])) {
foreach ($data['tabs'][0]['output'] as $element) {
if (!empty($element['#link']['#active'])) {
$has_active = TRUE;
}
}
}
foreach ($additions as $addition) {
$addition['localized_options'] = isset($addition['options']) ? $addition['options'] : array();
if (isset($addition['type']) && $addition['type'] == MENU_LOCAL_ACTION) {
$data['actions']['output'][] = array(
'#theme' => 'menu_local_action',
'#link' => $addition,
);
}
else {
$data['tabs'][0]['output'][] = array(
'#theme' => 'menu_local_task',
'#link' => $addition,
'#active' => (!$has_active && $root_path === $addition['href']),
);
}
}
if (!empty($data['tabs'][0]['output'])) {
uasort($data['tabs'][0]['output'], 'ctools_menu_sort');
$data['tabs'][0]['count'] = count($data['tabs'][0]['output']);
}
if (!empty($data['actions']['output'])) {
uasort($data['actions']['output'], 'ctools_menu_sort');
$data['actions']['count'] = count($data['actions']['output']);
}
}
}

View File

@ -0,0 +1,247 @@
<?php
/**
* @file
* Implement a modal form using AJAX.
*
* The modal form is implemented primarily from mc.js; this contains the
* Drupal specific stuff to use it. The modal is fairly generic and can
* be activated mostly by setting up the right classes, but if you are
* using the modal you must include links to the images in settings,
* because the javascript does not inherently know where the images are.
*
* You can accomplish this with this PHP code:
* @code {
* ctools_include('modal');
* ctools_modal_add_js();
* }
*
* You can have links and buttons bound to use the modal by adding the
* class ctools-use-modal.
*
* For links, the href of the link will be the destination, with any
* appearance of /nojs/ converted to /ajax/.
*
* For submit buttons, however, the URL is found a different, slightly
* more complex way. The ID of the item is taken and -url is appended to
* it to derive a class name. Then, all form elements that contain that
* class name are founded and their values put together to form a URL.
*
* For example, let's say you have an 'add' button, and it has a select
* form item that tells your system what widget it is adding. If the id
* of the add button is edit-add, you would place a hidden input with
* the base of your URL in the form and give it a class of 'edit-add-url'.
* You would then add 'edit-add-url' as a class to the select widget
* allowing you to convert this value to the form without posting.
*
* If no URL is found, the action of the form will be used and the entire
* form posted to it.
*/
function ctools_modal_add_js() {
// Provide a gate so we only do this once.
static $done = FALSE;
if ($done) {
return;
}
$settings = array(
'CToolsModal' => array(
'loadingText' => t('Loading...'),
'closeText' => t('Close Window'),
'closeImage' => theme('image', array(
'path' => ctools_image_path('icon-close-window.png'),
'title' => t('Close window'),
'alt' => t('Close window'),
)),
'throbber' => theme('image', array(
'path' => ctools_image_path('throbber.gif'),
'title' => t('Loading...'),
'alt' => t('Loading'),
)),
),
);
drupal_add_js($settings, 'setting');
drupal_add_library('system', 'jquery.form');
drupal_add_library('system', 'drupal.progress');
drupal_add_library('system', 'drupal.ajax');
ctools_add_js('modal');
ctools_add_css('modal');
$done = TRUE;
}
/**
* @todo this is deprecated
*/
function ctools_modal_add_plugin_js($plugins) {
$css = array();
$js = array(drupal_get_path('module', 'ctools') . '/js/dependent.js' => TRUE);
foreach ($plugins as $subtype) {
if (isset($subtype['js'])) {
foreach ($subtype['js'] as $file) {
if (file_exists($file)) {
$js[$file] = TRUE;
}
else if (file(exists($subtype['path'] . '/' . $file))) {
$js[$subtype['path'] . '/' . $file] = TRUE;
}
}
}
if (isset($subtype['css'])) {
foreach ($subtype['css'] as $file) {
if (file_exists($file)) {
$css[$file] = TRUE;
}
else if (file(exists($subtype['path'] . '/' . $file))) {
$css[$subtype['path'] . '/' . $file] = TRUE;
}
}
}
}
foreach (array_keys($js) as $file) {
drupal_add_js($file);
}
foreach (array_keys($css) as $file) {
drupal_add_css($file);
}
}
/**
* Place HTML within the modal.
*
* @param $title
* The title of the modal.
* @param $html
* The html to place within the modal.
*/
function ctools_modal_command_display($title, $html) {
if (is_array($html)) {
$html = drupal_render($html);
}
return array(
'command' => 'modal_display',
'title' => $title,
'output' => $html,
);
}
/**
* Dismiss the modal.
*/
function ctools_modal_command_dismiss() {
return array(
'command' => 'modal_dismiss',
);
}
/**
* Display loading screen in the modal
*/
function ctools_modal_command_loading() {
return array(
'command' => 'modal_loading',
);
}
/**
* Render an image as a button link. This will automatically apply an AJAX class
* to the link and add the appropriate javascript to make this happen.
*
* @param $image
* The path to an image to use that will be sent to theme('image') for rendering.
* @param $dest
* The destination of the link.
* @param $alt
* The alt text of the link.
* @param $class
* Any class to apply to the link. @todo this should be a options array.
*/
function ctools_modal_image_button($image, $dest, $alt, $class = '') {
return ctools_ajax_text_button(theme('image', array('path' => $image)), $dest, $alt, $class, 'ctools-use-modal');
}
/**
* Render text as a link. This will automatically apply an AJAX class
* to the link and add the appropriate javascript to make this happen.
*
* Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
* not use user input so this is a very minor concern.
*
* @param $text
* The text that will be displayed as the link.
* @param $dest
* The destination of the link.
* @param $alt
* The alt text of the link.
* @param $class
* Any class to apply to the link. @todo this should be a options array.
*/
function ctools_modal_text_button($text, $dest, $alt, $class = '') {
return ctools_ajax_text_button($text, $dest, $alt, $class, 'ctools-use-modal');
}
/**
* Wrap a form so that we can use it properly with AJAX. Essentially if the
* form wishes to render, it automatically does that, otherwise it returns
* so we can see submission results.
*
* @return
* The output of the form, if it was rendered. If $form_state['ajax']
* is set, this will use ctools_modal_form_render so it will be
* a $command object suitable for ajax_render already.
*
* The return will be NULL if the form was successfully submitted unless
* you specifically set re_render = TRUE. If ajax is set the
* form will never be redirected.
*/
function ctools_modal_form_wrapper($form_id, &$form_state) {
// This won't override settings already in.
$form_state += array(
're_render' => FALSE,
'no_redirect' => !empty($form_state['ajax']),
);
$output = drupal_build_form($form_id, $form_state);
if (!empty($form_state['ajax']) && (!$form_state['executed'] || $form_state['rebuild'])) {
return ctools_modal_form_render($form_state, $output);
}
return $output;
}
/**
* Render a form into an AJAX display.
*/
function ctools_modal_form_render($form_state, $output) {
if (is_array($output)) {
$output = drupal_render($output);
}
$title = empty($form_state['title']) ? drupal_get_title() : $form_state['title'];
// If there are messages for the form, render them.
if ($messages = theme('status_messages')) {
$output = $messages . $output;
}
$commands = array();
// If the form has not yet been rendered, render it.
$commands[] = ctools_modal_command_display($title, $output);
return $commands;
}
/**
* Perform a simple modal render and immediately exit.
*
* This is primarily used for error displays, since usually modals will
* contain forms.
*/
function ctools_modal_render($title, $output) {
$commands = array();
$commands[] = ctools_modal_command_display($title, $output);
print ajax_render($commands);
}

View File

@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains cron hooks for the object cache tool.
*
* We use this to clean up old object caches.
*/
function ctools_object_cache_cron() {
if (variable_get('ctools_last_cron', 0) < time() - 86400) {
variable_set('ctools_last_cron', time());
ctools_include('object-cache');
ctools_object_cache_clean();
}
}

View File

@ -0,0 +1,205 @@
<?php
/**
* @file
* The non-volatile object cache is used to store an object while it is
* being edited, so that we don't have to save until we're completely
* done. The cache should be 'cleaned' on a regular basis, meaning to
* remove old objects from the cache, but otherwise the data in this
* cache must remain stable, as it includes unsaved changes.
*/
/**
* Get an object from the non-volatile ctools cache.
*
* This function caches in memory as well, so that multiple calls to this
* will not result in multiple database reads.
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $name
* The name of the object being stored.
* @param $skip_cache
* Skip the memory cache, meaning this must be read from the db again.
* @param $sid
* The session id, allowing someone to use Session API or their own solution;
* defaults to session_id().
*
* @deprecated $skip_cache is deprecated in favor of drupal_static*
* @return
* The data that was cached.
*/
function ctools_object_cache_get($obj, $name, $skip_cache = FALSE, $sid = NULL) {
$cache = &drupal_static(__FUNCTION__, array());
$key = "$obj:$name";
if ($skip_cache) {
unset($cache[$key]);
}
if (!$sid) {
$sid = session_id();
}
if (!array_key_exists($key, $cache)) {
$data = db_query('SELECT * FROM {ctools_object_cache} WHERE sid = :session_id AND obj = :object AND name = :name', array(':session_id' => $sid, ':object' => $obj, ':name' => $name))
->fetchObject();
if ($data) {
$cache[$key] = unserialize($data->data);
}
}
return isset($cache[$key]) ? $cache[$key] : NULL;
}
/**
* Store an object in the non-volatile ctools cache.
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $name
* The name of the object being stored.
* @param $cache
* The object to be cached. This will be serialized prior to writing.
* @param $sid
* The session id, allowing someone to use Session API or their own solution;
* defaults to session_id().
*/
function ctools_object_cache_set($obj, $name, $cache, $sid = NULL) {
// Store the CTools session id in the user session to force a
// session for anonymous users in Drupal 7 and Drupal 6 Pressflow.
// see http://drupal.org/node/562374, http://drupal.org/node/861778
if (empty($GLOBALS['user']->uid) && empty($_SESSION['ctools_session_id'])) {
$_SESSION['ctools_hold_session'] = TRUE;
}
ctools_object_cache_clear($obj, $name, $sid);
if (!$sid) {
$sid = session_id();
}
db_insert('ctools_object_cache')
->fields(array(
'sid' => $sid,
'obj' => $obj,
'name' => $name,
'data' => serialize($cache),
'updated' => REQUEST_TIME,
))
->execute();
}
/**
* Remove an object from the non-volatile ctools cache
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $name
* The name of the object being removed.
* @param $sid
* The session id, allowing someone to use Session API or their own solution;
* defaults to session_id().
*/
function ctools_object_cache_clear($obj, $name, $sid = NULL) {
if (!$sid) {
$sid = session_id();
}
db_delete('ctools_object_cache')
->condition('sid', $sid)
->condition('obj', $obj)
->condition('name', $name)
->execute();
// Ensure the static cache is emptied of this obj:name set.
drupal_static_reset('ctools_object_cache_get');
}
/**
* Determine if another user has a given object cached.
*
* This is very useful for 'locking' objects so that only one user can
* modify them.
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $name
* The name of the object being removed.
* @param $sid
* The session id, allowing someone to use Session API or their own solution;
* defaults to session_id().
*
* @return
* An object containing the UID and updated date if found; NULL if not.
*/
function ctools_object_cache_test($obj, $name, $sid = NULL) {
if (!$sid) {
$sid = session_id();
}
return db_query('SELECT s.uid, c.updated FROM {ctools_object_cache} c INNER JOIN {sessions} s ON c.sid = s.sid WHERE s.sid <> :session_id AND c.obj = :obj AND c.name = :name ORDER BY c.updated ASC', array(':session_id' => $sid, ':obj' => $obj, ':name' => $name))
->fetchObject();
}
/**
* Get the cache status of a group of objects.
*
* This is useful for displaying lock status when listing a number of objects
* an an administration UI.
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $names
* An array of names of objects
*
* @return
* An array of objects containing the UID and updated date for each name found.
*/
function ctools_object_cache_test_objects($obj, $names) {
return db_query("SELECT c.name, s.uid, c.updated FROM {ctools_object_cache} c INNER JOIN {sessions} s ON c.sid = s.sid WHERE c.obj = :obj AND c.name IN (:names) ORDER BY c.updated ASC", array(':obj' => $obj, ':names' => $names))
->fetchAllAssoc('name');
}
/**
* Remove an object from the non-volatile ctools cache for all session IDs.
*
* This is useful for clearing a lock.
*
* @param $obj
* A 128 character or less string to define what kind of object is being
* stored; primarily this is used to prevent collisions.
* @param $name
* The name of the object being removed.
*/
function ctools_object_cache_clear_all($obj, $name) {
db_delete('ctools_object_cache')
->condition('obj', $obj)
->condition('name', $name)
->execute();
// Ensure the static cache is emptied of this obj:name set.
$cache = &drupal_static('ctools_object_cache_get', array());
unset($cache["$obj:$name"]);
}
/**
* Remove all objects in the object cache that are older than the
* specified age.
*
* @param $age
* The minimum age of objects to remove, in seconds. For example, 86400 is
* one day. Defaults to 7 days.
*/
function ctools_object_cache_clean($age = NULL) {
if (empty($age)) {
$age = 86400 * 7; // 7 days
}
db_delete('ctools_object_cache')
->condition('updated', REQUEST_TIME - $age, '<')
->execute();
}

View File

@ -0,0 +1,194 @@
<?php
/**
* Fetch metadata on a specific page_wizard plugin.
*
* @param $page_wizard
* Name of a panel page_wizard.
*
* @return
* An array with information about the requested panel page_wizard.
*/
function page_manager_get_page_wizard($page_wizard) {
ctools_include('plugins');
return ctools_get_plugins('page_manager', 'page_wizards', $page_wizard);
}
/**
* Fetch metadata for all page_wizard plugins.
*
* @return
* An array of arrays with information about all available panel page_wizards.
*/
function page_manager_get_page_wizards() {
ctools_include('plugins');
return ctools_get_plugins('page_manager', 'page_wizards');
}
/**
* Get the cached changes to a given wizard.
*
* @return
* A $cache object or a clean cache object if none could be loaded.
*/
function page_manager_get_wizard_cache($plugin) {
if (is_string($plugin)) {
$plugin = page_manager_get_page_wizard($plugin);
}
if (empty($plugin)) {
return;
}
ctools_include('object-cache');
// Since contexts might be cache, include this so they load.
ctools_include('context');
$cache = ctools_object_cache_get('page_manager_page_wizard', $plugin['name']);
if (!$cache) {
$cache = page_manager_make_wizard_cache($plugin);
}
return $cache;
}
function page_manager_make_wizard_cache($plugin) {
$cache = new stdClass;
$cache->plugin = $plugin;
if ($function = ctools_plugin_get_function($plugin, 'default cache')) {
$function($cache);
}
return $cache;
}
/**
* Store changes to a task handler in the object cache.
*/
function page_manager_set_wizard_cache($cache) {
ctools_include('object-cache');
ctools_object_cache_set('page_manager_page_wizard', $cache->plugin['name'], $cache);
}
/**
* Remove an item from the object cache.
*/
function page_manager_clear_wizard_cache($name) {
ctools_include('object-cache');
ctools_object_cache_clear('page_manager_page_wizard', $name);
}
/**
* Menu callback for the page wizard.
*/
function page_manager_page_wizard($name, $step = NULL) {
$plugin = page_manager_get_page_wizard($name);
if (!$plugin) {
return MENU_NOT_FOUND;
}
// Check for simple access string on plugin.
if (!empty($plugin['access']) && !user_access($plugin['access'])) {
return MENU_ACCESS_DENIED;
}
// Check for possibly more complex access callback on plugin.
if ($function = ctools_plugin_get_function($plugin, 'access callback') && !$function($plugin)) {
return MENU_ACCESS_DENIED;
}
// Create a basic wizard.in form info array and merge it with the
// plugin's.
$form_info = array(
'id' => 'page_manager_page_wizard',
'show trail' => TRUE,
'show back' => TRUE,
'show return' => FALSE,
'show cancel' => FALSE,
'next callback' => 'page_manager_page_wizard_next',
'finish callback' => 'page_manager_page_wizard_finish',
'path' => "admin/structure/pages/wizard/$name/%step",
);
$form_info = array_merge_recursive($form_info, $plugin['form info']);
// If step is unset, go with the basic step.
if (!isset($step)) {
$step = current(array_keys($form_info['order']));
$cache = page_manager_make_wizard_cache($plugin);
}
else {
$cache = page_manager_get_wizard_cache($plugin);
}
ctools_include('wizard');
$form_state = array(
'plugin' => $plugin,
'wizard cache' => $cache,
'type' => 'edit',
'rerender' => TRUE,
'step' => $step,
);
if (isset($plugin['page title'])) {
drupal_set_title($plugin['page title']);
}
if ($function = ctools_plugin_get_function($form_state['plugin'], 'start')) {
$function($form_info, $step, $form_state);
}
$output = ctools_wizard_multistep_form($form_info, $step, $form_state);
return $output;
}
/**
* Callback generated when the add page process is finished.
*/
function page_manager_page_wizard_finish(&$form_state) {
if ($function = ctools_plugin_get_function($form_state['plugin'], 'finish')) {
$function($form_state);
}
page_manager_clear_wizard_cache($form_state['wizard cache']->plugin['name']);
}
/**
* Callback generated when the 'next' button is clicked.
*
* All we do here is store the cache.
*/
function page_manager_page_wizard_next(&$form_state) {
if ($function = ctools_plugin_get_function($form_state['plugin'], 'next')) {
$function($form_state);
}
page_manager_set_wizard_cache($form_state['wizard cache']);
}
/**
* Provide a simple administrative list of all wizards.
*
* This is called as a page callback, but can also be used by any module
* that wants to get a list of wizards for its type.
*/
function page_manager_page_wizard_list($type = NULL) {
$plugins = page_manager_get_page_wizards();
if (empty($plugins)) {
return '<p>' . t('There are no wizards available at this time.') . '</p>';
}
uasort($plugins, 'ctools_plugin_sort');
$output = '<dl class="page-manager-wizards">';
foreach ($plugins as $id => $plugin) {
if (!$type || (isset($plugin['type']) && $plugin['type'] == $type)) {
$output .= '<dt>' . l($plugin['title'], 'admin/structure/pages/wizard/' . $id) . '</dt>';
$output .= '<dd class="description">' . $plugin['description'] . '</dd>';
}
}
$output .= '</dl>';
return $output;
}

View File

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains menu item registration for the page manager page wizards tool.
*/
function ctools_page_wizard_menu(&$items) {
if (!module_exists('page_manager')) {
return;
}
$base = array(
'access arguments' => array('use page manager'),
'file' => 'includes/page-wizard.inc',
'type' => MENU_CALLBACK,
);
$items['admin/structure/pages/wizard'] = array(
'title' => 'Wizards',
'page callback' => 'page_manager_page_wizard_list',
'page arguments' => array(4),
'weight' => -5,
'type' => MENU_LOCAL_TASK,
) + $base;
$items['admin/structure/pages/wizard/%'] = array(
'title' => 'Wizard',
'page callback' => 'page_manager_page_wizard',
'page arguments' => array(4),
) + $base;
}

View File

@ -0,0 +1,208 @@
<?php
/**
* @file
* Contains generic plugin administration functions.
*
* CTools includes the ability to (relatively) easily provide wizard based
* configuration for plugins, meaning plugins that need configuration can
* automatically allow multi-step forms.
*
* Implementing this
*/
/**
* Get a plugin configuration form.
*
* The $form_info and $form_state need to be preconfigured with data you'll need
* such as whether or not you're using ajax, or the modal. $form_info will need
* your next/submit callbacks so that you can cache your data appropriately.
*
* @param array $form_info
* This form_info *must* contain at least the path. next and finish callbacks
* are advisable but not necessary if using wizard's auto caching. Setting
* a cache mechanism is advisable. If not using auto caching, next and finish
* callbacks will be necessary.
*
* Also be sure to set up things such as AJAX settings and any temporary
* data you will need. Simply setting 'modal' => TRUE and
* 'modal return' => TRUE should make this system work well in the modal.
*
* In addition to standard wizard fields, this supports one extra field:
* - 'default form': A callback to a 'wrapper' that will be applied to either
* the first or a marked form. This is useful for adding global features that
* are applicable to all instances of a plugin, such as identifiers, or
* contexts, or the like.
*
* @param array &$form_state
* This is a standard form state array. This system imposes some requirements
* on what will be in the form state:
*
* - 'plugin': The plugin definition being edited.
* - 'conf': The configuration that is being edited, presumed to be an array.
* Ultimately at the end, this is where the modified config will be
* found.
* - 'op': The operation, either 'add' or 'edit'. This is used to derive form
* names and can also be used to derive default values from the plugin.
* - 'step': The current step. May be null if it is the 'first' step, but
* generally needs to be defined in the path.
*
* @param string $default_form
* An optional default form that can be added.
*
* @return
* If this function returns false, no form exists.
*/
function ctools_plugin_configure_form($form_info, &$form_state) {
// Turn the forms defined in the plugin into the format the wizard needs.
_ctools_plugin_configure_create_form_info($form_info, $form_state['plugin'], $form_state['op']);
if (empty($form_info['order'])) {
return FALSE;
}
ctools_include('wizard');
return ctools_wizard_multistep_form($form_info, $form_state['step'], $form_state);
}
function _ctools_plugin_configure_create_form_info(&$form_info, $plugin_definition, $op) {
// Provide a few extra defaults.
$form_info += array(
'id' => 'ctools_plugin_configure_form',
'show back' => TRUE,
);
$add_form = isset($form_info['add form name']) ? $form_info['add form name'] : 'add form';
$edit_form = isset($form_info['edit form name']) ? $form_info['edit form name'] : 'edit form';
// Figure out what the forms should actually be. Since they can be specified
// in a couple of different ways (in order to support simple declarations for
// the minimal forms but more complex declarations for powerful wizards).
if ($op == 'add') {
if (!empty($plugin_definition[$add_form])) {
$info = $plugin_definition[$add_form];
}
}
if (!isset($info) || $op == 'edit') {
// Use the edit form for the add form if add form was completely left off.
if (!empty($plugin_definition[$edit_form])) {
$info = $plugin_definition[$edit_form];
}
}
// If there is a default form wrapper, but no form is supplied,
// use the wrapper as the form.
if (empty($info) && !empty($form_info['default form'])) {
$info = $form_info['default form'];
}
// @todo we may want to make these titles more settable?
if (is_string($info)) {
if (empty($plugin_definition['title'])) {
$title = t('Configure');
}
else if ($op == 'add') {
$title = t('Configure new !plugin_title', array('!plugin_title' => $plugin_definition['title']));
}
else {
$title = t('Configure !plugin_title', array('!plugin_title' => $plugin_definition['title']));
}
if (empty($form_info['order'])) {
$form_info['order'] = array();
}
$form_info['order']['form'] = $title;
if (empty($form_info['forms'])) {
$form_info['forms'] = array();
}
$form_info['forms']['form'] = array(
'title' => $title,
'form id' => $info,
);
// Add the default form if one is specified.
if (!empty($form_info['default form']) && $form_info['forms']['form']['form id'] != $form_info['default form']) {
$form_info['forms']['form']['wrapper'] = $form_info['default form'];
}
// If no submit is supplied, supply the default submit which will do the
// most obvious task.
if (!function_exists($form_info['forms']['form']['form id'] . '_submit')) {
// Store the original wrapper so we can chain it.
if (!empty($form_info['forms']['form']['wrapper'])) {
$form_info['forms']['form']['original wrapper'] = $form_info['forms']['form']['wrapper'];
}
$form_info['forms']['form']['wrapper'] = 'ctools_plugins_default_form_wrapper';
}
}
else if (is_array($info)) {
if (empty($form_info['order'])) {
$form_info['order'] = array();
}
if (empty($form_info['forms'])) {
$form_info['forms'] = array();
}
$count = 0;
$base = 'step';
$wrapper = NULL;
foreach ($info as $form_id => $title) {
$step = $base . ++$count;
if (empty($wrapper)) {
$wrapper = $step;
}
if (is_array($title)) {
if (!empty($title['default'])) {
$wrapper = $step;
}
$title = $title['title'];
}
$form_info['order'][$step] = $title;
$form_info['forms'][$step] = array(
'title' => $title,
'form id' => $form_id,
);
}
if ($wrapper && !empty($form_info['default form'])) {
$form_info['forms'][$wrapper]['wrapper'] = $form_info['default form'];
}
}
}
/**
* A wrapper to provide a default submit so that plugins don't have to duplicate
* a whole bunch of code to do what most of them want to do anyway.
*/
function ctools_plugins_default_form_wrapper($form, &$form_state) {
$form_info = &$form_state['form_info'];
$info = $form_info['forms'][$form_state['step']];
if (isset($info['original wrapper']) && function_exists($info['original wrapper'])) {
$form = $info['original wrapper']($form, $form_state);
}
if (isset($form['buttons']['next'])) {
if (empty($form['buttons']['next']['#submit'])) {
$form['buttons']['next']['#submit'] = $form['#submit'];
}
$form['buttons']['next']['#submit'][] = 'ctools_plugins_default_form_wrapper_submit';
}
if (isset($form['buttons']['return'])) {
if (empty($form['buttons']['return']['#submit'])) {
$form['buttons']['return']['#submit'] = $form['#submit'];
}
$form['buttons']['return']['#submit'][] = 'ctools_plugins_default_form_wrapper_submit';
}
return $form;
}
/**
* Provide a default storage mechanism.
*/
function ctools_plugins_default_form_wrapper_submit(&$form, &$form_state) {
foreach (array_keys($form_state['plugin']['defaults']) as $key) {
if (isset($form_state['values'][$key])) {
$form_state['conf'][$key] = $form_state['values'][$key];
}
}
}

View File

@ -0,0 +1,915 @@
<?php
/**
* @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.
*/
/**
* Get an array of information about modules that support an API.
*
* This will ask each module if they support the given API, and if they do
* it will return an array of information about the modules that do.
*
* This function invokes hook_ctools_api. This invocation is statically
* cached, so feel free to call it as often per page run as you like, it
* will cost very little.
*
* 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.
*
* @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
* 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.
*/
function ctools_plugin_api_info($owner, $api, $minimum_version, $current_version) {
$cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$owner][$api])) {
$cache[$owner][$api] = array();
$hook = ctools_plugin_api_get_hook($owner, $api);
foreach (module_implements($hook) as $module) {
$function = $module . '_' . $hook;
$info = $function($owner, $api);
$version = NULL;
// This is added to make hook_views_api() compatible with this, since
// views used a different version key.
if (isset($info['version'])) {
$version = $info['version'];
}
else if (isset($info['api'])) {
$version = $info['api'];
}
if (!isset($version)) {
continue;
}
// Only process if version is between minimum and current, inclusive.
if (version_compare($version, $minimum_version, '>=') && version_compare($version, $current_version, '<=')) {
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 (version_compare($info['version'], $minimum_version, '>=') && version_compare($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;
}
}
}
// Allow other modules to hook in.
drupal_alter($hook, $cache[$owner][$api]);
}
return $cache[$owner][$api];
}
/**
* 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);
foreach ($info as $module => $plugin_info) {
if (!isset($already_done[$owner][$api][$module])) {
if (isset($plugin_info["$api file"])) {
$file = $plugin_info["$api file"];
}
else if (isset($plugin_info['file'])) {
$file = $plugin_info['file'];
}
else {
$file = "$module.$api.inc";
}
if (file_exists(DRUPAL_ROOT . "/$plugin_info[path]/$file")) {
require_once DRUPAL_ROOT . "/$plugin_info[path]/$file";
}
else if (file_exists(DRUPAL_ROOT . "/$file")) {
require_once DRUPAL_ROOT . "/$file";
}
$already_done[$owner][$api][$module] = TRUE;
}
}
return $info;
}
/**
* Find out what hook to use to determine if modules support an API.
*
* By default, most APIs will use hook_ctools_plugin_api, but some modules
* want sole ownership. This technique lets modules define what hook
* to use.
*/
function ctools_plugin_api_get_hook($owner, $api) {
// Allow modules to use their own hook for this. The only easy way to do
// this right now is with a magically named function.
if (function_exists($function = $owner . '_' . $api . '_hook_name')) {
$hook = $function();
}
else if (function_exists($function = $owner . '_ctools_plugin_api_hook_name')) {
$hook = $function();
}
// Do this last so that if the $function above failed to return, we have a
// sane default.
if (empty($hook)) {
$hook = 'ctools_plugin_api';
}
return $hook;
}
/**
* 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.
$plugins = &drupal_static('ctools_plugins', array());
$info = ctools_plugin_get_plugin_type_info();
// Bail out noisily if an invalid module/type combination is requested.
if (!isset($info[$module][$type])) {
watchdog('ctools', 'Invalid plugin module/type combination requested: module @module and type @type', array('@module' => $module, '@type' => $type), WATCHDOG_ERROR);
return array();
}
// Make sure our plugins array is populated.
if (!isset($plugins[$module][$type])) {
$plugins[$module][$type] = array();
}
// Attempt to shortcut this whole piece of code if we already have
// the requested plugin:
if ($id && array_key_exists($id, $plugins[$module][$type])) {
return $plugins[$module][$type][$id];
}
// 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.
$setup = &drupal_static('ctools_plugin_setup', 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])) {
$cache = cache_get("plugins:$module:$type", $info[$module][$type]['cache table']);
if (!empty($cache->data)) {
// Cache load succeeded so use the cached plugin list.
$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 the plugin can have child plugins, and we're loading all plugins,
// go through the list of plugins we have and find child plugins.
if (!$id && !empty($info[$module][$type]['child plugins'])) {
// If a plugin supports children, go through each plugin and ask.
$temp = array();
foreach ($plugins[$module][$type] as $name => $plugin) {
// The strpos ensures that we don't try to find children for plugins
// that are already children.
if (!empty($plugin['get children']) && function_exists($plugin['get children']) && strpos($name, ':') === FALSE) {
$temp = array_merge($plugin['get children']($plugin, $name), $temp);
}
else {
$temp[$name] = $plugin;
}
}
$plugins[$module][$type] = $temp;
}
}
// 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) {
// Use array_filter because looking for unknown plugins could cause NULL
// entries to appear in the list later.
return array_filter($plugins[$module][$type]);
}
// Check to see if we need to look for the file
if (!array_key_exists($id, $plugins[$module][$type])) {
// If we can have child plugins, check to see if the plugin name is in the
// format of parent:child and break it up if it is.
if (!empty($info[$module][$type]['child plugins']) && strpos($id, ':') !== FALSE) {
list($parent, $child) = explode(':', $id, 2);
}
else {
$parent = $id;
}
if (!array_key_exists($parent, $plugins[$module][$type])) {
$result = ctools_plugin_load_includes($info[$module][$type], $parent);
// Set to either what was returned or NULL.
$plugins[$module][$type][$parent] = isset($result[$parent]) ? $result[$parent] : NULL;
}
// If we are looking for a child, and have the parent, ask the parent for the child.
if (!empty($child) && !empty($plugins[$module][$type][$parent]) && function_exists($plugins[$module][$type][$parent]['get child'])) {
$plugins[$module][$type][$id] = $plugins[$module][$type][$parent]['get child']($plugins[$module][$type][$parent], $parent, $child);
}
}
// At this point we should either have the plugin, or a NULL.
return $plugins[$module][$type][$id];
}
/**
* Return the full list of plugin type info for all plugin types registered in
* the current system.
*
* This function manages its own cache getting/setting, and should always be
* used as the way to initially populate the list of plugin types. Make sure you
* call this function to properly populate the ctools_plugin_type_info static
* variable.
*
* @return array
* A multilevel array of plugin type info, the outer array keyed on module
* name and each inner array keyed on plugin type name.
*/
function ctools_plugin_get_plugin_type_info($flush = FALSE) {
$info_loaded = &drupal_static('ctools_plugin_type_info_loaded', FALSE);
$all_type_info = &drupal_static('ctools_plugin_type_info', array());
// Only trigger info loading once.
if ($info_loaded && !$flush) {
return $all_type_info;
}
$info_loaded = TRUE;
$cache = cache_get('ctools_plugin_type_info');
if (!empty($cache->data) && !$flush) {
// Plugin type info cache is warm, use it.
$all_type_info = $cache->data;
}
else {
// Cache expired, refill it.
foreach (module_implements('ctools_plugin_type') as $module) {
$module_infos = array();
$function = $module . '_ctools_plugin_type';
$module_infos = $function();
foreach ($module_infos as $plugin_type_name => $plugin_type_info) {
// Apply defaults. Array addition will not overwrite pre-existing keys.
$plugin_type_info += array(
'module' => $module,
'type' => $plugin_type_name,
'cache' => FALSE,
'cache table' => 'cache',
'classes' => array(),
'use hooks' => FALSE,
'defaults' => array(),
'process' => '',
'alterable' => TRUE,
'extension' => 'inc',
'info file' => FALSE,
'hook' => $module . '_' . $plugin_type_name,
'load themes' => FALSE,
);
$all_type_info[$module][$plugin_type_name] = $plugin_type_info;
}
}
cache_set('ctools_plugin_type_info', $all_type_info);
}
return $all_type_info;
}
/**
* Reset all static caches that affect the result of ctools_get_plugins().
*/
function ctools_get_plugins_reset() {
drupal_static_reset('ctools_plugins');
drupal_static_reset('ctools_plugin_setup');
drupal_static_reset('ctools_plugin_load_includes');
drupal_static_reset('ctools_plugin_api_info');
}
/**
* 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, $filename = NULL) {
// Keep a static array so we don't hit file_scan_directory more than necessary.
$all_files = &drupal_static(__FUNCTION__, array());
// store static of plugin arrays for reference because they can't be reincluded.
static $plugin_arrays = array();
// If we're being asked for all plugins of a type, skip any caching
// we may have done because this is an admin task and it's ok to
// spend the extra time.
if (!isset($filename)) {
$all_files[$info['module']][$info['type']] = NULL;
}
if (!isset($all_files[$info['module']][$info['type']])) {
// If a filename was set, we will try to load our list of files from
// cache. This is considered normal operation and we try to reduce
// the time spent finding files.
if (isset($filename)) {
$cache = cache_get("ctools_plugin_files:$info[module]:$info[type]");
if ($cache) {
$all_files[$info['module']][$info['type']] = $cache->data;
}
}
if (!isset($all_files[$info['module']][$info['type']])) {
$all_files[$info['module']][$info['type']] = array();
// Load all our plugins.
$directories = ctools_plugin_get_directories($info);
$extension = empty($info['info file']) ? $info['extension'] : 'info';
foreach ($directories as $module => $path) {
$all_files[$info['module']][$info['type']][$module] = file_scan_directory($path, '/\.' . $extension . '$/', array('key' => 'name'));
}
cache_set("ctools_plugin_files:$info[module]:$info[type]", $all_files[$info['module']][$info['type']]);
}
}
$file_list = $all_files[$info['module']][$info['type']];
$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) {
if ($filename) {
$files = isset($files[$filename]) ? array($filename => $files[$filename]) : array();
}
foreach ($files as $file) {
if (!empty($info['info file'])) {
// Parse a .info file
$result = ctools_plugin_process_info($info, $module, $file);
}
else {
// Parse a hook.
$plugin = NULL; // ensure that we don't have something leftover from earlier.
if (isset($plugin_arrays[$file->uri])) {
$identifier = $plugin_arrays[$file->uri];
}
else {
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()
// 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.
if (isset($plugin)) {
$plugin_arrays[$file->uri] = $plugin;
$identifier = $plugin;
}
else {
$identifier = $module . '_' . $file->name;
}
}
$result = ctools_plugin_process($info, $module, $identifier, dirname($file->uri), basename($file->uri), $file->name);
}
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']);
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']];
}
}
}
return $directories;
}
/**
* 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) {
if (isset($all_themes[$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;
}
/**
* 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.
*/
function ctools_plugin_process($info, $module, $identifier, $path, $file = NULL, $base = NULL) {
if (is_array($identifier)) {
$result = $identifier;
}
else {
$function = $identifier . '_' . $info['hook'];
if (!function_exists($function)) {
return NULL;
}
$result = $function();
if (!isset($result) || !is_array($result)) {
return NULL;
}
}
// 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, $plugin_type_info, $module, $path, $file) {
// Fill in global defaults.
foreach ($result as $name => $plugin) {
$result[$name] += array(
'module' => $module,
'name' => $name,
'path' => $path,
'file' => $file,
'plugin module' => $plugin_type_info['module'],
'plugin type' => $plugin_type_info['type'],
);
// Fill in plugin-specific defaults, if they exist.
if (!empty($plugin_type_info['defaults'])) {
if (is_array($plugin_type_info['defaults'])) {
$result[$name] += $plugin_type_info['defaults'];
}
}
// Allow the plugin to be altered before processing.
if (!empty($plugin_type_info['alterable']) && $plugin_type_info['alterable']) {
drupal_alter('ctools_plugin_pre', $result[$name], $plugin_type_info);
}
// Allow the plugin owner to do additional processing.
if (!empty($plugin_type_info['process']) && $function = ctools_plugin_get_function($plugin_type_info, 'process')) {
$function($result[$name], $plugin_type_info);
}
// Allow the plugin to be altered after processing.
if (!empty($plugin_type_info['alterable']) && $plugin_type_info['alterable']) {
drupal_alter('ctools_plugin_post', $result[$name], $plugin_type_info);
}
}
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->uri);
if ($result) {
$result = array($file->name => $result);
return _ctools_process_data($result, $info, $module, dirname($file->uri), basename($file->uri));
}
}
/**
* Ask a module for info about a particular plugin type.
*/
function ctools_plugin_get_info($module, $type) {
$all_info = ctools_plugin_get_plugin_type_info();
return isset($all_info[$module][$type]) ? $all_info[$module][$type] : array();
}
/**
* Get a function from a plugin, if it exists. If the plugin is not already
* loaded, try ctools_plugin_load_function() instead.
*
* @param $plugin_definition
* 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) {
// 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'])) {
// 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']);
if (empty($info['info file'])) {
require_once DRUPAL_ROOT . '/' . $plugin_definition['path'] . '/' . $plugin_definition['file'];
}
}
if (!isset($plugin_definition[$function_name])) {
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;
}
require_once DRUPAL_ROOT . '/' . $file;
}
}
else {
$function = $plugin_definition[$function_name];
}
if (function_exists($function)) {
return $function;
}
}
/**
* 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);
return ctools_plugin_get_function($plugin, $function_name);
}
/**
* Get a class from a plugin, if it exists. If the plugin is not already
* loaded, try ctools_plugin_load_class() instead.
*
* @param $plugin_definition
* The loaded plugin type.
* @param $class_name
* The identifier of the class. For example, 'handler'.
*
* @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) {
// 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'])) {
// 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']);
if (empty($info['info file'])) {
require_once DRUPAL_ROOT . '/' . $plugin_definition['path'] . '/' . $plugin_definition['file'];
}
}
$return = FALSE;
if (!isset($plugin_definition[$class_name])) {
return;
}
else if (is_string($plugin_definition[$class_name])) {
// Plugin uses the string form shorthand.
$return = $plugin_definition[$class_name];
}
else if (isset($plugin_definition[$class_name]['class'])) {
// Plugin uses the verbose array form.
$return = $plugin_definition[$class_name]['class'];
}
// @todo consider adding an else {watchdog(...)} here
return ($return && class_exists($return)) ? $return : NULL;
}
/**
* 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'.
*
* @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) {
$plugin = ctools_get_plugins($module, $type, $id);
return ctools_plugin_get_class($plugin, $class_name);
}
/**
* Sort callback for sorting plugins naturally.
*
* Sort first by weight, then by title.
*/
function ctools_plugin_sort($a, $b) {
if (is_object($a)) {
$a = (array) $a;
}
if (is_object($b)) {
$b = (array) $b;
}
if (empty($a['weight'])) {
$a['weight'] = 0;
}
if (empty($b['weight'])) {
$b['weight'] = 0;
}
if ($a['weight'] == $b['weight']) {
return strnatcmp(strtolower($a['title']), strtolower($b['title']));
}
return ($a['weight'] < $b['weight']) ? -1 : 1;
}

View File

@ -0,0 +1,77 @@
<?php
/**
* @file
*
* Registry magic. In a separate file to minimize unnecessary code loading.
*/
/**
* Implements (via delegation) hook_registry_files_alter().
*
* Alter the registry of files to automagically include all classes in
* class-based plugins.
*/
function _ctools_registry_files_alter(&$files, $indexed_modules) {
ctools_include('plugins');
// Remap the passed indexed modules into a useful format.
$modules = array();
foreach ($indexed_modules as $module_object) {
$modules[$module_object->name] = $module_object;
}
$all_type_info = ctools_plugin_get_plugin_type_info(TRUE);
foreach ($all_type_info as $module => $plugin_types) {
foreach ($plugin_types as $plugin_type_name => $plugin_type_info) {
if (empty($plugin_type_info['classes'])) {
// This plugin type does not use classes, so skip it.
continue;
}
// Retrieve the full list of plugins of this type.
$plugins = ctools_get_plugins($module, $plugin_type_name);
foreach ($plugins as $plugin_name => $plugin_definition) {
foreach ($plugin_type_info['classes'] as $class_key) {
if (empty($plugin_definition[$class_key])) {
// Plugin doesn't provide a class for this class key, so skip it.
continue;
}
if (is_string($plugin_definition[$class_key])) {
// Plugin definition uses the shorthand for defining a class name
// and location; look for the containing file by following naming
// convention.
$path = $plugin_definition['path'] . '/' . $plugin_definition[$class_key] . '.class.php';
}
else {
// Plugin uses the verbose definition to indicate where its class
// files are.
$class = $plugin_definition[$class_key]['class'];
// Use the filename if it's explicitly set, else follow the naming
// conventions.
$filename = isset($plugin_definition[$class_key]['file']) ? $plugin_definition[$class_key]['file'] : $class . '.class.php';
$base_path = isset($plugin_definition[$class_key]['path']) ? $plugin_definition[$class_key]['path'] : $plugin_definition['path'];
$path = "$base_path/$filename";
}
if (file_exists($path)) {
// If the file exists, add it to the files for registry building.
$files[$path] = array('module' => $plugin_definition['module'], 'weight' => $modules[$plugin_definition['module']]->weight);
}
else {
// Else, watchdog that we've got some erroneous plugin info.
$args = array(
'@plugin' => $plugin_definition['name'],
'@owner' => $module,
'@type' => $plugin_type_name,
'@file' => $path,
'@class' => $class_key,
);
watchdog('ctools', 'Plugin @plugin of plugin type @owner:@type points to nonexistent file @file for class handler @class.', $args);
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains theme registry and theme implementations for the content types.
*/
/**
* Implementation of hook_theme to load all content plugins and pass thru if
* necessary.
*/
function ctools_stylizer_theme(&$theme) {
$theme['ctools_stylizer_color_scheme_form'] = array(
'render element' => 'form',
'file' => 'includes/stylizer.inc',
);
$theme['ctools_stylizer_preview_form'] = array(
'render element' => 'form',
'file' => 'includes/stylizer.inc',
);
$theme['ctools_style_icon'] = array(
'variables' => array('image' => NULL, 'title' => NULL),
'file' => 'includes/stylizer.inc',
);
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains general utility functions for CTools that do not need to be
* in the module file.
*
* In particular, things that are only needed during hook_menu() and
* hook_theme() are placed here.
*/
/**
* 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($module, $type, &$items) {
$files = file_scan_directory(drupal_get_path('module', $module) . '/includes', '/\.' . $type . '\.inc$/', array('key' => 'name'));
foreach ($files as $file) {
require_once DRUPAL_ROOT . '/' . $file->uri;
list($tool) = explode('.', $file->name, 2);
$function = $module . '_' . str_replace ('-', '_', $tool) . '_' . str_replace('-', '_', $type);
if (function_exists($function)) {
$function($items);
}
}
}
/**
* Implementation of hook_theme_registry_alter()
*/
function ctools_theme_registry_alter(&$registry) {
// Move this one last last last so it can catch changes made by modules and themes.
$key = array_search('ctools_preprocess_page', $registry['page']['preprocess functions']);
if ($key) {
unset($registry['page']['preprocess functions'][$key]);
}
$registry['page']['preprocess functions'][] = 'ctools_preprocess_page';
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Generate new context classes by argument settings on the view.
*/
function ctools_views_get_argument_context($argument) {
if ($argument['type'] == 'context') {
if (strpos($argument['context'], '.')) {
list($context, $converter) = explode('.', $argument['context'], 2);
}
else {
// Backwards-compat for before we had a system for delimiting the data
// we retrieve out of context objects.
$context = $argument['context'];
}
if ($context == 'term' || $context == 'vocabulary') {
$context = 'entity:taxonomy_' . $context;
}
elseif ($entity = entity_get_info($context)) {
$context = 'entity:' . $context;
}
$class = 'ctools_context_' . (empty($argument['context_optional']) ? 'required' : 'optional');
$new_context = new $class($argument['label'], $context);
return $new_context;
}
}

View File

@ -0,0 +1,534 @@
<?php
/**
* @file
* CTools' multi-step form wizard tool.
*
* This tool enables the creation of multi-step forms that go from one
* form to another. The forms themselves can allow branching if they
* like, and there are a number of configurable options to how
* the wizard operates.
*
* The wizard can also be friendly to ajax forms, such as when used
* with the modal tool.
*
* The wizard provides callbacks throughout the process, allowing the
* owner to control the flow. The general flow of what happens is:
*
* Generate a form
* submit a form
* based upon button clicked, 'finished', 'next form', 'cancel' or 'return'.
*
* Each action has its own callback, so cached objects can be modifed and or
* turned into real objects. Each callback can make decisions about where to
* go next if it wishes to override the default flow.
*/
/**
* Display a multi-step form.
*
* Aside from the addition of the $form_info which contains an array of
* information and configuration so the multi-step wizard can do its thing,
* this function works a lot like drupal_build_form.
*
* Remember that the form builders for this form will receive
* &$form, &$form_state, NOT just &$form_state and no additional args.
*
* @param $form_info
* An array of form info. @todo document the array.
* @param $step
* The current form step.
* @param &$form_state
* The form state array; this is a reference so the caller can get back
* whatever information the form(s) involved left for it.
*/
function ctools_wizard_multistep_form($form_info, $step, &$form_state) {
// Make sure 'wizard' always exists for the form when dealing
// with form caching.
ctools_form_include($form_state, 'wizard');
// allow order array to be optional
if (empty($form_info['order'])) {
foreach ($form_info['forms'] as $step_id => $params) {
$form_info['order'][$step_id] = $params['title'];
}
}
if (!isset($step)) {
$keys = array_keys($form_info['order']);
$step = array_shift($keys);
}
ctools_wizard_defaults($form_info);
// If automated caching is enabled, ensure that everything is as it
// should be.
if (!empty($form_info['auto cache'])) {
// If the cache mechanism hasn't been set, default to the simple
// mechanism and use the wizard ID to ensure uniqueness so cache
// objects don't stomp on each other.
if (!isset($form_info['cache mechanism'])) {
$form_info['cache mechanism'] = 'simple::wizard::' . $form_info['id'];
}
// If not set, default the cache key to the wizard ID. This is often
// a unique ID of the object being edited or similar.
if (!isset($form_info['cache key'])) {
$form_info['cache key'] = $form_info['id'];
}
// If not set, default the cache location to storage. This is often
// somnething like 'conf'.
if (!isset($form_info['cache location'])) {
$form_info['cache location'] = 'storage';
}
// If absolutely nothing was set for the cache area to work on
if (!isset($form_state[$form_info['cache location']])) {
ctools_include('cache');
$form_state[$form_info['cache location']] = ctools_cache_get($form_info['cache mechanism'], $form_info['cache key']);
}
}
$form_state['step'] = $step;
$form_state['form_info'] = $form_info;
// Ensure we have form information for the current step.
if (!isset($form_info['forms'][$step])) {
return;
}
// Ensure that whatever include file(s) were requested by the form info are
// actually included.
$info = $form_info['forms'][$step];
if (!empty($info['include'])) {
if (is_array($info['include'])) {
foreach ($info['include'] as $file) {
ctools_form_include_file($form_state, $file);
}
}
else {
ctools_form_include_file($form_state, $info['include']);
}
}
// This tells drupal_build_form to apply our wrapper to the form. It
// will give it buttons and the like.
$form_state['wrapper_callback'] = 'ctools_wizard_wrapper';
if (!isset($form_state['rerender'])) {
$form_state['rerender'] = FALSE;
}
$form_state['no_redirect'] = TRUE;
$output = drupal_build_form($info['form id'], $form_state);
if (empty($form_state['executed']) || !empty($form_state['rerender'])) {
if (empty($form_state['title']) && !empty($info['title'])) {
$form_state['title'] = $info['title'];
}
if (!empty($form_state['ajax render'])) {
// Any include files should already be included by this point:
return $form_state['ajax render']($form_state, $output);
}
// Automatically use the modal tool if set to true.
if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
ctools_include('modal');
// This overwrites any previous commands.
$form_state['commands'] = ctools_modal_form_render($form_state, $output);
}
}
if (!empty($form_state['executed'])) {
// We use the plugins get_function format because it's powerful and
// not limited to just functions.
ctools_include('plugins');
if (isset($form_state['clicked_button']['#wizard type'])) {
$type = $form_state['clicked_button']['#wizard type'];
// If we have a callback depending upon the type of button that was
// clicked, call it.
if ($function = ctools_plugin_get_function($form_info, "$type callback")) {
$function($form_state);
}
// If auto-caching is on, we need to write the cache on next and
// clear the cache on finish.
if (!empty($form_info['auto cache'])) {
if ($type == 'next') {
ctools_include('cache');
ctools_cache_set($form_info['cache mechanism'], $form_info['cache key'], $form_state[$form_info['cache location']]);
}
elseif ($type == 'finish') {
ctools_include('cache');
ctools_cache_clear($form_info['cache mechanism'], $form_info['cache key']);
}
}
// Set a couple of niceties:
if ($type == 'finish') {
$form_state['complete'] = TRUE;
}
if ($type == 'cancel') {
$form_state['cancel'] = TRUE;
}
// If the modal is in use, some special code for it:
if (!empty($form_state['modal']) && empty($form_state['modal return'])) {
if ($type != 'next') {
// Automatically dismiss the modal if we're not going to another form.
ctools_include('modal');
$form_state['commands'][] = ctools_modal_command_dismiss();
}
}
}
if (empty($form_state['ajax'])) {
// redirect, if one is set.
if ($form_state['redirect']) {
if (is_array($form_state['redirect'])) {
call_user_func_array('drupal_goto', $form_state['redirect']);
}
else {
drupal_goto($form_state['redirect']);
}
}
}
else if (isset($form_state['ajax next'])) {
// Clear a few items off the form state so we don't double post:
$next = $form_state['ajax next'];
unset($form_state['ajax next']);
unset($form_state['executed']);
unset($form_state['post']);
unset($form_state['next']);
return ctools_wizard_multistep_form($form_info, $next, $form_state);
}
// If the callbacks wanted to do something besides go to the next form,
// it needs to have set $form_state['commands'] with something that can
// be rendered.
}
// Render ajax commands if we have any.
if (isset($form_state['ajax']) && isset($form_state['commands']) && empty($form_state['modal return'])) {
return ajax_render($form_state['commands']);
}
// Otherwise, return the output.
return $output;
}
/**
* Provide a wrapper around another form for adding multi-step information.
*/
function ctools_wizard_wrapper($form, &$form_state) {
$form_info = &$form_state['form_info'];
$info = $form_info['forms'][$form_state['step']];
// Determine the next form from this step.
// Create a form trail if we're supposed to have one.
$trail = array();
$previous = TRUE;
foreach ($form_info['order'] as $id => $title) {
if ($id == $form_state['step']) {
$previous = FALSE;
$class = 'wizard-trail-current';
}
elseif ($previous) {
$not_first = TRUE;
$class = 'wizard-trail-previous';
$form_state['previous'] = $id;
}
else {
$class = 'wizard-trail-next';
if (!isset($form_state['next'])) {
$form_state['next'] = $id;
}
if (empty($form_info['show trail'])) {
break;
}
}
if (!empty($form_info['show trail'])) {
if (!empty($form_info['free trail'])) {
// ctools_wizard_get_path() returns results suitable for
// $form_state['redirect] which can only be directly used in
// drupal_goto. We have to futz a bit with it.
$path = ctools_wizard_get_path($form_info, $id);
$options = array();
if (!empty($path[1])) {
$options = $path[1];
}
$title = l($title, $path[0], $options);
}
$trail[] = '<span class="' . $class . '">' . $title . '</span>';
}
}
// Display the trail if instructed to do so.
if (!empty($form_info['show trail'])) {
ctools_add_css('wizard');
$form['ctools_trail'] = array(
'#markup' => theme(array('ctools_wizard_trail__' . $form_info['id'], 'ctools_wizard_trail'), array('trail' => $trail)),
'#weight' => -1000,
);
}
if (empty($form_info['no buttons'])) {
// Ensure buttons stay on the bottom.
$form['buttons'] = array(
'#type' => 'actions',
'#weight' => 1000,
);
$button_attributes = array();
if (!empty($form_state['ajax']) && empty($form_state['modal'])) {
$button_attributes = array('class' => array('ctools-use-ajax'));
}
if (!empty($form_info['show back']) && isset($form_state['previous'])) {
$form['buttons']['previous'] = array(
'#type' => 'submit',
'#value' => $form_info['back text'],
'#next' => $form_state['previous'],
'#wizard type' => 'next',
'#weight' => -2000,
'#limit_validation_errors' => array(),
// hardcode the submit so that it doesn't try to save data.
'#submit' => array('ctools_wizard_submit'),
'#attributes' => $button_attributes,
);
if (isset($form_info['no back validate']) || isset($info['no back validate'])) {
$form['buttons']['previous']['#validate'] = array();
}
}
// If there is a next form, place the next button.
if (isset($form_state['next']) || !empty($form_info['free trail'])) {
$form['buttons']['next'] = array(
'#type' => 'submit',
'#value' => $form_info['next text'],
'#next' => !empty($form_info['free trail']) ? $form_state['step'] : $form_state['next'],
'#wizard type' => 'next',
'#weight' => -1000,
'#attributes' => $button_attributes,
);
}
// There are two ways the return button can appear. If this is not the
// end of the form list (i.e, there is a next) then it's "update and return"
// to be clear. If this is the end of the path and there is no next, we
// call it 'Finish'.
// Even if there is no direct return path (some forms may not want you
// leaving in the middle) the final button is always a Finish and it does
// whatever the return action is.
if (!empty($form_info['show return']) && !empty($form_state['next'])) {
$form['buttons']['return'] = array(
'#type' => 'submit',
'#value' => $form_info['return text'],
'#wizard type' => 'return',
'#attributes' => $button_attributes,
);
}
else if (empty($form_state['next']) || !empty($form_info['free trail'])) {
$form['buttons']['return'] = array(
'#type' => 'submit',
'#value' => $form_info['finish text'],
'#wizard type' => 'finish',
'#attributes' => $button_attributes,
);
}
// If we are allowed to cancel, place a cancel button.
if ((isset($form_info['cancel path']) && !isset($form_info['show cancel'])) || !empty($form_info['show cancel'])) {
$form['buttons']['cancel'] = array(
'#type' => 'submit',
'#value' => $form_info['cancel text'],
'#wizard type' => 'cancel',
// hardcode the submit so that it doesn't try to save data.
'#limit_validation_errors' => array(),
'#submit' => array('ctools_wizard_submit'),
'#attributes' => $button_attributes,
);
}
// Set up optional validate handlers.
$form['#validate'] = array();
if (function_exists($info['form id'] . '_validate')) {
$form['#validate'][] = $info['form id'] . '_validate';
}
if (isset($info['validate']) && function_exists($info['validate'])) {
$form['#validate'][] = $info['validate'];
}
// Set up our submit handler after theirs. Since putting something here will
// skip Drupal's autodetect, we autodetect for it.
// We make sure ours is after theirs so that they get to change #next if
// the want to.
$form['#submit'] = array();
if (function_exists($info['form id'] . '_submit')) {
$form['#submit'][] = $info['form id'] . '_submit';
}
if (isset($info['submit']) && function_exists($info['submit'])) {
$form['#submit'][] = $info['submit'];
}
$form['#submit'][] = 'ctools_wizard_submit';
}
if (!empty($form_state['ajax'])) {
$params = ctools_wizard_get_path($form_state['form_info'], $form_state['step']);
if (count($params) > 1) {
$url = array_shift($params);
$options = array();
$keys = array(0 => 'query', 1 => 'fragment');
foreach ($params as $key => $value) {
if (isset($keys[$key]) && isset($value)) {
$options[$keys[$key]] = $value;
}
}
$params = array($url, $options);
}
$form['#action'] = call_user_func_array('url', $params);
}
if (isset($info['wrapper']) && function_exists($info['wrapper'])) {
$form = $info['wrapper']($form, $form_state);
}
if (isset($form_info['wrapper']) && function_exists($form_info['wrapper'])) {
$form = $form_info['wrapper']($form, $form_state);
}
return $form;
}
/**
* On a submit, go to the next form.
*/
function ctools_wizard_submit(&$form, &$form_state) {
if (isset($form_state['clicked_button']['#wizard type'])) {
$type = $form_state['clicked_button']['#wizard type'];
// if AJAX enabled, we proceed slightly differently here.
if (!empty($form_state['ajax'])) {
if ($type == 'next') {
$form_state['ajax next'] = $form_state['clicked_button']['#next'];
}
}
else {
if ($type == 'cancel' && isset($form_state['form_info']['cancel path'])) {
$form_state['redirect'] = $form_state['form_info']['cancel path'];
}
else if ($type == 'next') {
$form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
if (!empty($_GET['destination'])) {
// We don't want drupal_goto redirect this request
// back. ctools_wizard_get_path ensures that the destination is
// carried over on subsequent pages.
unset($_GET['destination']);
}
}
else if (isset($form_state['form_info']['return path'])) {
$form_state['redirect'] = $form_state['form_info']['return path'];
}
else if ($type == 'finish' && isset($form_state['form_info']['cancel path'])) {
$form_state['redirect'] = $form_state['form_info']['cancel path'];
}
}
}
}
/**
* Create a path from the form info and a given step.
*/
function ctools_wizard_get_path($form_info, $step) {
if (is_array($form_info['path'])) {
foreach ($form_info['path'] as $id => $part) {
$form_info['path'][$id] = str_replace('%step', $step, $form_info['path'][$id]);
}
$path = $form_info['path'];
}
else {
$path = array(str_replace('%step', $step, $form_info['path']));
}
// If destination is set, carry it over so it'll take effect when
// saving. The submit handler will unset destination to avoid drupal_goto
// redirecting us.
if (!empty($_GET['destination'])) {
// Ensure that options is an array.
if (!isset($path[1]) || !is_array($path[1])) {
$path[1] = array();
}
// Ensure that the query part of options is an array.
$path[1] += array('query' => array());
// Add the destination parameter, if not set already.
$path[1]['query'] += drupal_get_destination();
}
return $path;
}
/**
* Set default parameters and callbacks if none are given.
* Callbacks follows pattern:
* $form_info['id']_$hook
* $form_info['id']_$form_info['forms'][$step_key]_$hook
*/
function ctools_wizard_defaults(&$form_info) {
$hook = $form_info['id'];
$defaults = array(
'show trail' => FALSE,
'free trail' => FALSE,
'show back' => FALSE,
'show cancel' => FALSE,
'show return' => FALSE,
'next text' => t('Continue'),
'back text' => t('Back'),
'return text' => t('Update and return'),
'finish text' => t('Finish'),
'cancel text' => t('Cancel'),
);
if (!empty($form_info['free trail'])) {
$defaults['next text'] = t('Update');
$defaults['finish text'] = t('Save');
}
$form_info = $form_info + $defaults;
// set form callbacks if they aren't defined
foreach ($form_info['forms'] as $step => $params) {
if (!$params['form id']) {
$form_callback = $hook . '_' . $step . '_form';
$form_info['forms'][$step]['form id'] = $form_callback;
}
}
// set button callbacks
$callbacks = array(
'back callback' => '_back',
'next callback' => '_next',
'return callback' => '_return',
'cancel callback' => '_cancel',
'finish callback' => '_finish',
);
foreach ($callbacks as $key => $callback) {
// never overwrite if explicity defined
if (empty($form_info[$key])) {
$wizard_callback = $hook . $callback;
if (function_exists($wizard_callback)) {
$form_info[$key] = $wizard_callback;
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Themable for the wizard tool.
*/
function ctools_wizard_theme(&$theme) {
$theme['ctools_wizard_trail'] = array(
'variables' => array('trail' => NULL),
'file' => 'includes/wizard.theme.inc',
);
}
/**
* Themable display of the 'breadcrumb' trail to show the order of the
* forms.
*/
function theme_ctools_wizard_trail($vars) {
$trail = $vars['trail'];
if (!empty($trail)) {
return '<div class="wizard-trail">' . implode(' » ', $trail) . '</div>';
}
}